@tsfpp/agents 1.3.2 → 1.3.3
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 +20 -0
- package/bin/bootstrap.sh +72 -10
- package/copilot/agents/tsfpp-audit.agent.md +2 -2
- package/copilot/agents/tsfpp-backfill-tests.agent.md +256 -0
- package/copilot/agents/tsfpp-refactor-engineer.agent.md +1 -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,26 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [1.3.3] - 2026-05-17
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added `copilot/agents/tsfpp-backfill-tests.agent.md` for creating tests in codebases that currently have no tests.
|
|
18
|
+
- Added `copilot/prompts/trunk-init-repo.prompt.md` for guided trunk-based repository initialization.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Minor fixes on idempotency of `init.mjs` files.
|
|
23
|
+
- Minor convenience fix for `refactor-engineer`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Updated `copilot/agents/tsfpp-audit.agent.md` to enforce inverse `undefined` checks as well, preventing shortcut patterns we do not want agents to use.
|
|
28
|
+
- Updated `copilot/copilot-instructions.md` for the same anti-shortcut rationale.
|
|
29
|
+
- Updated `copilot/instructions/tsfpp-base.instructions.md` for the same anti-shortcut rationale.
|
|
30
|
+
- Updated `init.mjs` accordingly to include deployment/support for `copilot/agents/tsfpp-backfill-tests.agent.md`.
|
|
31
|
+
- Updated `bin/bootstrap.sh` to improve bootstrap workflow behavior and script robustness.
|
|
32
|
+
|
|
13
33
|
## [1.3.2] - 2026-05-17
|
|
14
34
|
|
|
15
35
|
### 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 ""
|
|
@@ -175,7 +175,7 @@ Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@t
|
|
|
175
175
|
|
|
176
176
|
| Anti-pattern | Violation | Should be |
|
|
177
177
|
|---|---|---|
|
|
178
|
-
| `if (x === undefined)` / `if (x === null)` | MUST | `fromNullable(x)` → `Option<T
|
|
178
|
+
| `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
179
|
| `x ?? fallback` | MUST | `pipe(x, fromNullable, getOrElse(() => fallback))` |
|
|
180
180
|
| `try/catch` outside adapter boundary | MUST | `tryCatch` / `tryCatchAsync` |
|
|
181
181
|
| `.map()` on a fallible function | MUST | `traverseArray` |
|
|
@@ -191,7 +191,7 @@ Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@t
|
|
|
191
191
|
|
|
192
192
|
Checklist:
|
|
193
193
|
|
|
194
|
-
- [ ] No `if (x === undefined
|
|
194
|
+
- [ ] 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
195
|
- [ ] No `x ?? fallback` — use `getOrElse`
|
|
196
196
|
- [ ] No `try/catch` outside adapter boundaries — use `tryCatch`/`tryCatchAsync`
|
|
197
197
|
- [ ] No `.map()` on fallible function — use `traverseArray`
|
|
@@ -0,0 +1,256 @@
|
|
|
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. Focus: test."
|
|
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
|
+
---
|
|
75
|
+
|
|
76
|
+
## Execution workflow
|
|
77
|
+
|
|
78
|
+
**Step 1 — Inventory**
|
|
79
|
+
|
|
80
|
+
List all files in the target. For each file, identify:
|
|
81
|
+
- Exported symbols (functions, components, types, constants)
|
|
82
|
+
- Reachable success paths
|
|
83
|
+
- Reachable error / `None` / `Err` paths
|
|
84
|
+
- Branches, switch cases, ternary arms
|
|
85
|
+
|
|
86
|
+
Build a todo list before writing a single test.
|
|
87
|
+
|
|
88
|
+
**Step 2 — Identify test file location**
|
|
89
|
+
|
|
90
|
+
Co-locate the test file with the production file:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
src/domain/track.ts → src/domain/track.test.ts
|
|
94
|
+
src/handlers/tracks.ts → src/handlers/tracks.test.ts
|
|
95
|
+
src/features/TrackList.tsx → src/features/TrackList.test.tsx
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If a test file already exists, append to it — do not overwrite.
|
|
99
|
+
|
|
100
|
+
**Step 3 — Write tests slice by slice**
|
|
101
|
+
|
|
102
|
+
For each exported symbol, write tests in this order:
|
|
103
|
+
|
|
104
|
+
1. Primary success case
|
|
105
|
+
2. Each error / `None` / invalid-input path
|
|
106
|
+
3. Boundary values
|
|
107
|
+
4. Property tests for pure functions with `@law` annotations
|
|
108
|
+
|
|
109
|
+
Apply the correct layer pattern (see below). All tests must pass.
|
|
110
|
+
|
|
111
|
+
**Step 4 — Run the tests**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pnpm vitest run <test-file-path>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
**Step 5 — Backfill report**
|
|
120
|
+
|
|
121
|
+
Append a section to `docs/audits/backfill-<slug>-<date>.md`:
|
|
122
|
+
|
|
123
|
+
````markdown
|
|
124
|
+
## Backfill — `<file>`
|
|
125
|
+
|
|
126
|
+
**Tests written:** N
|
|
127
|
+
**Coverage added:**
|
|
128
|
+
- [x] `mkTrackId` — success path
|
|
129
|
+
- [x] `mkTrackId` — empty string → None
|
|
130
|
+
- [x] Property: any non-empty string is accepted
|
|
131
|
+
|
|
132
|
+
**Implementation gaps** (paths that cannot be tested because the implementation does not handle them):
|
|
133
|
+
- `fromUnknownTrack` does not handle missing `artistId` field — returns `err` but `getStringField` silently returns `None`; no typed error branch exists
|
|
134
|
+
|
|
135
|
+
**Uncovered by design** (paths excluded with justification):
|
|
136
|
+
- `absurd` branch in exhaustive switch — unreachable by construction
|
|
137
|
+
````
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Test rules (enforced here)
|
|
142
|
+
|
|
143
|
+
All rules from `TEST_CODING_STANDARD.md` apply:
|
|
144
|
+
|
|
145
|
+
| Rule | Constraint |
|
|
146
|
+
|---|---|
|
|
147
|
+
| 1.1 | Test observable outputs — never implementation details |
|
|
148
|
+
| 1.2 | Descriptions are full sentences describing behaviour |
|
|
149
|
+
| 1.3 | One logical assertion concept per test |
|
|
150
|
+
| 2.2 | Pure functions need fast-check property tests for `@law` annotations |
|
|
151
|
+
| 2.3 | React: RTL only |
|
|
152
|
+
| 2.4 | Network: MSW only |
|
|
153
|
+
| 3.3 | AAA structure — blank line between phases |
|
|
154
|
+
| 5.1 | No `getByTestId` |
|
|
155
|
+
| 5.2 | No `vi.fn()` for port interfaces — use in-memory stubs |
|
|
156
|
+
| 5.3 | No assertions on internal calls — assert observable outcome |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Layer-specific patterns
|
|
161
|
+
|
|
162
|
+
### `core`
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import * as fc from 'fast-check'
|
|
166
|
+
import { describe, expect, it } from 'vitest'
|
|
167
|
+
import { isSome, isNone, isOk, isErr, pipe } from '@tsfpp/prelude'
|
|
168
|
+
|
|
169
|
+
describe('mkTrackId', () => {
|
|
170
|
+
describe('when the input is a non-empty string', () => {
|
|
171
|
+
it('returns Some containing a branded TrackId', () => {
|
|
172
|
+
expect(isSome(mkTrackId('abc'))).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('when the input is empty', () => {
|
|
177
|
+
it('returns None', () => {
|
|
178
|
+
expect(mkTrackId('')).toEqual(none)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('accepts any non-empty string (property)', () => {
|
|
183
|
+
fc.assert(
|
|
184
|
+
fc.property(fc.string({ minLength: 1 }), (s) => {
|
|
185
|
+
expect(isSome(mkTrackId(s))).toBe(true)
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `api` / handler
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
it('responds with 201 and a Location header on valid input', async () => {
|
|
196
|
+
const req = new Request('http://localhost/v1/tracks', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
|
|
199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const res = await handler(req)
|
|
203
|
+
|
|
204
|
+
expect(res.status).toBe(201)
|
|
205
|
+
expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `react`
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { render, screen } from '@testing-library/react'
|
|
213
|
+
import userEvent from '@testing-library/user-event'
|
|
214
|
+
|
|
215
|
+
it('displays the track title', () => {
|
|
216
|
+
render(<TrackCard track={makeTrack({ title: 'Blue Flame' })} onSelect={none} />)
|
|
217
|
+
|
|
218
|
+
expect(screen.getByRole('heading', { name: /blue flame/i })).toBeInTheDocument()
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `dal`
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
describe('findById', () => {
|
|
226
|
+
describe('when the track exists', () => {
|
|
227
|
+
it('returns Some containing the track', async () => {
|
|
228
|
+
const track = makeTrack()
|
|
229
|
+
await repo.save(track)
|
|
230
|
+
|
|
231
|
+
const result = await repo.findById(track.id)
|
|
232
|
+
|
|
233
|
+
expect(isSome(result)).toBe(true)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('when the track does not exist', () => {
|
|
238
|
+
it('returns None', async () => {
|
|
239
|
+
const result = await repo.findById(mkTrackId('nonexistent'))
|
|
240
|
+
|
|
241
|
+
expect(isNone(result)).toBe(true)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## What you must NOT do
|
|
250
|
+
|
|
251
|
+
- Modify the implementation to make tests pass — the code is the source of truth here
|
|
252
|
+
- Skip the green-phase verification — every test must pass before you move on
|
|
253
|
+
- Write tests that assert on internal implementation details
|
|
254
|
+
- Overwrite existing passing tests
|
|
255
|
+
- Mark an implementation gap as a test failure — flag it in the report and skip
|
|
256
|
+
- Write new functionality — that belongs in `tsfpp-tdd` + `tsfpp-guarded-coding`
|
|
@@ -115,7 +115,7 @@ 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
119
|
|
|
120
120
|
**Step 2 — Work slice by slice**
|
|
121
121
|
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
|
+
}
|