@tsfpp/agents 1.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.
- package/CHANGELOG.md +39 -0
- package/README.md +118 -0
- package/claude/CLAUDE.md +110 -0
- package/copilot/agents/tsfpp-annotate.agent.md +203 -0
- package/copilot/agents/tsfpp-audit.agent.md +210 -0
- package/copilot/agents/tsfpp-guarded-coding.agent.md +175 -0
- package/copilot/agents/tsfpp-refactor-engineer.agent.md +175 -0
- package/copilot/copilot-instructions.md +51 -0
- package/copilot/instructions/tsfpp-api.instructions.md +89 -0
- package/copilot/instructions/tsfpp-base.instructions.md +87 -0
- package/copilot/instructions/tsfpp-prelude.instructions.md +141 -0
- package/copilot/instructions/tsfpp-react.instructions.md +87 -0
- package/copilot/prompts/tsfpp-boundary-review.prompt.md +152 -0
- package/copilot/prompts/tsfpp-new-module.prompt.md +152 -0
- package/init.mjs +117 -0
- package/package.json +48 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.ts"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# TSF++ prelude API
|
|
6
|
+
|
|
7
|
+
Full reference: `node_modules/@tsfpp/prelude/README.md`
|
|
8
|
+
Recipes: `node_modules/@tsfpp/prelude/RECIPES.md`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import {
|
|
14
|
+
// ADT constructors
|
|
15
|
+
some, none, ok, err,
|
|
16
|
+
// Type guards
|
|
17
|
+
isSome, isNone, isOk, isErr,
|
|
18
|
+
// Option combinators
|
|
19
|
+
mapO, flatMapO, orElse, getOrElse, fromNullable,
|
|
20
|
+
// Result combinators
|
|
21
|
+
map, flatMap, flatMapAsync, mapErr, tap, tapErr,
|
|
22
|
+
// Async adapters
|
|
23
|
+
tryCatch, tryCatchAsync,
|
|
24
|
+
// Traversal
|
|
25
|
+
traverseArray, traverseArrayO, sequenceArrayO,
|
|
26
|
+
// Pipe
|
|
27
|
+
pipe, flow, comp, complement,
|
|
28
|
+
// Utilities
|
|
29
|
+
absurd, unit,
|
|
30
|
+
// Types
|
|
31
|
+
type Option, type Result, type Unit, type Brand,
|
|
32
|
+
} from '@tsfpp/prelude'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Never `import from 'ramda'`.
|
|
36
|
+
|
|
37
|
+
## Option\<A\>
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// Construct
|
|
41
|
+
const a: Option<number> = some(42)
|
|
42
|
+
const b: Option<number> = none
|
|
43
|
+
|
|
44
|
+
// Guard before accessing .value
|
|
45
|
+
if (isSome(opt)) opt.value // safe
|
|
46
|
+
if (isNone(opt)) return ... // early exit
|
|
47
|
+
|
|
48
|
+
// Transform
|
|
49
|
+
pipe(opt, mapO(n => n + 1))
|
|
50
|
+
pipe(opt, flatMapO(n => n > 0 ? some(n) : none))
|
|
51
|
+
pipe(opt, getOrElse(() => 0))
|
|
52
|
+
pipe(opt, orElse(() => some(defaultValue)))
|
|
53
|
+
|
|
54
|
+
// Lift from nullable
|
|
55
|
+
const opt = fromNullable(maybeNull) // null | undefined → Option<T>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Result\<T, E\>
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Construct
|
|
62
|
+
const r: Result<number, string> = ok(42)
|
|
63
|
+
const e: Result<number, string> = err('oops')
|
|
64
|
+
|
|
65
|
+
// Guard before accessing .value / .error
|
|
66
|
+
if (isOk(r)) r.value // T
|
|
67
|
+
if (isErr(r)) r.error // E
|
|
68
|
+
|
|
69
|
+
// Transform
|
|
70
|
+
pipe(r, map(v => v + 1))
|
|
71
|
+
pipe(r, flatMap(v => v > 0 ? ok(v) : err('non-positive')))
|
|
72
|
+
pipe(r, mapErr(e => `Wrapped: ${e}`))
|
|
73
|
+
|
|
74
|
+
// Side effects without breaking the chain
|
|
75
|
+
pipe(r, tap(v => log.debug({ v })))
|
|
76
|
+
pipe(r, tapErr(e => log.warn({ e })))
|
|
77
|
+
|
|
78
|
+
// Async adapter — wraps throwing code
|
|
79
|
+
const result = await tryCatchAsync(
|
|
80
|
+
() => db.findById(id),
|
|
81
|
+
e => mkDbError(e),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Sync adapter
|
|
85
|
+
const result = tryCatch(
|
|
86
|
+
() => JSON.parse(raw),
|
|
87
|
+
e => `parse error: ${e}`,
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## pipe
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const result = pipe(
|
|
95
|
+
input,
|
|
96
|
+
mapO(transform),
|
|
97
|
+
flatMapO(validate),
|
|
98
|
+
getOrElse(() => fallback),
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Unit
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// Success with no meaningful value — use ok(unit), not ok(undefined)
|
|
106
|
+
const save = (): Result<Unit, DbError> => ok(unit)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## absurd
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// Exhaustiveness witness — type error if a union variant is unhandled
|
|
113
|
+
default: return absurd(x)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Brand
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
type TrackId = Brand<string, 'TrackId'>
|
|
120
|
+
|
|
121
|
+
const mkTrackId = brand<string, 'TrackId'>(
|
|
122
|
+
s => s.length > 0,
|
|
123
|
+
s => `Invalid TrackId: "${s}"`,
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Traversal
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// ReadonlyArray<A> → (A → Result<B, E>) → Result<ReadonlyArray<B>, E>
|
|
131
|
+
const results = traverseArray(validate)(items)
|
|
132
|
+
|
|
133
|
+
// ReadonlyArray<A> → (A → Option<B>) → Option<ReadonlyArray<B>>
|
|
134
|
+
const options = traverseArrayO(lookup)(items)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Discriminant convention
|
|
138
|
+
|
|
139
|
+
- Prelude ADTs use `_tag` internally — **never access it directly**
|
|
140
|
+
- Use exported guards: `isSome`, `isNone`, `isOk`, `isErr`
|
|
141
|
+
- Domain ADTs use `kind` as discriminant
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.tsx"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# TSF++ React rules
|
|
6
|
+
|
|
7
|
+
Full standard: `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`
|
|
8
|
+
Extends: tsfpp-base.instructions.md (all base rules apply to `.tsx` too)
|
|
9
|
+
|
|
10
|
+
## Component shape
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// Props: readonly record, no optional fields — use Option<T>
|
|
14
|
+
type TrackCardProps = {
|
|
15
|
+
readonly track: Track
|
|
16
|
+
readonly onSelect: Option<(id: TrackId) => void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Component: pure function, explicit return type
|
|
20
|
+
const TrackCard = ({ track, onSelect }: TrackCardProps): React.ReactElement => { ... }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## State
|
|
24
|
+
|
|
25
|
+
Model state as a discriminated union — never boolean soup:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// Yes
|
|
29
|
+
type LoadState =
|
|
30
|
+
| { readonly kind: 'idle' }
|
|
31
|
+
| { readonly kind: 'loading' }
|
|
32
|
+
| { readonly kind: 'success'; readonly data: ReadonlyArray<Track> }
|
|
33
|
+
| { readonly kind: 'error'; readonly message: string }
|
|
34
|
+
|
|
35
|
+
// No
|
|
36
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
37
|
+
const [hasError, setHasError] = useState(false)
|
|
38
|
+
const [data, setData] = useState(null)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Data fetching
|
|
42
|
+
|
|
43
|
+
Use TanStack Query. Never `useEffect` for fetching:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Yes
|
|
47
|
+
const { data, isPending, isError } = useQuery({ queryKey: ['tracks'], queryFn: fetchTracks })
|
|
48
|
+
|
|
49
|
+
// No
|
|
50
|
+
useEffect(() => { fetch('/api/tracks').then(...) }, [])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## useEffect
|
|
54
|
+
|
|
55
|
+
Allowed only for genuine external synchronisation (DOM events, third-party library lifecycle, WebSocket). Must include a comment explaining why `useEffect` is the only option:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
// NOTE(author, date): Syncing to external ResizeObserver — no React equivalent
|
|
60
|
+
const observer = new ResizeObserver(...)
|
|
61
|
+
return () => observer.disconnect()
|
|
62
|
+
}, [ref])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Forbidden in React
|
|
66
|
+
|
|
67
|
+
- `useEffect` for data fetching or derived state
|
|
68
|
+
- Prop drilling > 2 levels — lift state or use context/Jotai
|
|
69
|
+
- `any` in prop types or event handlers
|
|
70
|
+
- Mutable refs as state (`useRef` for values that drive rendering)
|
|
71
|
+
- Inline object/array literals in JSX props without `useMemo`
|
|
72
|
+
|
|
73
|
+
## Event handlers
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// Yes — named, explicit type
|
|
77
|
+
const handleSelect = (id: TrackId): void => { ... }
|
|
78
|
+
|
|
79
|
+
// No — inline arrow in JSX prop recreated every render
|
|
80
|
+
<Button onClick={() => doSomething(id)} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Memoisation
|
|
84
|
+
|
|
85
|
+
- Wrap expensive computations in `useMemo`
|
|
86
|
+
- Wrap callbacks passed to child components in `useCallback`
|
|
87
|
+
- Do not memoize everything — only when a profiler or render trace shows it matters
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scaffolds a new TSF++-compliant module with types, smart constructors, exports, JSDoc, and a test file skeleton.
|
|
3
|
+
name: TSF++ new module
|
|
4
|
+
argument-hint: "module=<name> layer=<core|api|dal|react> description=<one sentence>"
|
|
5
|
+
agent: agent
|
|
6
|
+
tools:
|
|
7
|
+
- edit/createFile
|
|
8
|
+
- edit/editFiles
|
|
9
|
+
- read/readFile
|
|
10
|
+
- search/fileSearch
|
|
11
|
+
- vscode/askQuestions
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# TSF++ new module
|
|
15
|
+
|
|
16
|
+
Scaffold a new TSF++-compliant module from scratch.
|
|
17
|
+
|
|
18
|
+
The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
|
|
19
|
+
The prelude API is at `node_modules/@tsfpp/prelude/README.md`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Required inputs
|
|
24
|
+
|
|
25
|
+
If any of the following are missing, ask for them before proceeding:
|
|
26
|
+
|
|
27
|
+
- **Module name** — e.g. `track`, `artist`, `audio-asset`
|
|
28
|
+
- **Layer** — `core` · `api` · `dal` · `react`
|
|
29
|
+
- **Domain description** — one sentence: what does this module represent or do?
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What to generate
|
|
34
|
+
|
|
35
|
+
### 1. Source file — `src/<layer>/<module-name>.ts`
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
/**
|
|
39
|
+
* @module <module-name>
|
|
40
|
+
*
|
|
41
|
+
* <Domain description>.
|
|
42
|
+
*
|
|
43
|
+
* @packageDocumentation
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { type Option, type Result, some, none, ok, err } from '@tsfpp/prelude'
|
|
47
|
+
|
|
48
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* <What this branded type represents in the domain.>
|
|
52
|
+
*/
|
|
53
|
+
export type <ModuleName>Id = Brand<string, '<ModuleName>Id'>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* <What this sum type represents. List variants.>
|
|
57
|
+
*/
|
|
58
|
+
export type <ModuleName> = {
|
|
59
|
+
readonly id: <ModuleName>Id
|
|
60
|
+
readonly <field>: <Type>
|
|
61
|
+
// … additional fields
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Errors that can occur when working with <ModuleName> values.
|
|
68
|
+
*/
|
|
69
|
+
export type <ModuleName>Error =
|
|
70
|
+
| { readonly kind: 'invalid_id'; readonly raw: string }
|
|
71
|
+
| { readonly kind: 'not_found'; readonly id: <ModuleName>Id }
|
|
72
|
+
|
|
73
|
+
// ─── Smart constructors ───────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Constructs a validated {@link <ModuleName>Id} from a raw string.
|
|
77
|
+
*
|
|
78
|
+
* @param raw - The raw string to validate.
|
|
79
|
+
* @returns `some` with a branded id when valid; `none` when the format is invalid.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const id = mk<ModuleName>Id('abc-123')
|
|
83
|
+
* // => some(<ModuleName>Id)
|
|
84
|
+
*/
|
|
85
|
+
export const mk<ModuleName>Id = (raw: string): Option<<ModuleName>Id> =>
|
|
86
|
+
raw.length > 0 ? some(raw as <ModuleName>Id) : none
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Constructs a {@link <ModuleName>} from validated inputs.
|
|
90
|
+
*
|
|
91
|
+
* @param params - Validated field values.
|
|
92
|
+
* @returns `ok` with the constructed value; `err` with a typed error on validation failure.
|
|
93
|
+
*/
|
|
94
|
+
export const mk<ModuleName> = (params: {
|
|
95
|
+
readonly id: <ModuleName>Id
|
|
96
|
+
readonly <field>: <Type>
|
|
97
|
+
}): Result<<ModuleName>, <ModuleName>Error> => {
|
|
98
|
+
// validate invariants here
|
|
99
|
+
return ok(params)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Test file — `src/<layer>/<module-name>.test.ts`
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { describe, expect, it } from 'vitest'
|
|
107
|
+
import { isSome, isNone, isOk, isErr } from '@tsfpp/prelude'
|
|
108
|
+
import { mk<ModuleName>Id, mk<ModuleName> } from './<module-name>'
|
|
109
|
+
|
|
110
|
+
describe('mk<ModuleName>Id', () => {
|
|
111
|
+
it('returns some for a valid id', () => {
|
|
112
|
+
expect(isSome(mk<ModuleName>Id('abc-123'))).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns none for an empty string', () => {
|
|
116
|
+
expect(isNone(mk<ModuleName>Id(''))).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('mk<ModuleName>', () => {
|
|
121
|
+
it('returns ok for valid inputs', () => {
|
|
122
|
+
// arrange
|
|
123
|
+
// act
|
|
124
|
+
// assert
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('returns err for invalid inputs', () => {
|
|
128
|
+
// arrange
|
|
129
|
+
// act
|
|
130
|
+
// assert
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Rules
|
|
138
|
+
|
|
139
|
+
- Never use placeholder comments like `// TODO: implement` in the source file — either implement it or use a properly formatted `// TODO(unknown, <date>): <reason>` marker.
|
|
140
|
+
- The error union must cover every failure mode the smart constructors can produce.
|
|
141
|
+
- Every exported symbol must have a JSDoc block before the file is considered complete.
|
|
142
|
+
- Test file must have at least one passing case and one failing case per smart constructor.
|
|
143
|
+
- Follow layer-specific constraints from `tsfpp-guarded-coding` for the specified layer.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Completion
|
|
148
|
+
|
|
149
|
+
Report:
|
|
150
|
+
1. Files created and their paths
|
|
151
|
+
2. Exported symbols and their types
|
|
152
|
+
3. Any invariants that still need implementing (listed as `TODO` markers in the source)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scaffolds a new TSF++-compliant module with types, smart constructors, exports, JSDoc, and a test file skeleton.
|
|
3
|
+
name: TSF++ new module
|
|
4
|
+
argument-hint: "module=<name> layer=<core|api|dal|react> description=<one sentence>"
|
|
5
|
+
agent: agent
|
|
6
|
+
tools:
|
|
7
|
+
- edit/createFile
|
|
8
|
+
- edit/editFiles
|
|
9
|
+
- read/readFile
|
|
10
|
+
- search/fileSearch
|
|
11
|
+
- vscode/askQuestions
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# TSF++ new module
|
|
15
|
+
|
|
16
|
+
Scaffold a new TSF++-compliant module from scratch.
|
|
17
|
+
|
|
18
|
+
The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
|
|
19
|
+
The prelude API is at `node_modules/@tsfpp/prelude/README.md`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Required inputs
|
|
24
|
+
|
|
25
|
+
If any of the following are missing, ask for them before proceeding:
|
|
26
|
+
|
|
27
|
+
- **Module name** — e.g. `track`, `artist`, `audio-asset`
|
|
28
|
+
- **Layer** — `core` · `api` · `dal` · `react`
|
|
29
|
+
- **Domain description** — one sentence: what does this module represent or do?
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What to generate
|
|
34
|
+
|
|
35
|
+
### 1. Source file — `src/<layer>/<module-name>.ts`
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
/**
|
|
39
|
+
* @module <module-name>
|
|
40
|
+
*
|
|
41
|
+
* <Domain description>.
|
|
42
|
+
*
|
|
43
|
+
* @packageDocumentation
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { type Option, type Result, some, none, ok, err } from '@tsfpp/prelude'
|
|
47
|
+
|
|
48
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* <What this branded type represents in the domain.>
|
|
52
|
+
*/
|
|
53
|
+
export type <ModuleName>Id = Brand<string, '<ModuleName>Id'>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* <What this sum type represents. List variants.>
|
|
57
|
+
*/
|
|
58
|
+
export type <ModuleName> = {
|
|
59
|
+
readonly id: <ModuleName>Id
|
|
60
|
+
readonly <field>: <Type>
|
|
61
|
+
// … additional fields
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Errors that can occur when working with <ModuleName> values.
|
|
68
|
+
*/
|
|
69
|
+
export type <ModuleName>Error =
|
|
70
|
+
| { readonly kind: 'invalid_id'; readonly raw: string }
|
|
71
|
+
| { readonly kind: 'not_found'; readonly id: <ModuleName>Id }
|
|
72
|
+
|
|
73
|
+
// ─── Smart constructors ───────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Constructs a validated {@link <ModuleName>Id} from a raw string.
|
|
77
|
+
*
|
|
78
|
+
* @param raw - The raw string to validate.
|
|
79
|
+
* @returns `some` with a branded id when valid; `none` when the format is invalid.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const id = mk<ModuleName>Id('abc-123')
|
|
83
|
+
* // => some(<ModuleName>Id)
|
|
84
|
+
*/
|
|
85
|
+
export const mk<ModuleName>Id = (raw: string): Option<<ModuleName>Id> =>
|
|
86
|
+
raw.length > 0 ? some(raw as <ModuleName>Id) : none
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Constructs a {@link <ModuleName>} from validated inputs.
|
|
90
|
+
*
|
|
91
|
+
* @param params - Validated field values.
|
|
92
|
+
* @returns `ok` with the constructed value; `err` with a typed error on validation failure.
|
|
93
|
+
*/
|
|
94
|
+
export const mk<ModuleName> = (params: {
|
|
95
|
+
readonly id: <ModuleName>Id
|
|
96
|
+
readonly <field>: <Type>
|
|
97
|
+
}): Result<<ModuleName>, <ModuleName>Error> => {
|
|
98
|
+
// validate invariants here
|
|
99
|
+
return ok(params)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Test file — `src/<layer>/<module-name>.test.ts`
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { describe, expect, it } from 'vitest'
|
|
107
|
+
import { isSome, isNone, isOk, isErr } from '@tsfpp/prelude'
|
|
108
|
+
import { mk<ModuleName>Id, mk<ModuleName> } from './<module-name>'
|
|
109
|
+
|
|
110
|
+
describe('mk<ModuleName>Id', () => {
|
|
111
|
+
it('returns some for a valid id', () => {
|
|
112
|
+
expect(isSome(mk<ModuleName>Id('abc-123'))).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns none for an empty string', () => {
|
|
116
|
+
expect(isNone(mk<ModuleName>Id(''))).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('mk<ModuleName>', () => {
|
|
121
|
+
it('returns ok for valid inputs', () => {
|
|
122
|
+
// arrange
|
|
123
|
+
// act
|
|
124
|
+
// assert
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('returns err for invalid inputs', () => {
|
|
128
|
+
// arrange
|
|
129
|
+
// act
|
|
130
|
+
// assert
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Rules
|
|
138
|
+
|
|
139
|
+
- Never use placeholder comments like `// TODO: implement` in the source file — either implement it or use a properly formatted `// TODO(unknown, <date>): <reason>` marker.
|
|
140
|
+
- The error union must cover every failure mode the smart constructors can produce.
|
|
141
|
+
- Every exported symbol must have a JSDoc block before the file is considered complete.
|
|
142
|
+
- Test file must have at least one passing case and one failing case per smart constructor.
|
|
143
|
+
- Follow layer-specific constraints from `tsfpp-guarded-coding` for the specified layer.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Completion
|
|
148
|
+
|
|
149
|
+
Report:
|
|
150
|
+
1. Files created and their paths
|
|
151
|
+
2. Exported symbols and their types
|
|
152
|
+
3. Any invariants that still need implementing (listed as `TODO` markers in the source)
|
package/init.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @tsfpp/agents init
|
|
4
|
+
*
|
|
5
|
+
* Copies Copilot instructions, chatmodes, prompts, and CLAUDE.md
|
|
6
|
+
* into the correct locations in the consumer's project.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm dlx @tsfpp/agents (one-shot, no install)
|
|
10
|
+
* node node_modules/@tsfpp/agents/init.mjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { createInterface } from 'node:readline';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
|
|
22
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
23
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
24
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
25
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
26
|
+
|
|
27
|
+
// ─── File map ─────────────────────────────────────────────────────────────────
|
|
28
|
+
// Each entry: [source (relative to this file), destination (relative to cwd)]
|
|
29
|
+
|
|
30
|
+
const FILES = [
|
|
31
|
+
// Always-on workspace instructions
|
|
32
|
+
['copilot/copilot-instructions.md', '.github/copilot-instructions.md'],
|
|
33
|
+
|
|
34
|
+
// Scoped instruction files
|
|
35
|
+
['copilot/instructions/tsfpp-base.instructions.md', '.github/instructions/tsfpp-base.instructions.md'],
|
|
36
|
+
['copilot/instructions/tsfpp-react.instructions.md', '.github/instructions/tsfpp-react.instructions.md'],
|
|
37
|
+
['copilot/instructions/tsfpp-api.instructions.md', '.github/instructions/tsfpp-api.instructions.md'],
|
|
38
|
+
['copilot/instructions/tsfpp-prelude.instructions.md', '.github/instructions/tsfpp-prelude.instructions.md'],
|
|
39
|
+
|
|
40
|
+
// Agents
|
|
41
|
+
['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
|
|
42
|
+
['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
|
|
43
|
+
['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
|
|
44
|
+
['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
|
|
45
|
+
|
|
46
|
+
// Reusable prompts
|
|
47
|
+
['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
|
|
48
|
+
['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
|
|
49
|
+
|
|
50
|
+
// Claude Code
|
|
51
|
+
['claude/CLAUDE.md', 'CLAUDE.md'],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async function ensureDir(filePath) {
|
|
57
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function confirm(question) {
|
|
61
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
rl.question(question, (answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(bold(' @tsfpp/agents — init'));
|
|
74
|
+
console.log(dim(' Copies Copilot and Claude Code configuration into your project.\n'));
|
|
75
|
+
|
|
76
|
+
const results = { copied: [], skipped: [], failed: [] };
|
|
77
|
+
|
|
78
|
+
for (const [src, dest] of FILES) {
|
|
79
|
+
const srcPath = join(__dirname, src);
|
|
80
|
+
const destPath = join(cwd, dest);
|
|
81
|
+
|
|
82
|
+
if (existsSync(destPath)) {
|
|
83
|
+
const overwrite = await confirm(
|
|
84
|
+
` ${yellow('!')} ${dest} already exists. Overwrite? ${dim('[y/N]')} `
|
|
85
|
+
);
|
|
86
|
+
if (!overwrite) {
|
|
87
|
+
results.skipped.push(dest);
|
|
88
|
+
console.log(` ${dim('–')} ${dim(dest)} ${dim('(skipped)')}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await ensureDir(destPath);
|
|
95
|
+
await copyFile(srcPath, destPath);
|
|
96
|
+
results.copied.push(dest);
|
|
97
|
+
console.log(` ${green('✓')} ${dest}`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
results.failed.push(dest);
|
|
100
|
+
console.log(` \x1b[31m✗\x1b[0m ${dest} ${dim(`(${err.message})`)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(dim(' ─────────────────────────────────────────'));
|
|
108
|
+
console.log(` ${green(results.copied.length + ' copied')} ${yellow(results.skipped.length + ' skipped')} ${results.failed.length > 0 ? `\x1b[31m${results.failed.length} failed\x1b[0m` : dim('0 failed')}`);
|
|
109
|
+
console.log();
|
|
110
|
+
|
|
111
|
+
if (results.failed.length === 0) {
|
|
112
|
+
console.log(' ' + bold('Done.') + ' Reload VS Code to activate Copilot instructions.');
|
|
113
|
+
console.log(dim(' Commit the generated files — they are workspace configuration.\n'));
|
|
114
|
+
} else {
|
|
115
|
+
console.log(' Some files could not be copied. Check the errors above.\n');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsfpp/agents",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Workspace AI tooling for TSF++ projects: scoped instructions, coding agents, and reusable prompts",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tsfpp",
|
|
7
|
+
"copilot",
|
|
8
|
+
"agents",
|
|
9
|
+
"instructions",
|
|
10
|
+
"claude",
|
|
11
|
+
"functional",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"homepage": "https://github.com/tsfpp/agents#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/tsfpp/agents.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/tsfpp/agents/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"tsfpp-agents": "./init.mjs"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"copilot/",
|
|
32
|
+
"claude/",
|
|
33
|
+
"init.mjs",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@tsfpp/standard": ">=1.0.0",
|
|
40
|
+
"@tsfpp/prelude": ">=1.0.0",
|
|
41
|
+
"@tsfpp/boundary": ">=1.0.0",
|
|
42
|
+
"@tsfpp/eslint-config":">=1.0.0",
|
|
43
|
+
"@tsfpp/tsconfig": ">=1.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|