@pmelab/gtd 18.0.0 → 19.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,15 @@ import {
3
3
  codeThreads,
4
4
  env,
5
5
  head,
6
+ quote,
7
+ accessFor,
6
8
  skillsFor,
7
9
  start,
8
10
  type AgentOptions,
9
11
  type SummaryContext,
10
12
  } from "../flows/index.js"
11
13
  import {
14
+ accessPreamble,
12
15
  skillsPreamble,
13
16
  styleBlock,
14
17
  styleFormatContract,
@@ -16,7 +19,6 @@ import {
16
19
  designPersona,
17
20
  architectPersona,
18
21
  reviewerPersona,
19
- specReviewerPersona,
20
22
  builderPersona,
21
23
  finisherPersona,
22
24
  escalationPersona,
@@ -37,12 +39,16 @@ export const withSkills = (skills: string | undefined, prompt: string): string =
37
39
  return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`
38
40
  }
39
41
 
40
- /** An `agent()` step whose prompt opens with the preamble naming its scope's skills. */
42
+ /** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
41
43
  export const agentWithSkills = (
42
44
  name: string,
43
45
  prompt: string,
44
46
  options: AgentOptions = {},
45
- ): Promise<void> => agent(name, withSkills(skillsFor(name).join(", "), prompt), options)
47
+ ): Promise<void> => {
48
+ const access = accessPreamble(accessFor(name, options.file))
49
+ const body = access === "" ? prompt : `${access}\n\n${prompt}`
50
+ return agent(name, withSkills(skillsFor(name).join(", "), body), options)
51
+ }
46
52
 
47
53
  export const unwindFailure = (commit: string): string =>
48
54
  `gtd could not unwind ${commit} out of your working tree.`
@@ -76,16 +82,6 @@ What each change does next (then run \`gtd land\`):
76
82
  - **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
77
83
  `
78
84
 
79
- export const architecturePreMessage = (): string =>
80
- `Judging whether \`.gtd/REQUIREMENTS.md\`'s settled concerns need a
81
- dedicated architecture pass — real structural decisions, multiple
82
- integration points, or a non-obvious tradeoff — before packages
83
- are written. Run \`gtd judge answer\` and pipe a verdict for
84
- \`architectureWarranted\` — or land untouched to run the full pass
85
- (the conservative default; a skipped judgment never suppresses
86
- it).
87
- `
88
-
89
85
  export const startGateBlockedMessage = (): string =>
90
86
  `The test baseline is red — gtd will not start new work on a broken suite.
91
87
  \`.gtd/FEEDBACK.md\` holds the failing output.
@@ -263,22 +259,46 @@ ${footnoteFoldIn}${codeThreadReplies()}
263
259
  authority is to merge only, never to split — the whole problem
264
260
  is over-granularity
265
261
  - Record every merge under \`## Merged Concerns\`,
266
- carrying both merged requirements verbatim so spec review
267
- still covers each independently
262
+ carrying both merged requirements verbatim so each requirement
263
+ stays traceable to its tests
268
264
  - A merge raises no open question and stops for no
269
265
  human — do not route it to \`architecture.gate\` for a veto; the
270
- human sees it when reviewing the plan, and spec review is the
271
- real safety net
266
+ human sees it when reviewing the plan
272
267
  - Prefer fewer, larger packages — the smallest independently
273
268
  valuable change, not the smallest change that compiles
274
269
  - Every open point here is TECHNICAL — triage already resolved
275
270
  the product ones, one phase earlier. PERMISSIVE: answer it
276
271
  yourself unless you genuinely cannot defend a default; a wrong
277
- technical call is still caught at spec review
272
+ technical call is still caught by the full run and the quality lap
278
273
  - The narrow exception: a simplification that drops something
279
274
  \`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
280
275
  silent default
281
276
 
277
+ - \`.gtd/ARCHITECTURE.md\` is the acceptance spec the human signs
278
+ off. It carries these sections, in this order:
279
+ 1. \`## Open Questions\` (qa format, first, only when present)
280
+ 2. \`## Interfaces\` — new and changed signatures/contracts
281
+ only, as fenced TypeScript (or the repository's language)
282
+ 3. \`## Call Stacks\` — seam level, entry point → module
283
+ boundary; one line per hop, never per line of code
284
+ 4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
285
+ (whatever e2e framework the repository uses), each preceded
286
+ by a line \`- e2e: <path>\`. With no user-visible
287
+ behaviour change, the literal line \`No e2e change.\` plus a
288
+ one-line reason — never empty or missing
289
+ 5. \`## Unit Tests\` — one \`### <interface>\` per entry of
290
+ \`## Interfaces\`, each test a line
291
+ \`- unit: <path> — <behaviour> (covers <concern>)\`;
292
+ never against an internal helper. A requirement no test can
293
+ cover (docs, README, deletions, config) goes under a
294
+ \`### Chores\` heading in this section as
295
+ \`- chore: <path> — <what> (covers <concern>)\`
296
+ 6. \`## Merged Concerns\` — kept, when any merge happened
297
+ 7. \`## Answered Questions\` — last
298
+ - Coverage rule: every concern in \`.gtd/REQUIREMENTS.md\` appears
299
+ in at least one \`(covers …)\` tail, as a test or a chore. Each
300
+ test entry carries its file path and level (unit / e2e)
301
+
282
302
  ${questionBar}
283
303
  ## Return lap
284
304
 
@@ -294,26 +314,41 @@ export const architectureDecomposePrompt = (): string =>
294
314
 
295
315
  ${stateFileRules}
296
316
  - The only state files this turn touches are the package files
297
- under \`.gtd/packages/\` and \`.gtd/ARCHITECTURE.md\` (deleted) —
298
- no other files for notes or output
317
+ under \`.gtd/packages/\` — no other files for notes or output.
318
+ Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
319
+ still needs it, and package files may reference it
299
320
  - Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
300
321
  conversation, otherwise read it (the converged technical
301
- plan). It already lists an ordered set of concerns with every
302
- merge/split judgement made — this turn is a mechanical
303
- write-out, not a planning one. A \`## Merged Concerns\` heading
304
- there records those merges, never a concern of its own: write
305
- no package file for it
306
- - Write one package file per concern, in the settled order,
307
- under \`.gtd/packages/\` (e.g. \`.gtd/packages/01-name.md\`,
308
- \`02-name.md\`, ...), each carrying that concern's
309
- requirement(s) — both, independently, if merged — its
310
- independent tasks, and each task's acceptance criteria as
311
- \`- [ ]\` checkboxes and relevant paths
312
- - Do not merge or split concerns here — that judgement already
313
- happened; carry the settled grouping over verbatim. No
314
- package file may reference any other \`.gtd/\` file
315
- - Once written, delete \`.gtd/ARCHITECTURE.md\`. Leave everything
316
- uncommitted and finish
322
+ plan). It already lists the concerns, interfaces, e2e scenarios
323
+ and unit tests — this turn groups them into packages. A
324
+ \`## Merged Concerns\` heading there records merges, never a
325
+ concern of its own: write no package file for it
326
+ - A package is a consecutive run of \`## Unit Tests\` entries that go
327
+ green together on the fast suite, at most ~8 unit-test entries
328
+ each. Keep the merge rule: concerns whose footprints centre on the
329
+ same files stay in one package unless the later one only consumes
330
+ an interface the earlier one creates. Prefer fewer, larger
331
+ packages. Order them so each stays green on the fast suite —
332
+ interface-introducing packages first
333
+ - Write the packages under \`.gtd/packages/\`, each carrying its
334
+ requirement(s) — both, independently, if merged — its tasks, and
335
+ each task's acceptance criteria as \`- [ ]\` checkboxes and
336
+ relevant paths:
337
+ - \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
338
+ \`## E2E Scenarios\` as tests in the repository's own e2e
339
+ framework and leaves them red. Write no such file when
340
+ \`## E2E Scenarios\` says "No e2e change."
341
+ - \`01-…\` onward — the unit-test packages. A requirement no test
342
+ covers (docs, README, deletions, config) becomes a chore
343
+ package, or a task of one; chore packages declare no tests
344
+ - the last numbered package is the wiring package: CLI entry,
345
+ workflow composition, config. A red e2e after it is a bug, not
346
+ missing work
347
+ - Every package with tests has a \`## Tests\` section, one line per
348
+ declared test, copied from the architecture entries:
349
+ \`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
350
+ none
351
+ - Leave everything uncommitted and finish
317
352
  `
318
353
 
319
354
  export const architectureGateAnswerMessage = (): string =>
@@ -346,7 +381,7 @@ What each change does next (then run \`gtd land\`):
346
381
  export const packagesItemBuildingPrompt = (pkg: string): string =>
347
382
  `${stateFileRules}
348
383
  - The only state file this turn may write is \`.gtd/SATISFIED.md\`;
349
- never delete the package file (the spec-review gate reads it
384
+ never delete the package file (the declared-tests guard reads it
350
385
  after you)
351
386
  - The package to implement is \`${pkg}\`
352
387
  - First check its acceptance criteria against the current tree —
@@ -355,6 +390,13 @@ export const packagesItemBuildingPrompt = (pkg: string): string =>
355
390
  with each criterion's concrete evidence (commit, file, or
356
391
  symbol), change nothing else, and finish. Otherwise implement
357
392
  normally and skip that file
393
+ - Write the tests the package declares under \`## Tests\` first,
394
+ then the code that makes them pass; the turn is refused unless
395
+ every declared test is in your diff
396
+ - Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
397
+ from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
398
+ step code that typechecks, and leaves them red — no
399
+ implementation
358
400
  - Implement every task the package describes, no more, no less,
359
401
  fanning independent ones out to parallel subagents where your
360
402
  harness supports it; leave other package files untouched
@@ -376,17 +418,6 @@ ${fixFeedbackPrompt}
376
418
  - Leave everything uncommitted and finish your turn — do not commit
377
419
  `
378
420
 
379
- export const packagesItemFixSpecPrompt = (pkg: string): string =>
380
- `${stateFileRules}
381
- - The only state file this turn touches is
382
- \`.gtd/SPEC_FEEDBACK.md\` — address it, then delete it
383
- - Read it (the reviewer's concerns) and the package spec
384
- (\`${pkg}\`), then fix the code to
385
- resolve every concern
386
- - Delete \`.gtd/SPEC_FEEDBACK.md\` once resolved; leave everything
387
- else uncommitted and finish your turn
388
- `
389
-
390
421
  export const healthJudgeMessage = (): string =>
391
422
  `The check is still red, and this isn't the first round —
392
423
  \`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
@@ -434,59 +465,34 @@ turn, or land untouched to give it one more attempt at the same
434
465
  analysis.
435
466
  `
436
467
 
437
- export const packagesItemSpecPreMessage = (): string =>
438
- `Judging whether the code already satisfies each requirement in the
439
- package spec, before spending a full review turn. Run \`gtd judge
440
- answer\` and pipe a verdict per section — or land untouched to run
441
- the full review (the conservative default; a skipped judgment
442
- never suppresses anything).
443
- `
444
-
445
- /** The sections a pre-judge could not clear, when it cleared the rest. */
446
- const specScope = (failing: readonly string[]): string =>
447
- failing.length > 0
448
- ? `- A pre-judge already found the other sections satisfied. Confine
449
- your review to only these sections:
450
- ${failing.map((title) => ` - ${title}\n`).join("")}`
451
- : ""
452
-
453
- export const packagesItemSpecReviewPrompt = (
454
- pkg: string,
455
- failing: readonly string[] = [],
456
- ): string =>
457
- `${styleBlock}
458
-
459
- You are reviewing a freshly-built work package against its own
460
- spec.
461
-
462
- ${stateFileRules}
463
- - The only state file this turn touches is
464
- \`.gtd/SPEC_FEEDBACK.md\` — write it only when you find problems
465
- - The package spec is \`${pkg}\`
466
- ${specScope(failing)}- Verify the implementation against it: tasks done, criteria
467
- met, code sound and consistent with the codebase. No diff is
468
- given — read the range yourself, from \`${start()}\`
469
- to the working tree, process-wide (it can span earlier
470
- packages)
471
- - You own that bar; nothing downstream re-weighs your findings
472
- - Write nothing when the package fully satisfies its spec —
473
- silence is your approval. Otherwise write
474
- \`.gtd/SPEC_FEEDBACK.md\` listing what would violate the spec if
475
- it shipped unaddressed, specific enough to act on, each as its
476
- own \`## \` heading
477
- - Never fix anything yourself and never delete the package
478
- file — a later step owns that
479
- `
480
-
481
- export const specReviewerSystem = (): string =>
482
- `${specReviewerPersona}
468
+ /** What the tail's full-run fix needs of a `BuiltPlan` (structural: `packages.ts` imports this module). */
469
+ export interface BuildContext {
470
+ readonly ranges: readonly { readonly pkg: string; readonly from: string; readonly to: string }[]
471
+ readonly scenarios: {
472
+ readonly added: readonly string[]
473
+ readonly changed: readonly string[]
474
+ }
475
+ }
483
476
 
484
- ${agentConduct}`
477
+ const buildContextPrompt = (built: BuildContext): string => {
478
+ const list = (paths: readonly string[]): string =>
479
+ paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n")
480
+ return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
481
+ - A red scenario that is NEW in this plan likely means missing wiring
482
+ between packages; new scenarios:
483
+ ${list(built.scenarios.added)}
484
+ - A red CHANGED or existing scenario or test is a regression a package
485
+ introduced; scenarios this plan changed:
486
+ ${list(built.scenarios.changed)}
487
+ - Each package's commits, to bisect a break:
488
+ ${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
489
+ `
490
+ }
485
491
 
486
- export const buildFixPrompt = (): string =>
492
+ export const buildFixPrompt = (built?: BuildContext): string =>
487
493
  `${stateFileRules}
488
494
  ${fixFeedbackPrompt}
489
- - Leave everything uncommitted — do not commit
495
+ ${built === undefined ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
490
496
  `
491
497
 
492
498
  export const finisherSystem = (): string =>
@@ -877,3 +883,25 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
877
883
  Print the closing message and stop — this writes nothing itself.
878
884
  `
879
885
  }
886
+
887
+ export interface WordingDrift {
888
+ readonly path: string
889
+ /** `- `/`+ `-prefixed, in file order. */
890
+ readonly lines: readonly string[]
891
+ }
892
+
893
+ export const scenarioWordingMessage = (at: string, drifted: readonly WordingDrift[]): string =>
894
+ `The e2e scenario wording was frozen when package 0 landed, and the last
895
+ turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
896
+
897
+ ${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
898
+
899
+ To accept the change, land untouched.
900
+
901
+ To reject it, run
902
+
903
+ git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
904
+
905
+ and land. Restoring only some of the files is a partial accept: every file
906
+ still differing from the frozen wording is accepted.
907
+ `
@@ -14,7 +14,8 @@ import {
14
14
  } from "../flows/index.js"
15
15
  import { baseline, gate } from "./health.js"
16
16
  import { packages } from "./packages.js"
17
- import { architecturePass, design } from "./planning.js"
17
+ import { architecture, design } from "./planning.js"
18
+ import type { FrozenScenarios } from "./scenarios.js"
18
19
  import { buildTail, type ReviewOutcome } from "./review.js"
19
20
  import { ARCHITECTURE, FEEDBACK, REQUIREMENTS, REVIEW } from "./steps.js"
20
21
  import * as t from "./text.js"
@@ -33,11 +34,13 @@ import * as t from "./text.js"
33
34
  export { threads, type ThreadInfo } from "../flows/index.js"
34
35
  export { defaults, envDefaults } from "./vars.js"
35
36
  export { skills } from "./skills.js"
37
+ export { access } from "./access.js"
36
38
  export { agentWithSkills } from "./text.js"
37
39
  export * from "./steps.js"
38
40
  export * from "./health.js"
39
41
  export * from "./planning.js"
40
42
  export * from "./packages.js"
43
+ export * from "./scenarios.js"
41
44
  export * from "./review.js"
42
45
 
43
46
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
@@ -69,11 +72,13 @@ export const reUnwind = async (
69
72
  /** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
70
73
  export const planAndBuild = async (firstBase: string): Promise<void> => {
71
74
  let base = firstBase
75
+ let carried: FrozenScenarios | undefined
72
76
  for (;;) {
73
77
  await design(base)
74
- await architecturePass()
75
- await packages()
76
- const outcome = await buildTail(false, base)
78
+ await architecture()
79
+ const built = await packages(carried)
80
+ carried = built.frozen
81
+ const outcome = await buildTail(false, base, built)
77
82
  if (outcome.verdict === "signoff") return
78
83
  await reUnwind(outcome)
79
84
  base = outcome.base
@@ -9,9 +9,7 @@ export const defaults: Readonly<Record<string, string>> = {
9
9
  reviewBase: "",
10
10
  judgeBudgetBytes: "32768",
11
11
  judgeIdenticalMinP: "0.7",
12
- specPreJudge: "0.9",
13
12
  reviewNoteActionable: "0.7",
14
- architectureSkipMinP: "0.85",
15
13
  // Decides the turn count: one `build.quality.<lens>` scope per entry, each
16
14
  // keyed in `./skills.ts`.
17
15
  qualityReviews:
@@ -21,6 +19,7 @@ export const defaults: Readonly<Record<string, string>> = {
21
19
  /** Environment settings. */
22
20
  export const envDefaults: Readonly<Record<string, string>> = {
23
21
  testCommand: "npm test",
22
+ fastTestCommand: "",
24
23
  plannerModel: "smart",
25
24
  coderModel: "base",
26
25
  }
@@ -1,115 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import type { Change } from "../flows/index.js"
3
- import { filterChanges, packageDiff } from "./diff.js"
4
-
5
- const change = (over: Partial<Change> & Pick<Change, "path" | "status">): Change => ({
6
- before: undefined,
7
- after: undefined,
8
- ...over,
9
- })
10
-
11
- // A cap high enough that nothing is ever dropped, for cases exercising the
12
- // rendering pipeline rather than the byte-budget behavior itself.
13
- const NO_CAP = 10_000
14
-
15
- describe("packageDiff / rendering", () => {
16
- it("renders a normal edit as hunks with three lines of context", () => {
17
- const before = Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join("\n")
18
- const after = before.replace("line 5", "line five")
19
- const text = packageDiff([change({ path: "a.ts", status: "modified", before, after })], NO_CAP)
20
- expect(text).toContain("--- a/a.ts")
21
- expect(text).toContain("+++ b/a.ts")
22
- expect(text).toContain("-line 5")
23
- expect(text).toContain("+line five")
24
- // three lines of context on either side of the one changed line
25
- expect(text).toContain(" line 2")
26
- expect(text).toContain(" line 8")
27
- expect(text).not.toContain(" line 1\n")
28
- })
29
-
30
- it("degrades a 1500+ line change to its summary line", () => {
31
- const before = Array.from({ length: 2000 }, (_, i) => `line ${i}`).join("\n")
32
- const after = Array.from({ length: 2000 }, (_, i) => `changed ${i}`).join("\n")
33
- const text = packageDiff(
34
- [change({ path: "big.ts", status: "modified", before, after })],
35
- NO_CAP,
36
- )
37
- expect(text).toBe("big.ts: +2000/-2000 lines, too large to inline")
38
- })
39
-
40
- it("renders files in path order for byte-identical output across replays", () => {
41
- const a = change({ path: "z.ts", status: "added", after: "z" })
42
- const b = change({ path: "a.ts", status: "added", after: "a" })
43
- expect(packageDiff([a, b], NO_CAP)).toBe(packageDiff([b, a], NO_CAP))
44
- expect(packageDiff([a, b], NO_CAP).indexOf("a.ts")).toBeLessThan(
45
- packageDiff([a, b], NO_CAP).indexOf("z.ts"),
46
- )
47
- })
48
- })
49
-
50
- describe("filterChanges / exclusion list", () => {
51
- it("excludes a lockfile change entirely", () => {
52
- const c = change({ path: "package-lock.json", status: "modified", before: "a", after: "b" })
53
- expect(filterChanges([c])).toEqual([])
54
- expect(packageDiff([c], NO_CAP)).toBe("")
55
- })
56
-
57
- it("excludes .gtd/** paths", () => {
58
- const c = change({ path: ".gtd/PLAN.md", status: "added", after: "x" })
59
- expect(filterChanges([c])).toEqual([])
60
- })
61
-
62
- it("excludes a binary file by extension", () => {
63
- const c = change({ path: "logo.png", status: "added", after: "binary-ish" })
64
- expect(filterChanges([c])).toEqual([])
65
- })
66
-
67
- it("excludes a binary file by content, separately from extension", () => {
68
- const c = change({ path: "weird.txt", status: "added", after: "abc\0def" })
69
- expect(filterChanges([c])).toEqual([])
70
- })
71
-
72
- it("keeps an ordinary source file", () => {
73
- const c = change({ path: "src/a.ts", status: "added", after: "export const a = 1\n" })
74
- expect(filterChanges([c])).toEqual([c])
75
- })
76
- })
77
-
78
- describe("packageDiff", () => {
79
- it("drops whole files from the end and names them in a trailer", () => {
80
- const a = change({ path: "a.ts", status: "added", after: "export const a = 1\n" })
81
- const b = change({
82
- path: "b.ts",
83
- status: "added",
84
- after: Array.from({ length: 50 }, (_, i) => `line ${i}`).join("\n"),
85
- })
86
- const text = packageDiff([a, b], 120)
87
- expect(text).toContain("--- /dev/null")
88
- expect(text).toContain("+++ b/a.ts")
89
- expect(text).not.toContain("b.ts\n")
90
- expect(text).toContain("1 file(s) omitted for the judge's byte budget: b.ts")
91
- })
92
-
93
- it("counts the omission trailer itself against the cap, so many dropped paths never push the total past it", () => {
94
- const kept = change({ path: "a.ts", status: "added", after: "export const a = 1\n" })
95
- const rest = Array.from({ length: 6 }, (_, i) =>
96
- change({ path: `f${i}.ts`, status: "added", after: "y" }),
97
- )
98
- // A naive cap check (size the kept text alone, append the trailer
99
- // afterwards) would keep a.ts plus two of the small files here — 150
100
- // bytes of text — then tack on an unbudgeted trailer for the other four,
101
- // landing well past capBytes. Budgeting the trailer itself must instead
102
- // drop enough files that the total, trailer included, stays at or under it.
103
- const capBytes = 150
104
- const text = packageDiff([kept, ...rest], capBytes)
105
- expect(Buffer.byteLength(text, "utf8")).toBeLessThanOrEqual(capBytes)
106
- expect(text).toContain("omitted")
107
- })
108
-
109
- it("keeps every file when the cap is not exceeded", () => {
110
- const a = change({ path: "a.ts", status: "added", after: "x" })
111
- const text = packageDiff([a], 10_000)
112
- expect(text).not.toContain("omitted")
113
- expect(text).toContain("a.ts")
114
- })
115
- })