@tsfpp/agents 1.0.0 → 1.0.2
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 +19 -0
- package/LICENSE +21 -0
- package/README.md +2 -1
- package/claude/CLAUDE.md +5 -5
- package/copilot/agents/tsfpp-annotate.agent.md +1 -1
- package/copilot/agents/tsfpp-audit.agent.md +4 -4
- package/copilot/agents/tsfpp-guarded-coding.agent.md +3 -3
- package/copilot/agents/tsfpp-refactor-engineer.agent.md +1 -1
- package/copilot/copilot-instructions.md +73 -35
- package/copilot/instructions/tsfpp-api.instructions.md +1 -1
- package/copilot/instructions/tsfpp-base.instructions.md +1 -1
- package/copilot/instructions/tsfpp-react.instructions.md +1 -1
- package/copilot/prompts/tsfpp-boundary-review.prompt.md +87 -128
- package/copilot/prompts/tsfpp-new-module.prompt.md +1 -1
- package/init.mjs +250 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,25 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
12
12
|
|
|
13
13
|
No changes yet.
|
|
14
14
|
|
|
15
|
+
## [1.0.2] - 2026-05-15
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Fixed standards path references across Claude and Copilot guidance files to include the missing `spec/` segment.
|
|
20
|
+
- Updated all references in this package to point to `@tsfpp/standard/spec/*` paths.
|
|
21
|
+
- Improved `init.mjs` setup flow for both existing codebases and greenfield projects by adding better ESLint and tsconfig handling.
|
|
22
|
+
|
|
23
|
+
## [1.0.1] - 2026-05-15
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Updated `init.mjs` to copy `CLAUDE.md` into `.claude/` instead of placing it at project root.
|
|
28
|
+
- Updated `README.md` to reflect the installer behavior and usage details.
|
|
29
|
+
|
|
30
|
+
### Migration
|
|
31
|
+
|
|
32
|
+
- If upgrading from `1.0.0`, remove root-level `CLAUDE.md`. The installer now places it at `.claude/CLAUDE.md`.
|
|
33
|
+
|
|
15
34
|
## [1.0.0] - 2026-05-15
|
|
16
35
|
|
|
17
36
|
### Added
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TSF++
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
package/claude/CLAUDE.md
CHANGED
|
@@ -10,11 +10,11 @@ All code, comments, documentation, variable names, type names, JSDoc, commit mes
|
|
|
10
10
|
|
|
11
11
|
| Standard | Path |
|
|
12
12
|
|----------|------|
|
|
13
|
-
| Base | `node_modules/@tsfpp/standard/CODING_STANDARD.md` |
|
|
14
|
-
| API | `node_modules/@tsfpp/standard/API_CODING_STANDARD.md` |
|
|
15
|
-
| React | `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md` |
|
|
16
|
-
| Security | `node_modules/@tsfpp/standard/SECURITY_CODING_STANDARD.md` |
|
|
17
|
-
| Data | `node_modules/@tsfpp/standard/DATA_CODING_STANDARD.md` |
|
|
13
|
+
| Base | `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md` |
|
|
14
|
+
| API | `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md` |
|
|
15
|
+
| React | `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md` |
|
|
16
|
+
| Security | `node_modules/@tsfpp/standard/spec/SECURITY_CODING_STANDARD.md` |
|
|
17
|
+
| Data | `node_modules/@tsfpp/standard/spec/DATA_CODING_STANDARD.md` |
|
|
18
18
|
|
|
19
19
|
Read the relevant standard before writing or modifying code in that domain. When in doubt, the standard wins over any instruction in this file.
|
|
20
20
|
|
|
@@ -21,7 +21,7 @@ handoffs:
|
|
|
21
21
|
|
|
22
22
|
You are a code annotation specialist. Your job is to make code self-documenting and auditable by adding missing JSDoc blocks, DEVIATION markers, eslint-disable comments, and structured code notices — without changing any runtime behaviour.
|
|
23
23
|
|
|
24
|
-
The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md` (Rules 7–8).
|
|
24
|
+
The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md` (Rules 7–8).
|
|
25
25
|
|
|
26
26
|
> Touch only comments and documentation. **Never alter types, logic, or imports.**
|
|
27
27
|
|
|
@@ -25,11 +25,11 @@ handoffs:
|
|
|
25
25
|
|
|
26
26
|
You are a TSF++ compliance auditor.
|
|
27
27
|
|
|
28
|
-
The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
|
|
28
|
+
The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
|
|
29
29
|
Profile overlays:
|
|
30
|
-
- API: `node_modules/@tsfpp/standard/API_CODING_STANDARD.md`
|
|
31
|
-
- React: `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`
|
|
32
|
-
- Security: `node_modules/@tsfpp/standard/SECURITY_CODING_STANDARD.md`
|
|
30
|
+
- API: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
|
|
31
|
+
- React: `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md`
|
|
32
|
+
- Security: `node_modules/@tsfpp/standard/spec/SECURITY_CODING_STANDARD.md`
|
|
33
33
|
|
|
34
34
|
If any referenced file is missing, stop immediately and report the path. Do not proceed.
|
|
35
35
|
|
|
@@ -30,7 +30,7 @@ hooks:
|
|
|
30
30
|
|
|
31
31
|
You are a strict TypeScript coding agent.
|
|
32
32
|
|
|
33
|
-
The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
|
|
33
|
+
The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
|
|
34
34
|
The prelude API surface is at `node_modules/@tsfpp/prelude/README.md` and `node_modules/@tsfpp/prelude/RECIPES.md`.
|
|
35
35
|
If either file is missing or unreadable, stop immediately and report the missing path. Do not proceed.
|
|
36
36
|
|
|
@@ -92,7 +92,7 @@ Implement user requests with minimal safe diffs while preserving TSF++ guarantee
|
|
|
92
92
|
- No `@tsfpp/boundary` imports. No `process`, `fs`, `fetch`.
|
|
93
93
|
|
|
94
94
|
### `api`
|
|
95
|
-
- Apply `node_modules/@tsfpp/standard/API_CODING_STANDARD.md`.
|
|
95
|
+
- Apply `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`.
|
|
96
96
|
- All input parsed and validated with Zod at the boundary.
|
|
97
97
|
- Handlers return `Promise<Response>` via `@tsfpp/boundary` response builders.
|
|
98
98
|
- Errors mapped through `apiErrorToResponse`; never raw `throw`.
|
|
@@ -106,7 +106,7 @@ Implement user requests with minimal safe diffs while preserving TSF++ guarantee
|
|
|
106
106
|
- No domain logic. No HTTP semantics. Pure data translation.
|
|
107
107
|
|
|
108
108
|
### `react`
|
|
109
|
-
- Apply `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`.
|
|
109
|
+
- Apply `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md`.
|
|
110
110
|
- Component state as discriminated union (never boolean soup).
|
|
111
111
|
- Data fetching via TanStack Query; no raw `useEffect` for fetching.
|
|
112
112
|
- `useEffect` allowed only for genuine external synchronisation; requires an explanatory comment.
|
|
@@ -30,7 +30,7 @@ hooks:
|
|
|
30
30
|
|
|
31
31
|
You are a TSF++ refactoring agent. Your input is an audit report produced by the TSF++ Audit agent. Your job is to fix every violation in that report while introducing zero new violations.
|
|
32
32
|
|
|
33
|
-
The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
|
|
33
|
+
The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
|
|
34
34
|
The prelude API surface is at `node_modules/@tsfpp/prelude/README.md` and `node_modules/@tsfpp/prelude/RECIPES.md`.
|
|
35
35
|
If either file is missing or unreadable, stop immediately and report the missing path.
|
|
36
36
|
|
|
@@ -1,51 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
applyTo: "{**/routes/**,**/handlers/**,**/api/**}/*.ts"
|
|
3
|
+
---
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# TSF++ API rules
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Full standard: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
|
|
8
|
+
Boundary API: `node_modules/@tsfpp/boundary/README.md`
|
|
9
|
+
Extends: tsfpp-base.instructions.md (all base rules apply)
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## Handler shape
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
Handlers are thin. The only permitted steps are: parse → call use-case → map response.
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
```ts
|
|
16
|
+
const createTrackHandler = async (req: Request): Promise<Response> => {
|
|
17
|
+
const ctx = extractContext(req) // 1. context
|
|
18
|
+
const body = CreateTrackSchema.safeParse(await req.json())
|
|
19
|
+
if (!body.success) return fromZodError(body.error, ctx.traceId) // 2. validate
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- Security: `node_modules/@tsfpp/standard/SECURITY_CODING_STANDARD.md`
|
|
21
|
+
const result = await createTrack(body.data) // 3. use-case
|
|
22
|
+
return pipe(result, fold(apiErrorToResponse, createdResponse)) // 4. map
|
|
23
|
+
}
|
|
24
|
+
```
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
## Boundary imports
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
All HTTP primitives come from `@tsfpp/boundary`:
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
extractContext, fromZodError, apiErrorToResponse,
|
|
33
|
+
okResponse, createdResponse, noContentResponse, acceptedResponse,
|
|
34
|
+
problemResponse, mkProblem,
|
|
35
|
+
} from '@tsfpp/boundary'
|
|
36
|
+
```
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
Never construct `new Response(...)` directly in a handler.
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
## Validation
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|------|-------|
|
|
35
|
-
| Write new TSF++-compliant code | `tsfpp-guarded-coding` |
|
|
36
|
-
| Audit a file, module, or layer for violations | `tsfpp-audit` |
|
|
37
|
-
| Fix violations from an audit report | `tsfpp-refactor-engineer` |
|
|
38
|
-
| Add JSDoc, DEVIATION comments, and code markers | `tsfpp-annotate` |
|
|
42
|
+
All input validated with Zod at the boundary. Schema lives next to the route:
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
```ts
|
|
45
|
+
const CreateTrackSchema = z.object({
|
|
46
|
+
title: z.string().min(1).max(255),
|
|
47
|
+
artistId: z.string().uuid(),
|
|
48
|
+
})
|
|
49
|
+
```
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
Never pass unvalidated `req.body` or `req.json()` into the domain.
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
## Errors
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
```ts
|
|
56
|
+
// Yes — Result propagates; mapped once at the boundary
|
|
57
|
+
const result: Result<Track, ApiError> = await createTrack(input)
|
|
58
|
+
return pipe(result, fold(apiErrorToResponse, createdResponse))
|
|
59
|
+
|
|
60
|
+
// No — throw crosses the boundary untyped
|
|
61
|
+
throw new Error('not found')
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Context
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const { traceId, principalId } = extractContext(req)
|
|
68
|
+
// Never: req.headers.get('x-trace-id') in business logic
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Status codes
|
|
72
|
+
|
|
73
|
+
| Situation | Code | Builder |
|
|
74
|
+
|-----------|------|---------|
|
|
75
|
+
| Read success | 200 | `okResponse` |
|
|
76
|
+
| Created | 201 | `createdResponse` |
|
|
77
|
+
| Accepted (async) | 202 | `acceptedResponse` |
|
|
78
|
+
| No content | 204 | `noContentResponse` |
|
|
79
|
+
| Validation failure | 422 | `fromZodError` |
|
|
80
|
+
| Not found | 404 | `problemResponse(mkProblem(404, ...))` |
|
|
81
|
+
| Conflict | 409 | `problemResponse(mkProblem(409, ...))` |
|
|
82
|
+
| Server error | 500 | `problemResponse(mkProblem(500, ...))` |
|
|
83
|
+
|
|
84
|
+
## Security
|
|
85
|
+
|
|
86
|
+
- All routes require authentication unless explicitly marked `// PUBLIC`
|
|
87
|
+
- Never log `principalId`, credentials, or request bodies at `info` level
|
|
88
|
+
- Never reflect user input in error messages without sanitisation
|
|
89
|
+
- Idempotency keys required on mutating operations — use `withIdempotency`
|
|
@@ -4,7 +4,7 @@ applyTo: "{**/routes/**,**/handlers/**,**/api/**}/*.ts"
|
|
|
4
4
|
|
|
5
5
|
# TSF++ API rules
|
|
6
6
|
|
|
7
|
-
Full standard: `node_modules/@tsfpp/standard/API_CODING_STANDARD.md`
|
|
7
|
+
Full standard: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
|
|
8
8
|
Boundary API: `node_modules/@tsfpp/boundary/README.md`
|
|
9
9
|
Extends: tsfpp-base.instructions.md (all base rules apply)
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ applyTo: "**/*.tsx"
|
|
|
4
4
|
|
|
5
5
|
# TSF++ React rules
|
|
6
6
|
|
|
7
|
-
Full standard: `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`
|
|
7
|
+
Full standard: `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md`
|
|
8
8
|
Extends: tsfpp-base.instructions.md (all base rules apply to `.tsx` too)
|
|
9
9
|
|
|
10
10
|
## Component shape
|
|
@@ -1,152 +1,111 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
3
|
-
name: TSF++
|
|
4
|
-
argument-hint: "
|
|
2
|
+
description: Targeted review of API boundary code against @tsfpp/boundary patterns and the API coding standard. Faster than a full audit — no report file, inline findings only.
|
|
3
|
+
name: TSF++ boundary review
|
|
4
|
+
argument-hint: "File(s) or directory to review, e.g. src/routes/tracks.ts"
|
|
5
5
|
agent: agent
|
|
6
6
|
tools:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- search/
|
|
7
|
+
- read
|
|
8
|
+
- search/codebase
|
|
9
|
+
- search/textSearch
|
|
10
|
+
- search/usages
|
|
11
11
|
- vscode/askQuestions
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
# TSF++
|
|
14
|
+
# TSF++ boundary review
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
A focused, read-only review of API handler and route code against the `@tsfpp/boundary` patterns and the API coding standard.
|
|
17
17
|
|
|
18
|
-
The
|
|
19
|
-
The
|
|
18
|
+
The API standard is at `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`.
|
|
19
|
+
The boundary API surface is at `node_modules/@tsfpp/boundary/README.md` and `node_modules/@tsfpp/boundary/RECIPES.md`.
|
|
20
|
+
|
|
21
|
+
> Read only. No file edits. Findings are reported inline in chat.
|
|
22
|
+
> For a full audit with a tracked report, use the `tsfpp-audit` agent with `focus: boundary`.
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
23
|
-
## Required
|
|
26
|
+
## Required input
|
|
24
27
|
|
|
25
|
-
If
|
|
28
|
+
If a target has not been provided, ask:
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
- **Layer** — `core` · `api` · `dal` · `react`
|
|
29
|
-
- **Domain description** — one sentence: what does this module represent or do?
|
|
30
|
+
> Which file(s) or directory should I review? (e.g. `src/routes/tracks.ts`, `src/routes/`)
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
33
|
-
##
|
|
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
|
-
```
|
|
34
|
+
## Checklist
|
|
102
35
|
|
|
103
|
-
|
|
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
|
-
```
|
|
36
|
+
Review every handler in the target against each item below. Report findings as a table at the end.
|
|
134
37
|
|
|
135
|
-
|
|
38
|
+
### Handler shape
|
|
39
|
+
|
|
40
|
+
- [ ] Handler is a pure function: `(req: Request) => Promise<Response>`
|
|
41
|
+
- [ ] Only three steps: parse → call use-case → map response
|
|
42
|
+
- [ ] No business logic inside the handler body
|
|
43
|
+
- [ ] No database calls, logging setup, or infrastructure imports in the handler
|
|
44
|
+
|
|
45
|
+
### Context extraction
|
|
46
|
+
|
|
47
|
+
- [ ] `extractContext` used to obtain `traceId` and `principalId`
|
|
48
|
+
- [ ] Raw headers (`req.headers.get(...)`) not accessed in business logic
|
|
49
|
+
- [ ] `traceId` passed to all `mkProblem` calls
|
|
50
|
+
|
|
51
|
+
### Input validation
|
|
52
|
+
|
|
53
|
+
- [ ] All input validated with a Zod schema before entering the domain
|
|
54
|
+
- [ ] Schema defined adjacent to the route, not inline in the handler
|
|
55
|
+
- [ ] `fromZodError` used to map `ZodError` to a typed response
|
|
56
|
+
- [ ] No unvalidated `req.json()` or `req.body` passed to the domain
|
|
57
|
+
|
|
58
|
+
### Response builders
|
|
59
|
+
|
|
60
|
+
- [ ] `okResponse` used for 200
|
|
61
|
+
- [ ] `createdResponse` used for 201
|
|
62
|
+
- [ ] `acceptedResponse` used for 202 (async operations)
|
|
63
|
+
- [ ] `noContentResponse` used for 204
|
|
64
|
+
- [ ] `problemResponse(mkProblem(...))` used for 4xx/5xx
|
|
65
|
+
- [ ] `new Response(...)` not constructed directly in a handler
|
|
66
|
+
|
|
67
|
+
### Error mapping
|
|
136
68
|
|
|
137
|
-
|
|
69
|
+
- [ ] `apiErrorToResponse` used as the single error mapping point
|
|
70
|
+
- [ ] No `throw` or `try/catch` in the handler body
|
|
71
|
+
- [ ] `fold` or `pipe` used to map `Result` to a response — no manual `if (isErr(...))` branching
|
|
138
72
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
73
|
+
### Security baseline
|
|
74
|
+
|
|
75
|
+
- [ ] Route requires authentication unless marked `// PUBLIC`
|
|
76
|
+
- [ ] `principalId` not logged at `info` level
|
|
77
|
+
- [ ] No user input reflected in error `detail` fields without sanitisation
|
|
78
|
+
- [ ] Mutating routes (`POST`, `PUT`, `PATCH`, `DELETE`) have idempotency handling or a documented reason why it is not needed
|
|
79
|
+
|
|
80
|
+
### Imports
|
|
81
|
+
|
|
82
|
+
- [ ] All boundary primitives imported from `@tsfpp/boundary`
|
|
83
|
+
- [ ] No boundary primitives re-implemented locally
|
|
144
84
|
|
|
145
85
|
---
|
|
146
86
|
|
|
147
|
-
##
|
|
87
|
+
## Output format
|
|
88
|
+
|
|
89
|
+
Report findings as a table per handler:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
## `POST /v1/tracks` — createTrackHandler
|
|
93
|
+
|
|
94
|
+
| Check | Status | Finding |
|
|
95
|
+
|-------|--------|---------|
|
|
96
|
+
| Handler shape | ✅ | — |
|
|
97
|
+
| Context extraction | ⚠️ | `req.headers.get('x-trace-id')` used directly on line 14 |
|
|
98
|
+
| Input validation | ✅ | — |
|
|
99
|
+
| Response builders | ❌ | `new Response(JSON.stringify(...), { status: 200 })` on line 31 |
|
|
100
|
+
| Error mapping | ✅ | — |
|
|
101
|
+
| Security baseline | ✅ | — |
|
|
102
|
+
| Imports | ✅ | — |
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
After all handlers, append a one-line summary:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
3 handlers reviewed · 2 findings · 1 clean
|
|
109
|
+
```
|
|
148
110
|
|
|
149
|
-
|
|
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)
|
|
111
|
+
If no findings are found, say so explicitly — do not omit the summary.
|
|
@@ -15,7 +15,7 @@ tools:
|
|
|
15
15
|
|
|
16
16
|
Scaffold a new TSF++-compliant module from scratch.
|
|
17
17
|
|
|
18
|
-
The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
|
|
18
|
+
The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
|
|
19
19
|
The prelude API is at `node_modules/@tsfpp/prelude/README.md`.
|
|
20
20
|
|
|
21
21
|
---
|
package/init.mjs
CHANGED
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @tsfpp/agents init
|
|
4
4
|
*
|
|
5
|
-
* Copies Copilot
|
|
6
|
-
* into the correct locations in the consumer's project.
|
|
5
|
+
* Copies Copilot agents, instructions, prompts, and Claude Code configuration
|
|
6
|
+
* into the correct locations in the consumer's project. Also generates
|
|
7
|
+
* eslint.config.js if not already present.
|
|
7
8
|
*
|
|
8
9
|
* Usage:
|
|
9
10
|
* pnpm dlx @tsfpp/agents (one-shot, no install)
|
|
10
11
|
* node node_modules/@tsfpp/agents/init.mjs
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
14
|
-
import { existsSync }
|
|
15
|
-
import { join, dirname }
|
|
16
|
-
import { fileURLToPath }
|
|
17
|
-
import { createInterface }
|
|
14
|
+
import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { createInterface } from 'node:readline';
|
|
18
19
|
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
const cwd = process.cwd();
|
|
@@ -48,33 +49,149 @@ const FILES = [
|
|
|
48
49
|
['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
|
|
49
50
|
|
|
50
51
|
// Claude Code
|
|
51
|
-
['claude/CLAUDE.md', 'CLAUDE.md'],
|
|
52
|
+
['claude/CLAUDE.md', '.claude/CLAUDE.md'],
|
|
52
53
|
];
|
|
53
54
|
|
|
55
|
+
// ─── ESLint config generation ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const PROFILES = {
|
|
58
|
+
base: { import: `import tsfpp from '@tsfpp/eslint-config'`, spread: 'tsfpp' },
|
|
59
|
+
react: { import: `import tsfppReact from '@tsfpp/eslint-config/react'`, spread: 'tsfppReact' },
|
|
60
|
+
api: { import: `import tsfppApi from '@tsfpp/eslint-config/api'`, spread: 'tsfppApi' },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async function detectWorkspacePackages() {
|
|
64
|
+
const wsFile = join(cwd, 'pnpm-workspace.yaml');
|
|
65
|
+
if (!existsSync(wsFile)) return null;
|
|
66
|
+
|
|
67
|
+
const yaml = await readFile(wsFile, 'utf8');
|
|
68
|
+
const patterns = [...yaml.matchAll(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/gm)]
|
|
69
|
+
.map(m => m[1].trim().replace(/\/\*\*?$/, '')); // strip trailing /* or /**
|
|
70
|
+
|
|
71
|
+
const packages = [];
|
|
72
|
+
for (const pattern of patterns) {
|
|
73
|
+
const absPattern = join(cwd, pattern);
|
|
74
|
+
if (!existsSync(absPattern)) continue;
|
|
75
|
+
const entries = await readdir(absPattern, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.isDirectory()) packages.push(`${pattern}/${entry.name}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return packages.length > 0 ? packages : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function askProfile(label) {
|
|
84
|
+
console.log(`\n Profile for ${bold(label)}:`);
|
|
85
|
+
console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
|
|
86
|
+
console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
|
|
87
|
+
console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
|
|
88
|
+
const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
|
|
89
|
+
return choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function generateMonorepoConfig(packageProfiles) {
|
|
93
|
+
// Collect which profiles are used
|
|
94
|
+
const usedProfiles = [...new Set(Object.values(packageProfiles))];
|
|
95
|
+
|
|
96
|
+
const imports = usedProfiles.map(p => PROFILES[p].import).join('\n');
|
|
97
|
+
|
|
98
|
+
// base spreads globally (no files filter); react/api are scoped per package
|
|
99
|
+
const basePackages = Object.entries(packageProfiles)
|
|
100
|
+
.filter(([, p]) => p === 'base')
|
|
101
|
+
.map(([pkg]) => `'${pkg}/src/**'`);
|
|
102
|
+
|
|
103
|
+
const scopedProfiles = ['react', 'api'].flatMap(profile => {
|
|
104
|
+
const pkgs = Object.entries(packageProfiles)
|
|
105
|
+
.filter(([, p]) => p === profile)
|
|
106
|
+
.map(([pkg]) => `'${pkg}/src/**'`);
|
|
107
|
+
if (pkgs.length === 0) return [];
|
|
108
|
+
const spread = PROFILES[profile].spread;
|
|
109
|
+
return [
|
|
110
|
+
` // ${profile}`,
|
|
111
|
+
` ...${spread}.map(c => ({ ...c, files: [${pkgs.join(', ')}] })),`,
|
|
112
|
+
];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const baseSpread = basePackages.length > 0
|
|
116
|
+
? ` // base\n ...tsfpp.map(c => ({ ...c, files: [${basePackages.join(', ')}] })),`
|
|
117
|
+
: ` // base — applies to all files not matched by a scoped profile\n ...tsfpp,`;
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
imports,
|
|
121
|
+
'',
|
|
122
|
+
'export default [',
|
|
123
|
+
baseSpread,
|
|
124
|
+
...scopedProfiles,
|
|
125
|
+
']',
|
|
126
|
+
'',
|
|
127
|
+
].join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function generateSingleConfig(profile) {
|
|
131
|
+
const { import: imp, spread } = PROFILES[profile];
|
|
132
|
+
return `${imp}\nexport default [...${spread}]\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function writeEslintConfig() {
|
|
136
|
+
const packages = await detectWorkspacePackages();
|
|
137
|
+
|
|
138
|
+
let content;
|
|
139
|
+
let description;
|
|
140
|
+
|
|
141
|
+
if (packages) {
|
|
142
|
+
console.log(`\n Monorepo detected — ${packages.length} package(s) found.\n`);
|
|
143
|
+
const packageProfiles = {};
|
|
144
|
+
for (const pkg of packages) {
|
|
145
|
+
packageProfiles[pkg] = await askProfile(pkg);
|
|
146
|
+
}
|
|
147
|
+
content = generateMonorepoConfig(packageProfiles);
|
|
148
|
+
description = 'monorepo';
|
|
149
|
+
} else {
|
|
150
|
+
const profile = await askProfile('this project');
|
|
151
|
+
content = generateSingleConfig(profile);
|
|
152
|
+
description = `profile: ${profile}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await writeFile(join(cwd, 'eslint.config.js'), content, 'utf8');
|
|
157
|
+
results.copied.push('eslint.config.js');
|
|
158
|
+
console.log(`\n ${green('✓')} eslint.config.js ${dim(`(${description})`)}`);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
results.failed.push('eslint.config.js');
|
|
161
|
+
console.log(`\n \x1b[31m✗\x1b[0m eslint.config.js ${dim(`(${err.message})`)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
54
165
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
55
166
|
|
|
56
167
|
async function ensureDir(filePath) {
|
|
57
168
|
await mkdir(dirname(filePath), { recursive: true });
|
|
58
169
|
}
|
|
59
170
|
|
|
60
|
-
async function
|
|
171
|
+
async function ask(question) {
|
|
61
172
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
62
173
|
return new Promise((resolve) => {
|
|
63
174
|
rl.question(question, (answer) => {
|
|
64
175
|
rl.close();
|
|
65
|
-
resolve(answer.trim().toLowerCase()
|
|
176
|
+
resolve(answer.trim().toLowerCase());
|
|
66
177
|
});
|
|
67
178
|
});
|
|
68
179
|
}
|
|
69
180
|
|
|
181
|
+
async function confirm(question) {
|
|
182
|
+
return (await ask(question)) === 'y';
|
|
183
|
+
}
|
|
184
|
+
|
|
70
185
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
71
186
|
|
|
72
187
|
console.log();
|
|
73
188
|
console.log(bold(' @tsfpp/agents — init'));
|
|
74
|
-
console.log(dim('
|
|
189
|
+
console.log(dim(' Sets up Copilot agents, instructions, and ESLint config.\n'));
|
|
75
190
|
|
|
76
191
|
const results = { copied: [], skipped: [], failed: [] };
|
|
77
192
|
|
|
193
|
+
// ── Copy files ────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
78
195
|
for (const [src, dest] of FILES) {
|
|
79
196
|
const srcPath = join(__dirname, src);
|
|
80
197
|
const destPath = join(cwd, dest);
|
|
@@ -101,7 +218,128 @@ for (const [src, dest] of FILES) {
|
|
|
101
218
|
}
|
|
102
219
|
}
|
|
103
220
|
|
|
104
|
-
//
|
|
221
|
+
// ── Generate eslint.config.js ─────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
console.log();
|
|
224
|
+
|
|
225
|
+
const eslintDest = join(cwd, 'eslint.config.js');
|
|
226
|
+
|
|
227
|
+
if (existsSync(eslintDest)) {
|
|
228
|
+
const overwrite = await confirm(
|
|
229
|
+
` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
|
|
230
|
+
);
|
|
231
|
+
if (!overwrite) {
|
|
232
|
+
results.skipped.push('eslint.config.js');
|
|
233
|
+
console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
|
|
234
|
+
} else {
|
|
235
|
+
await writeEslintConfig();
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
await writeEslintConfig();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function writeEslintConfig() {
|
|
242
|
+
console.log(` Which ESLint profile does this project use?`);
|
|
243
|
+
console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
|
|
244
|
+
console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
|
|
245
|
+
console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
|
|
246
|
+
|
|
247
|
+
const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
|
|
248
|
+
const profile = choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await writeFile(eslintDest, ESLINT_PROFILES[profile], 'utf8');
|
|
252
|
+
results.copied.push('eslint.config.js');
|
|
253
|
+
console.log(` ${green('✓')} eslint.config.js ${dim(`(profile: ${profile})`)}`);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
results.failed.push('eslint.config.js');
|
|
256
|
+
console.log(` \x1b[31m✗\x1b[0m eslint.config.js ${dim(`(${err.message})`)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Generate tsconfig.json ────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
console.log();
|
|
263
|
+
|
|
264
|
+
await writeTsConfigs(await detectWorkspacePackages());
|
|
265
|
+
|
|
266
|
+
async function writeTsConfigs(packages) {
|
|
267
|
+
const PRESETS = {
|
|
268
|
+
app: { extends: '@tsfpp/tsconfig/app', label: 'app — application / tool (noEmit)' },
|
|
269
|
+
lib: { extends: '@tsfpp/tsconfig/lib', label: 'lib — publishable package (declaration, composite)' },
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
async function askPreset(label) {
|
|
273
|
+
console.log(`\n tsconfig preset for ${bold(label)}:`);
|
|
274
|
+
console.log(` ${dim('1')} app ${dim('— application / tool (noEmit: true)')}`);
|
|
275
|
+
console.log(` ${dim('2')} lib ${dim('— publishable package (declaration, composite)')}`);
|
|
276
|
+
const choice = await ask(` ${dim('[1/2, default: 1]')} `);
|
|
277
|
+
return choice === '2' ? 'lib' : 'app';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function generateTsConfig(preset, extra = {}) {
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
extends: PRESETS[preset].extends,
|
|
283
|
+
compilerOptions: { rootDir: 'src', ...extra },
|
|
284
|
+
include: ['src'],
|
|
285
|
+
}, null, 2) + '\n';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function generateRootTsConfig(packagePaths) {
|
|
289
|
+
return JSON.stringify({
|
|
290
|
+
files: [],
|
|
291
|
+
references: packagePaths.map(p => ({ path: p })),
|
|
292
|
+
}, null, 2) + '\n';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function writeIfConfirmed(destPath, content, label) {
|
|
296
|
+
if (existsSync(destPath)) {
|
|
297
|
+
const overwrite = await confirm(
|
|
298
|
+
` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
|
|
299
|
+
);
|
|
300
|
+
if (!overwrite) {
|
|
301
|
+
results.skipped.push(label);
|
|
302
|
+
console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped)')}`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
await ensureDir(destPath);
|
|
308
|
+
await writeFile(destPath, content, 'utf8');
|
|
309
|
+
results.copied.push(label);
|
|
310
|
+
console.log(` ${green('✓')} ${label}`);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
results.failed.push(label);
|
|
313
|
+
console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (packages) {
|
|
318
|
+
// Monorepo: tsconfig per package + root references
|
|
319
|
+
console.log(` Generating tsconfig.json per package.\n`);
|
|
320
|
+
const packagePresets = {};
|
|
321
|
+
for (const pkg of packages) {
|
|
322
|
+
packagePresets[pkg] = await askPreset(pkg);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const [pkg, preset] of Object.entries(packagePresets)) {
|
|
326
|
+
const destPath = join(cwd, pkg, 'tsconfig.json');
|
|
327
|
+
const content = generateTsConfig(preset);
|
|
328
|
+
await writeIfConfirmed(destPath, content, `${pkg}/tsconfig.json`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Root references tsconfig
|
|
332
|
+
const rootDest = join(cwd, 'tsconfig.json');
|
|
333
|
+
const rootContent = generateRootTsConfig(packages);
|
|
334
|
+
await writeIfConfirmed(rootDest, rootContent, 'tsconfig.json (root references)');
|
|
335
|
+
} else {
|
|
336
|
+
// Single package
|
|
337
|
+
const preset = await askPreset('this project');
|
|
338
|
+
const dest = join(cwd, 'tsconfig.json');
|
|
339
|
+
const content = generateTsConfig(preset);
|
|
340
|
+
await writeIfConfirmed(dest, content, 'tsconfig.json');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
105
343
|
|
|
106
344
|
console.log();
|
|
107
345
|
console.log(dim(' ─────────────────────────────────────────'));
|