@tsfpp/agents 1.3.2 → 1.3.4
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/CHANGELOG.md +29 -0
- package/bin/bootstrap.sh +72 -10
- package/copilot/agents/tsfpp-audit.agent.md +24 -14
- package/copilot/agents/tsfpp-backfill-tests.agent.md +262 -0
- package/copilot/agents/tsfpp-refactor-engineer.agent.md +3 -1
- package/copilot/copilot-instructions.md +1 -1
- package/copilot/instructions/tsfpp-base.instructions.md +1 -1
- package/copilot/prompts/trunk-init-repo.prompt.md +115 -0
- package/init.mjs +8 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [1.3.4] - 2026-05-18
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Updated `audit`, `backfill-tests`, and `refactor-engineer` so they do not hand off too easily when workable slices of implementation are readily available.
|
|
18
|
+
- Updated audit/backfill log filename convention to include both time and focus.
|
|
19
|
+
- Improved handover flow from `backfill-tests` to `audit`.
|
|
20
|
+
- Added additional summary domains to report on in the audit log.
|
|
21
|
+
|
|
22
|
+
## [1.3.3] - 2026-05-17
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added `copilot/agents/tsfpp-backfill-tests.agent.md` for creating tests in codebases that currently have no tests.
|
|
27
|
+
- Added `copilot/prompts/trunk-init-repo.prompt.md` for guided trunk-based repository initialization.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Minor fixes on idempotency of `init.mjs` files.
|
|
32
|
+
- Minor convenience fix for `refactor-engineer`.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Updated `copilot/agents/tsfpp-audit.agent.md` to enforce inverse `undefined` checks as well, preventing shortcut patterns we do not want agents to use.
|
|
37
|
+
- Updated `copilot/copilot-instructions.md` for the same anti-shortcut rationale.
|
|
38
|
+
- Updated `copilot/instructions/tsfpp-base.instructions.md` for the same anti-shortcut rationale.
|
|
39
|
+
- Updated `init.mjs` accordingly to include deployment/support for `copilot/agents/tsfpp-backfill-tests.agent.md`.
|
|
40
|
+
- Updated `bin/bootstrap.sh` to improve bootstrap workflow behavior and script robustness.
|
|
41
|
+
|
|
13
42
|
## [1.3.2] - 2026-05-17
|
|
14
43
|
|
|
15
44
|
### Fixed
|
package/bin/bootstrap.sh
CHANGED
|
@@ -24,13 +24,23 @@ if [[ -n "$PROJECT_NAME" ]]; then
|
|
|
24
24
|
ok "Created directory: $PROJECT_NAME"
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
-
# ── 1.
|
|
27
|
+
# ── 1. git init ───────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
30
|
+
ok "Git repository already exists — skipping git init"
|
|
31
|
+
else
|
|
32
|
+
dim "Initialising git repository…"
|
|
33
|
+
git init -b main > /dev/null
|
|
34
|
+
ok "git init -b main"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ── 2. pnpm init ──────────────────────────────────────────────────────────────
|
|
28
38
|
|
|
29
39
|
dim "Initialising package.json…"
|
|
30
40
|
pnpm init --yes > /dev/null
|
|
31
41
|
ok "pnpm init"
|
|
32
42
|
|
|
33
|
-
# ──
|
|
43
|
+
# ── 3. Install TSF++ ecosystem ────────────────────────────────────────────────
|
|
34
44
|
|
|
35
45
|
dim "Installing TSF++ packages (this may take a moment)…"
|
|
36
46
|
pnpm add -D \
|
|
@@ -43,7 +53,7 @@ pnpm add -D \
|
|
|
43
53
|
@tsfpp/agents
|
|
44
54
|
ok "Installed TSF++ ecosystem"
|
|
45
55
|
|
|
46
|
-
# ──
|
|
56
|
+
# ── 4. tsconfig.json ──────────────────────────────────────────────────────────
|
|
47
57
|
|
|
48
58
|
dim "Writing tsconfig.json…"
|
|
49
59
|
cat > tsconfig.json << 'EOF'
|
|
@@ -57,7 +67,7 @@ cat > tsconfig.json << 'EOF'
|
|
|
57
67
|
EOF
|
|
58
68
|
ok "tsconfig.json"
|
|
59
69
|
|
|
60
|
-
# ──
|
|
70
|
+
# ── 5. eslint.config.js ───────────────────────────────────────────────────────
|
|
61
71
|
|
|
62
72
|
dim "Writing eslint.config.js…"
|
|
63
73
|
cat > eslint.config.js << 'EOF'
|
|
@@ -66,7 +76,7 @@ export default tsfpp
|
|
|
66
76
|
EOF
|
|
67
77
|
ok "eslint.config.js"
|
|
68
78
|
|
|
69
|
-
# ──
|
|
79
|
+
# ── 6. package.json — type + scripts ─────────────────────────────────────────
|
|
70
80
|
|
|
71
81
|
dim "Patching package.json…"
|
|
72
82
|
npm pkg set type="module" --silent
|
|
@@ -75,7 +85,7 @@ npm pkg set scripts.lint="eslint src" --silent
|
|
|
75
85
|
npm pkg set scripts.check="pnpm typecheck && pnpm lint" --silent
|
|
76
86
|
ok "package.json scripts"
|
|
77
87
|
|
|
78
|
-
# ──
|
|
88
|
+
# ── 7. src/index.ts ───────────────────────────────────────────────────────────
|
|
79
89
|
|
|
80
90
|
dim "Creating src/index.ts…"
|
|
81
91
|
mkdir -p src
|
|
@@ -87,17 +97,69 @@ cat > src/index.ts << 'EOF'
|
|
|
87
97
|
EOF
|
|
88
98
|
ok "src/index.ts"
|
|
89
99
|
|
|
90
|
-
# ──
|
|
100
|
+
# ── 8. Copilot agents (.github) ───────────────────────────────────────────────
|
|
91
101
|
|
|
92
102
|
dim "Installing Copilot agents…"
|
|
93
103
|
node node_modules/@tsfpp/agents/init.mjs
|
|
94
104
|
ok "Copilot agents installed"
|
|
95
105
|
|
|
96
|
-
# ──
|
|
106
|
+
# ── 9. .gitignore ────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
if [[ ! -f .gitignore ]]; then
|
|
109
|
+
dim "Writing .gitignore…"
|
|
110
|
+
cat > .gitignore << 'GITIGNORE'
|
|
111
|
+
# Dependencies
|
|
112
|
+
node_modules/
|
|
113
|
+
|
|
114
|
+
# Build output
|
|
115
|
+
dist/
|
|
116
|
+
build/
|
|
117
|
+
out/
|
|
118
|
+
coverage/
|
|
119
|
+
.turbo/
|
|
120
|
+
|
|
121
|
+
# TypeScript
|
|
122
|
+
*.tsbuildinfo
|
|
123
|
+
|
|
124
|
+
# Environment
|
|
125
|
+
.env
|
|
126
|
+
.env.*
|
|
127
|
+
!.env.example
|
|
128
|
+
|
|
129
|
+
# OS
|
|
130
|
+
.DS_Store
|
|
131
|
+
Thumbs.db
|
|
132
|
+
|
|
133
|
+
# Editor
|
|
134
|
+
.vscode/*
|
|
135
|
+
!.vscode/extensions.json
|
|
136
|
+
!.vscode/settings.json
|
|
137
|
+
.idea/
|
|
138
|
+
|
|
139
|
+
# pnpm
|
|
140
|
+
.pnpm-store/
|
|
141
|
+
|
|
142
|
+
# Logs
|
|
143
|
+
*.log
|
|
144
|
+
npm-debug.log*
|
|
145
|
+
pnpm-debug.log*
|
|
146
|
+
GITIGNORE
|
|
147
|
+
ok ".gitignore"
|
|
148
|
+
else
|
|
149
|
+
ok ".gitignore already exists — skipping"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# ── 10. Husky ─────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
dim "Activating Husky hooks…"
|
|
155
|
+
pnpm exec husky install > /dev/null 2>&1 && ok "Husky hooks activated" || ok "Husky not configured — skipping (run 'pnpm exec husky install' after adding husky to package.json)"
|
|
156
|
+
|
|
157
|
+
# ── 11. Done ──────────────────────────────────────────────────────────────────
|
|
97
158
|
|
|
98
159
|
echo ""
|
|
99
160
|
echo -e "${GREEN}TSF++ sandbox ready.${RESET}"
|
|
100
161
|
echo ""
|
|
101
|
-
echo " pnpm check
|
|
102
|
-
echo " code .
|
|
162
|
+
echo " pnpm check — typecheck + lint"
|
|
163
|
+
echo " code . — open in VS Code"
|
|
164
|
+
echo " /trunk-init-repo — initialise git remote and push to GitHub"
|
|
103
165
|
echo ""
|
|
@@ -39,13 +39,13 @@ If any referenced file is missing, stop immediately and report the path. Do not
|
|
|
39
39
|
|
|
40
40
|
## Session start
|
|
41
41
|
|
|
42
|
-
If
|
|
42
|
+
If `target` and `focus` are present in the message (e.g. `target=src/ focus=test`) or can be inferred from handoff context (e.g. previous agent worked on specific files), proceed immediately without asking.
|
|
43
|
+
|
|
44
|
+
If and only if either is missing and cannot be inferred, ask once:
|
|
43
45
|
|
|
44
46
|
> **Target** — path, package name, or layer to audit (e.g. `src/domain`, `@tsfpp/prelude`, `api layer`)?
|
|
45
47
|
> **Focus** — `all` · `types` · `boundary` · `complexity` · `loc` · `annotations` · `security` · `react` · `data` · `prelude` · `test` · or comma-separated combination?
|
|
46
48
|
|
|
47
|
-
Do not proceed until both are confirmed.
|
|
48
|
-
|
|
49
49
|
---
|
|
50
50
|
|
|
51
51
|
## Mission
|
|
@@ -59,7 +59,7 @@ Systematically inspect the target for TSF++ violations. Slice the work into mana
|
|
|
59
59
|
Create the report file **before starting any inspection**:
|
|
60
60
|
|
|
61
61
|
```
|
|
62
|
-
docs/audits/<target-slug>-<YYYYMMDD-HHmm>.md
|
|
62
|
+
docs/audits/<target-slug>-<focus>-<YYYYMMDD-HHmm>.md
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Use this template exactly:
|
|
@@ -79,13 +79,20 @@ Use this template exactly:
|
|
|
79
79
|
|
|
80
80
|
> Fill in after all slices are complete.
|
|
81
81
|
|
|
82
|
-
| Category | Violations | Deviations | Passed |
|
|
83
|
-
|
|
84
|
-
| Types | — | — | — |
|
|
85
|
-
| Purity | — | — | — |
|
|
86
|
-
| Boundary | — | — | — |
|
|
87
|
-
| Annotations | — | — | — |
|
|
88
|
-
| Complexity | — | — | — |
|
|
82
|
+
| Category | Violations | Deviations | Passed | N/A |
|
|
83
|
+
|-------------|-----------|------------|--------|-----|
|
|
84
|
+
| Types | — | — | — | — |
|
|
85
|
+
| Purity | — | — | — | — |
|
|
86
|
+
| Boundary | — | — | — | — |
|
|
87
|
+
| Annotations | — | — | — | — |
|
|
88
|
+
| Complexity | — | — | — | — |
|
|
89
|
+
| Prelude | — | — | — | — |
|
|
90
|
+
| React | — | — | — | — |
|
|
91
|
+
| Data | — | — | — | — |
|
|
92
|
+
| Security | — | — | — | — |
|
|
93
|
+
| Tests | — | — | — | — |
|
|
94
|
+
|
|
95
|
+
_N/A — focus not applicable to this target (e.g. React row when no `.tsx` files in scope)_
|
|
89
96
|
|
|
90
97
|
---
|
|
91
98
|
|
|
@@ -175,7 +182,7 @@ Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@t
|
|
|
175
182
|
|
|
176
183
|
| Anti-pattern | Violation | Should be |
|
|
177
184
|
|---|---|---|
|
|
178
|
-
| `if (x === undefined)` / `if (x === null)` | MUST | `fromNullable(x)` → `Option<T
|
|
185
|
+
| `if (x === undefined)` / `if (x !== undefined)` / `if (x === null)` / `if (x !== null)` / `if (!x)` | MUST | `fromNullable(x)` → `Option<T>`; use `isSome` / `isNone` to branch |
|
|
179
186
|
| `x ?? fallback` | MUST | `pipe(x, fromNullable, getOrElse(() => fallback))` |
|
|
180
187
|
| `try/catch` outside adapter boundary | MUST | `tryCatch` / `tryCatchAsync` |
|
|
181
188
|
| `.map()` on a fallible function | MUST | `traverseArray` |
|
|
@@ -191,7 +198,7 @@ Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@t
|
|
|
191
198
|
|
|
192
199
|
Checklist:
|
|
193
200
|
|
|
194
|
-
- [ ] No `if (x === undefined
|
|
201
|
+
- [ ] No nullability checks in any form — `if (x === undefined)`, `if (x !== undefined)`, `if (x === null)`, `if (x !== null)`, `if (!x)`, `x ?? y` — use `fromNullable` / `getOrElse` / `isSome`
|
|
195
202
|
- [ ] No `x ?? fallback` — use `getOrElse`
|
|
196
203
|
- [ ] No `try/catch` outside adapter boundaries — use `tryCatch`/`tryCatchAsync`
|
|
197
204
|
- [ ] No `.map()` on fallible function — use `traverseArray`
|
|
@@ -259,7 +266,10 @@ All focus areas above in sequence. For `.tsx` files, include `react` automatical
|
|
|
259
266
|
List all files in scope. Group into logical slices (≤ 300 LOC per slice, or one cohesive module). Populate the slice index table in the report.
|
|
260
267
|
|
|
261
268
|
**Step 2 — Create report**
|
|
262
|
-
Write `docs/audits/<slug>-<
|
|
269
|
+
Write `docs/audits/<target-slug>-<focus>-<YYYYMMDD-HHmm>.md` with the template above before touching any source file.
|
|
270
|
+
Example: `docs/audits/src-domain-prelude-20260517-1430.md` or `docs/audits/src-all-20260517-0900.md`.
|
|
271
|
+
|
|
272
|
+
> **Do not suggest handoffs or pause between slices.** Work through all slices without interruption. Update the report after each slice. Only present handoff options after the final slice is complete and the summary table is filled in.
|
|
263
273
|
|
|
264
274
|
**Step 3 — Inspect slice by slice**
|
|
265
275
|
For each slice:
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >
|
|
3
|
+
Writes tests for existing code that has no test coverage. Reads the
|
|
4
|
+
implementation, derives the implicit contract, writes passing tests that
|
|
5
|
+
specify that contract, and reports uncovered edge cases and error paths.
|
|
6
|
+
Use this for retroactive coverage — not for new functionality (use
|
|
7
|
+
tsfpp-tdd for that).
|
|
8
|
+
name: tsfpp-backfill-tests
|
|
9
|
+
argument-hint: "target=<path|module|layer>"
|
|
10
|
+
tools:
|
|
11
|
+
- edit/createFile
|
|
12
|
+
- edit/editFiles
|
|
13
|
+
- execute/runInTerminal
|
|
14
|
+
- execute/getTerminalOutput
|
|
15
|
+
- execute/testFailure
|
|
16
|
+
- read
|
|
17
|
+
- search
|
|
18
|
+
- todo
|
|
19
|
+
- vscode/askQuestions
|
|
20
|
+
handoffs:
|
|
21
|
+
- label: Audit test coverage
|
|
22
|
+
agent: tsfpp-audit
|
|
23
|
+
prompt: "Audit the test files just written for TSF++ compliance. Use the same target as this backfill session. Focus: test. Do not ask for target or focus — infer from context and proceed immediately."
|
|
24
|
+
send: false
|
|
25
|
+
- label: Fix uncovered paths in implementation
|
|
26
|
+
agent: tsfpp-guarded-coding
|
|
27
|
+
prompt: "The backfill report lists uncovered edge cases and error paths in the implementation. Address them."
|
|
28
|
+
send: false
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# TSF++ Backfill Tests
|
|
32
|
+
|
|
33
|
+
You write tests for **existing code that has no test coverage**.
|
|
34
|
+
|
|
35
|
+
Full testing standard: `node_modules/@tsfpp/standard/spec/TEST_CODING_STANDARD.md`
|
|
36
|
+
Full coding standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
37
|
+
|
|
38
|
+
> This agent is for **retroactive coverage** only.
|
|
39
|
+
> For new functionality, use `tsfpp-tdd` instead — tests must come before implementation.
|
|
40
|
+
> Tests you write here must pass against the existing implementation.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Session start
|
|
45
|
+
|
|
46
|
+
Infer the layer per file from the path and contents:
|
|
47
|
+
|
|
48
|
+
| Signal | Layer |
|
|
49
|
+
|--------|-------|
|
|
50
|
+
| `.tsx`, React imports | `react` |
|
|
51
|
+
| `handler`, `route`, `@tsfpp/boundary` imports | `api` |
|
|
52
|
+
| `repository`, `db`, `drizzle`, query builders | `dal` |
|
|
53
|
+
| `argv`, `process`, `cli` | `cli` |
|
|
54
|
+
| Pure types, domain logic, no framework imports | `core` |
|
|
55
|
+
|
|
56
|
+
State the layer and proceed immediately. Ask only if the layer is genuinely ambiguous.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Mission
|
|
61
|
+
|
|
62
|
+
1. Read the existing implementation thoroughly.
|
|
63
|
+
2. Derive the implicit contract from the code.
|
|
64
|
+
3. Write passing tests that make that contract explicit.
|
|
65
|
+
4. Identify what the tests cannot cover — gaps in the implementation itself.
|
|
66
|
+
5. Produce a backfill report.
|
|
67
|
+
|
|
68
|
+
You succeed when:
|
|
69
|
+
- Every public export has at least one test for its primary success case
|
|
70
|
+
- Every reachable error path has a corresponding test
|
|
71
|
+
- Every branch and switch case is exercised
|
|
72
|
+
- All tests pass green
|
|
73
|
+
|
|
74
|
+
> **Do not suggest handoffs or pause between slices.** Work through all slices
|
|
75
|
+
> without interruption. Intermediate lint/typecheck runs and report updates are
|
|
76
|
+
> expected and correct. Only present handoff options after the final slice is
|
|
77
|
+
> complete and the backfill report is finished.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Execution workflow
|
|
82
|
+
|
|
83
|
+
**Step 1 — Inventory**
|
|
84
|
+
|
|
85
|
+
List all files in the target. For each file, identify:
|
|
86
|
+
- Exported symbols (functions, components, types, constants)
|
|
87
|
+
- Reachable success paths
|
|
88
|
+
- Reachable error / `None` / `Err` paths
|
|
89
|
+
- Branches, switch cases, ternary arms
|
|
90
|
+
|
|
91
|
+
Build a todo list before writing a single test.
|
|
92
|
+
|
|
93
|
+
**Step 2 — Identify test file location**
|
|
94
|
+
|
|
95
|
+
Co-locate the test file with the production file:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
src/domain/track.ts → src/domain/track.test.ts
|
|
99
|
+
src/handlers/tracks.ts → src/handlers/tracks.test.ts
|
|
100
|
+
src/features/TrackList.tsx → src/features/TrackList.test.tsx
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If a test file already exists, append to it — do not overwrite.
|
|
104
|
+
|
|
105
|
+
**Step 3 — Write tests slice by slice**
|
|
106
|
+
|
|
107
|
+
For each exported symbol, write tests in this order:
|
|
108
|
+
|
|
109
|
+
1. Primary success case
|
|
110
|
+
2. Each error / `None` / invalid-input path
|
|
111
|
+
3. Boundary values
|
|
112
|
+
4. Property tests for pure functions with `@law` annotations
|
|
113
|
+
|
|
114
|
+
Apply the correct layer pattern (see below). All tests must pass.
|
|
115
|
+
|
|
116
|
+
**Step 4 — Run the tests**
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pnpm vitest run <test-file-path>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
All tests must be green. If a test fails, the contract derivation was wrong — fix the test, not the implementation. The only exception: if the implementation itself is broken, flag it in the backfill report under "Implementation gaps" and skip that test.
|
|
123
|
+
|
|
124
|
+
**Step 5 — Backfill report**
|
|
125
|
+
|
|
126
|
+
Append a section to `docs/audits/backfill-<target-slug>-<YYYYMMDD-HHmm>.md`:
|
|
127
|
+
Example: `docs/audits/backfill-src-domain-20260517-1430.md`.
|
|
128
|
+
|
|
129
|
+
````markdown
|
|
130
|
+
## Backfill — `<file>`
|
|
131
|
+
|
|
132
|
+
**Tests written:** N
|
|
133
|
+
**Coverage added:**
|
|
134
|
+
- [x] `mkTrackId` — success path
|
|
135
|
+
- [x] `mkTrackId` — empty string → None
|
|
136
|
+
- [x] Property: any non-empty string is accepted
|
|
137
|
+
|
|
138
|
+
**Implementation gaps** (paths that cannot be tested because the implementation does not handle them):
|
|
139
|
+
- `fromUnknownTrack` does not handle missing `artistId` field — returns `err` but `getStringField` silently returns `None`; no typed error branch exists
|
|
140
|
+
|
|
141
|
+
**Uncovered by design** (paths excluded with justification):
|
|
142
|
+
- `absurd` branch in exhaustive switch — unreachable by construction
|
|
143
|
+
````
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Test rules (enforced here)
|
|
148
|
+
|
|
149
|
+
All rules from `TEST_CODING_STANDARD.md` apply:
|
|
150
|
+
|
|
151
|
+
| Rule | Constraint |
|
|
152
|
+
|---|---|
|
|
153
|
+
| 1.1 | Test observable outputs — never implementation details |
|
|
154
|
+
| 1.2 | Descriptions are full sentences describing behaviour |
|
|
155
|
+
| 1.3 | One logical assertion concept per test |
|
|
156
|
+
| 2.2 | Pure functions need fast-check property tests for `@law` annotations |
|
|
157
|
+
| 2.3 | React: RTL only |
|
|
158
|
+
| 2.4 | Network: MSW only |
|
|
159
|
+
| 3.3 | AAA structure — blank line between phases |
|
|
160
|
+
| 5.1 | No `getByTestId` |
|
|
161
|
+
| 5.2 | No `vi.fn()` for port interfaces — use in-memory stubs |
|
|
162
|
+
| 5.3 | No assertions on internal calls — assert observable outcome |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Layer-specific patterns
|
|
167
|
+
|
|
168
|
+
### `core`
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import * as fc from 'fast-check'
|
|
172
|
+
import { describe, expect, it } from 'vitest'
|
|
173
|
+
import { isSome, isNone, isOk, isErr, pipe } from '@tsfpp/prelude'
|
|
174
|
+
|
|
175
|
+
describe('mkTrackId', () => {
|
|
176
|
+
describe('when the input is a non-empty string', () => {
|
|
177
|
+
it('returns Some containing a branded TrackId', () => {
|
|
178
|
+
expect(isSome(mkTrackId('abc'))).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('when the input is empty', () => {
|
|
183
|
+
it('returns None', () => {
|
|
184
|
+
expect(mkTrackId('')).toEqual(none)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('accepts any non-empty string (property)', () => {
|
|
189
|
+
fc.assert(
|
|
190
|
+
fc.property(fc.string({ minLength: 1 }), (s) => {
|
|
191
|
+
expect(isSome(mkTrackId(s))).toBe(true)
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `api` / handler
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
it('responds with 201 and a Location header on valid input', async () => {
|
|
202
|
+
const req = new Request('http://localhost/v1/tracks', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const res = await handler(req)
|
|
209
|
+
|
|
210
|
+
expect(res.status).toBe(201)
|
|
211
|
+
expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `react`
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { render, screen } from '@testing-library/react'
|
|
219
|
+
import userEvent from '@testing-library/user-event'
|
|
220
|
+
|
|
221
|
+
it('displays the track title', () => {
|
|
222
|
+
render(<TrackCard track={makeTrack({ title: 'Blue Flame' })} onSelect={none} />)
|
|
223
|
+
|
|
224
|
+
expect(screen.getByRole('heading', { name: /blue flame/i })).toBeInTheDocument()
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `dal`
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
describe('findById', () => {
|
|
232
|
+
describe('when the track exists', () => {
|
|
233
|
+
it('returns Some containing the track', async () => {
|
|
234
|
+
const track = makeTrack()
|
|
235
|
+
await repo.save(track)
|
|
236
|
+
|
|
237
|
+
const result = await repo.findById(track.id)
|
|
238
|
+
|
|
239
|
+
expect(isSome(result)).toBe(true)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('when the track does not exist', () => {
|
|
244
|
+
it('returns None', async () => {
|
|
245
|
+
const result = await repo.findById(mkTrackId('nonexistent'))
|
|
246
|
+
|
|
247
|
+
expect(isNone(result)).toBe(true)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## What you must NOT do
|
|
256
|
+
|
|
257
|
+
- Modify the implementation to make tests pass — the code is the source of truth here
|
|
258
|
+
- Skip the green-phase verification — every test must pass before you move on
|
|
259
|
+
- Write tests that assert on internal implementation details
|
|
260
|
+
- Overwrite existing passing tests
|
|
261
|
+
- Mark an implementation gap as a test failure — flag it in the report and skip
|
|
262
|
+
- Write new functionality — that belongs in `tsfpp-tdd` + `tsfpp-guarded-coding`
|
|
@@ -115,7 +115,9 @@ If a function exceeds 40 lines, cyclomatic complexity 10, or nesting depth 4: de
|
|
|
115
115
|
## Execution workflow
|
|
116
116
|
|
|
117
117
|
**Step 1 — Read the report**
|
|
118
|
-
Parse the audit report. Build a todo list of all open violations grouped by slice.
|
|
118
|
+
Parse the audit report. Build a todo list of all open violations grouped by slice. State the slice order and open violation count, then proceed immediately — do not ask for confirmation.
|
|
119
|
+
|
|
120
|
+
> **Do not suggest handoffs or pause between slices.** Work through all violations without interruption. Intermediate lint/typecheck runs and report updates are expected and correct. Only present handoff options after every violation is resolved and the report is marked complete.
|
|
119
121
|
|
|
120
122
|
**Step 2 — Work slice by slice**
|
|
121
123
|
For each slice with open violations:
|
|
@@ -26,7 +26,7 @@ Canonical source: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md` — whe
|
|
|
26
26
|
- `default:` in an exhaustive switch — use `absurd(x)` instead
|
|
27
27
|
- `import from 'ramda'` — use `@tsfpp/prelude`
|
|
28
28
|
- `new Map()` `new Set()` — use `intoMap` / `intoSet` from `@tsfpp/prelude`
|
|
29
|
-
- `if (x === null)` `if (x === undefined)` `x ?? y` — use `fromNullable` / `getOrElse`
|
|
29
|
+
- `if (x === null)` `if (x !== null)` `if (x === undefined)` `if (x !== undefined)` `if (!x)` `x ?? y` — any nullability check in any form; use `fromNullable` → `Option<T>`, then `isSome` / `isNone` / `getOrElse`
|
|
30
30
|
- `try/catch` in core — use `tryCatch` / `tryCatchAsync` from `@tsfpp/prelude`
|
|
31
31
|
|
|
32
32
|
## Always
|
|
@@ -22,7 +22,7 @@ Full standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
|
22
22
|
- `default:` in an exhaustive switch — use `absurd(x)` instead
|
|
23
23
|
- `import from 'ramda'` — use `@tsfpp/prelude`
|
|
24
24
|
- `new Map()` `new Set()` — use `intoMap` / `intoSet` from `@tsfpp/prelude`
|
|
25
|
-
- `if (x === null)` `if (x === undefined)` `x ?? y` — use `fromNullable` / `getOrElse`
|
|
25
|
+
- `if (x === null)` `if (x !== null)` `if (x === undefined)` `if (x !== undefined)` `if (!x)` `x ?? y` — any nullability check in any form; use `fromNullable` → `Option<T>`, then `isSome` / `isNone` / `getOrElse`
|
|
26
26
|
- `try/catch` in core — use `tryCatch` / `tryCatchAsync` from `@tsfpp/prelude`
|
|
27
27
|
|
|
28
28
|
## Always
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Initialize git repository
|
|
2
|
+
|
|
3
|
+
Set up a clean git repository for this TSF++ project with a `main` branch, a
|
|
4
|
+
proper `.gitignore`, an initial conventional commit, and an optional remote.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
|
|
10
|
+
### 1 — Check git status
|
|
11
|
+
|
|
12
|
+
Run `git status` to determine whether a repository already exists.
|
|
13
|
+
|
|
14
|
+
- If already initialized: report the current branch and proceed to step 3.
|
|
15
|
+
- If not initialized: run `git init -b main` and confirm.
|
|
16
|
+
|
|
17
|
+
### 2 — Create `.gitignore`
|
|
18
|
+
|
|
19
|
+
If no `.gitignore` exists, create one. If one exists, verify it covers at
|
|
20
|
+
minimum all entries below and append any that are missing.
|
|
21
|
+
|
|
22
|
+
```gitignore
|
|
23
|
+
# Dependencies
|
|
24
|
+
node_modules/
|
|
25
|
+
|
|
26
|
+
# Build output
|
|
27
|
+
dist/
|
|
28
|
+
build/
|
|
29
|
+
out/
|
|
30
|
+
coverage/
|
|
31
|
+
.turbo/
|
|
32
|
+
|
|
33
|
+
# TypeScript
|
|
34
|
+
*.tsbuildinfo
|
|
35
|
+
|
|
36
|
+
# Environment
|
|
37
|
+
.env
|
|
38
|
+
.env.*
|
|
39
|
+
!.env.example
|
|
40
|
+
|
|
41
|
+
# OS
|
|
42
|
+
.DS_Store
|
|
43
|
+
Thumbs.db
|
|
44
|
+
|
|
45
|
+
# Editor
|
|
46
|
+
.vscode/*
|
|
47
|
+
!.vscode/extensions.json
|
|
48
|
+
!.vscode/settings.json
|
|
49
|
+
.idea/
|
|
50
|
+
|
|
51
|
+
# pnpm
|
|
52
|
+
.pnpm-store/
|
|
53
|
+
|
|
54
|
+
# Logs
|
|
55
|
+
*.log
|
|
56
|
+
npm-debug.log*
|
|
57
|
+
pnpm-debug.log*
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3 — Stage and commit
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git add .
|
|
64
|
+
git status
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Show the staged file list. Then commit:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git commit -m "chore: initial project setup"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The commit message follows Conventional Commits. Do not use a different format.
|
|
74
|
+
|
|
75
|
+
### 4 — Remote (optional)
|
|
76
|
+
|
|
77
|
+
Ask once:
|
|
78
|
+
|
|
79
|
+
> Do you want to add a remote? If so, paste the repository URL (or press Enter
|
|
80
|
+
> to skip):
|
|
81
|
+
|
|
82
|
+
If a URL is provided:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git remote add origin <url>
|
|
86
|
+
git push -u origin main
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This is the only legitimate direct push to `main` in the lifetime of this
|
|
90
|
+
repository. After this, all changes go through branches and pull requests.
|
|
91
|
+
|
|
92
|
+
Then activate Husky hooks (they may have silently failed during `pnpm install`
|
|
93
|
+
if git was not yet initialized at that point):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pnpm exec husky install
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If skipped, confirm the local repository is ready and suggest adding a remote
|
|
100
|
+
later with:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git remote add origin <url>
|
|
104
|
+
git push -u origin main
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Rules
|
|
110
|
+
|
|
111
|
+
- Never force-push (`--force`) on `main`.
|
|
112
|
+
- Never amend the initial commit once pushed.
|
|
113
|
+
- If `git push` fails due to an existing remote history, stop and report — do
|
|
114
|
+
not `--force`.
|
|
115
|
+
- Do not create any branches other than `main` during this flow.
|
package/init.mjs
CHANGED
|
@@ -42,12 +42,14 @@ const FILES = [
|
|
|
42
42
|
|
|
43
43
|
// Agents
|
|
44
44
|
['copilot/agents/tsfpp-tdd.agent.md', '.github/agents/tsfpp-tdd.agent.md'],
|
|
45
|
+
['copilot/agents/tsfpp-backfill-tests.agent.md', '.github/agents/tsfpp-backfill-tests.agent.md'],
|
|
45
46
|
['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
|
|
46
47
|
['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
|
|
47
48
|
['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
|
|
48
49
|
['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
|
|
49
50
|
|
|
50
51
|
// Reusable prompts
|
|
52
|
+
['copilot/prompts/trunk-init-repo.prompt.md', '.github/prompts/trunk-init-repo.prompt.md'],
|
|
51
53
|
['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
|
|
52
54
|
['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
|
|
53
55
|
|
|
@@ -78,13 +80,17 @@ async function detectWorkspacePackages() {
|
|
|
78
80
|
const patterns = [...yaml.matchAll(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/gm)]
|
|
79
81
|
.map(m => m[1].trim().replace(/\/\*\*?$/, '')); // strip trailing /* or /**
|
|
80
82
|
|
|
83
|
+
const IGNORE = new Set(['dist', 'build', 'out', 'coverage', 'node_modules', '.git', '.turbo', 'tmp']);
|
|
84
|
+
|
|
81
85
|
const packages = [];
|
|
82
86
|
for (const pattern of patterns) {
|
|
83
87
|
const absPattern = join(cwd, pattern);
|
|
84
88
|
if (!existsSync(absPattern)) continue;
|
|
85
89
|
const entries = await readdir(absPattern, { withFileTypes: true });
|
|
86
90
|
for (const entry of entries) {
|
|
87
|
-
if (entry.isDirectory()
|
|
91
|
+
if (entry.isDirectory() && !IGNORE.has(entry.name)) {
|
|
92
|
+
packages.push(`${pattern}/${entry.name}`);
|
|
93
|
+
}
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
return packages.length > 0 ? packages : null;
|
|
@@ -248,8 +254,6 @@ if (existsSync(eslintDest)) {
|
|
|
248
254
|
await writeEslintConfig();
|
|
249
255
|
}
|
|
250
256
|
|
|
251
|
-
|
|
252
|
-
|
|
253
257
|
// ── Generate tsconfig.json ────────────────────────────────────────────────────
|
|
254
258
|
|
|
255
259
|
console.log();
|
|
@@ -345,4 +349,4 @@ if (results.failed.length === 0) {
|
|
|
345
349
|
} else {
|
|
346
350
|
console.log(' Some files could not be copied. Check the errors above.\n');
|
|
347
351
|
process.exit(1);
|
|
348
|
-
}
|
|
352
|
+
}
|