Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "functype-developer",
|
||||
"description": "Assist contributors developing the functype library - architecture patterns, creating new types, testing strategies",
|
||||
"version": "0.0.0-2025.11.28",
|
||||
"author": {
|
||||
"name": "Jordan Burke",
|
||||
"email": "jordan.burke@gmail.com"
|
||||
},
|
||||
"skills": [
|
||||
"./skills/functype-developer"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# functype-developer
|
||||
|
||||
Assist contributors developing the functype library - architecture patterns, creating new types, testing strategies
|
||||
64
plugin.lock.json
Normal file
64
plugin.lock.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:jordanburke/functype:functype-developer",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "6603aaddc47ed6e63a4a985c0f87f8721ae4e26d",
|
||||
"treeHash": "d37c45926becc57acfb493824600a4729689edcdb44ee92eacb411a216a17c9a",
|
||||
"generatedAt": "2025-11-28T10:19:19.171683Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "functype-developer",
|
||||
"description": "Assist contributors developing the functype library - architecture patterns, creating new types, testing strategies"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "10cf1772de9714f02565076c68736c288f55dcadbf070b6c094f983d558ddf19"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "6cb228bda39d16de3fd927a5ea5a9c4debcd40c1145e791031646f4b60ebf354"
|
||||
},
|
||||
{
|
||||
"path": "skills/functype-developer/SKILL.md",
|
||||
"sha256": "e7c769880e9a89cab71d0e1c74b9700ba0589d6f8fc5a77160a82adba7429062"
|
||||
},
|
||||
{
|
||||
"path": "skills/functype-developer/references/architecture.md",
|
||||
"sha256": "4134b03bca497d0078a659b51e02a28f22f6fecfa7b1c73ecbc2c4fb318c72db"
|
||||
},
|
||||
{
|
||||
"path": "skills/functype-developer/references/testing-patterns.md",
|
||||
"sha256": "d6ffb3a191c1bcbe064960d0be1b9b0e78e832a9edb5e3f6f76b36e4758d4f69"
|
||||
},
|
||||
{
|
||||
"path": "skills/functype-developer/references/adding-types.md",
|
||||
"sha256": "6cd138f168486ab0fa464bc607e8c5f6d364398df0ef301043f6ac287314946e"
|
||||
},
|
||||
{
|
||||
"path": "skills/functype-developer/scripts/validate.sh",
|
||||
"sha256": "95fded8e7bacb0d0dcc67e40d971b45fb4df1590472b4e79bb9234e9bc29e98f"
|
||||
},
|
||||
{
|
||||
"path": "skills/functype-developer/scripts/new-type-template.sh",
|
||||
"sha256": "cb834148d1e63bf1d13f9e52306ac33fc07c94d8f030b795be430633fc9168d6"
|
||||
}
|
||||
],
|
||||
"dirSha256": "d37c45926becc57acfb493824600a4729689edcdb44ee92eacb411a216a17c9a"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
558
skills/functype-developer/SKILL.md
Normal file
558
skills/functype-developer/SKILL.md
Normal file
@@ -0,0 +1,558 @@
|
||||
---
|
||||
name: functype-developer
|
||||
description: Assist contributors working on the functype library codebase. Use this skill when creating new data structures, implementing functional interfaces (Functor, Monad, Foldable), adding tests, debugging library internals, or understanding functype's Scala-inspired architecture patterns including the Base pattern, HKT system, and Companion utilities.
|
||||
---
|
||||
|
||||
# Functype Library Developer
|
||||
|
||||
## Overview
|
||||
|
||||
Guide for contributing to the functype TypeScript library. This skill provides architectural patterns, development workflows, and tooling for implementing new functional data structures following functype's Scala-inspired design.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Trigger this skill when:
|
||||
|
||||
- Creating new data structures or types
|
||||
- Implementing functional interfaces (Functor, Monad, Foldable, etc.)
|
||||
- Adding or fixing tests
|
||||
- Understanding library architecture
|
||||
- Debugging functional type implementations
|
||||
- Following the Base pattern and type class system
|
||||
- Working with the Feature Matrix
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js**: ≥ 18.0.0
|
||||
- **pnpm**: 10.12.1
|
||||
- **TypeScript**: Strict mode enabled
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Development (build with watch)
|
||||
pnpm dev
|
||||
|
||||
# Before committing (MUST PASS)
|
||||
pnpm validate
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Run specific test file
|
||||
pnpm vitest run test/specific.spec.ts
|
||||
|
||||
# Check types without building
|
||||
pnpm compile
|
||||
```
|
||||
|
||||
### Pre-Commit Checklist
|
||||
|
||||
**Always run before committing:**
|
||||
|
||||
```bash
|
||||
pnpm validate
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
1. **Format**: Prettier formatting
|
||||
2. **Lint**: ESLint checks
|
||||
3. **Test**: All Vitest tests
|
||||
4. **Build**: Production build
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Scala-Inspired Constructor Pattern
|
||||
|
||||
All types follow this pattern:
|
||||
|
||||
```typescript
|
||||
// Constructor function returns object with methods
|
||||
const option = Option(value) // Constructor
|
||||
option.map((x) => x + 1) // Instance methods
|
||||
Option.none() // Companion methods
|
||||
```
|
||||
|
||||
**NOT class-based:**
|
||||
|
||||
```typescript
|
||||
// ❌ Don't do this
|
||||
class Option<T> {
|
||||
constructor(value: T) { ... }
|
||||
}
|
||||
|
||||
// ✅ Do this
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
return value == null ? none() : some(value)
|
||||
}
|
||||
```
|
||||
|
||||
### Base Pattern
|
||||
|
||||
Use the `Base` function to add common functionality to all types:
|
||||
|
||||
```typescript
|
||||
import { Base } from "@/core/base"
|
||||
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
if (value == null) {
|
||||
return Base("None", {
|
||||
// methods here
|
||||
map: <B>(f: (val: T) => B) => Option.none<B>(),
|
||||
// ...
|
||||
})
|
||||
}
|
||||
|
||||
return Base("Some", {
|
||||
// methods here
|
||||
map: <B>(f: (val: T) => B) => Option(f(value)),
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Base provides:**
|
||||
|
||||
- `Typeable` interface (type metadata)
|
||||
- Standard `toString()` method
|
||||
- Consistent object structure
|
||||
|
||||
### Type System Foundation
|
||||
|
||||
**Base constraint:**
|
||||
|
||||
```typescript
|
||||
import type { Type } from "@/functor"
|
||||
|
||||
// Use Type instead of any
|
||||
function process<T extends Type>(value: T): void {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Never use `any`**:
|
||||
|
||||
- Use `unknown` for uncertain types
|
||||
- Use `Type` for generic constraints
|
||||
- Use proper type definitions
|
||||
|
||||
### Functional Interfaces
|
||||
|
||||
Every container type should implement these when applicable:
|
||||
|
||||
**Core interfaces** (see `references/architecture.md` for details):
|
||||
|
||||
- `Functor` - map over values
|
||||
- `Applicative` - apply wrapped functions
|
||||
- `Monad` - flatMap for sequencing
|
||||
- `Foldable` - extract via pattern matching
|
||||
- `Traversable` - collection operations
|
||||
- `Serializable` - JSON/YAML/binary serialization
|
||||
|
||||
**Reference the Feature Matrix:**
|
||||
Check `docs/FUNCTYPE_FEATURE_MATRIX.md` to see which interfaces each type implements and what methods are required.
|
||||
|
||||
## Creating a New Data Structure
|
||||
|
||||
### Step-by-Step Guide
|
||||
|
||||
**1. Research existing patterns**
|
||||
|
||||
```bash
|
||||
# Use the Explore agent to understand the codebase
|
||||
# Look at Option, Either, or Try as reference implementations
|
||||
```
|
||||
|
||||
**2. Create module structure**
|
||||
|
||||
```bash
|
||||
mkdir -p src/mynewtype
|
||||
touch src/mynewtype/index.ts
|
||||
touch test/mynewtype.spec.ts
|
||||
```
|
||||
|
||||
**3. Use the template script**
|
||||
|
||||
```bash
|
||||
# Run the new-type-template script
|
||||
./claude/skills/functype-developer/scripts/new-type-template.sh MyNewType
|
||||
```
|
||||
|
||||
This generates:
|
||||
|
||||
- Basic type structure
|
||||
- Required interface implementations
|
||||
- Test file boilerplate
|
||||
|
||||
**4. Implement the type**
|
||||
|
||||
Follow the constructor pattern:
|
||||
|
||||
```typescript
|
||||
// src/mynewtype/index.ts
|
||||
import { Base } from "@/core/base"
|
||||
import type { Functor } from "@/functor"
|
||||
|
||||
export type MyNewType<T> = Functor<T> & {
|
||||
// Your methods here
|
||||
getValue(): T
|
||||
}
|
||||
|
||||
export function MyNew<T>(value: T): MyNewType<T> {
|
||||
return Base("MyNewType", {
|
||||
// Functor
|
||||
map: <B>(f: (val: T) => B): MyNewType<B> => {
|
||||
return MyNew(f(value))
|
||||
},
|
||||
|
||||
// Your methods
|
||||
getValue: () => value,
|
||||
|
||||
// Pipe for composition
|
||||
pipe: () => ({
|
||||
map: (f: (val: T) => any) => MyNew(value).map(f),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// Companion methods
|
||||
MyNew.empty = <T>() => MyNew<T>(null as any)
|
||||
```
|
||||
|
||||
**5. Add exports**
|
||||
|
||||
Update `src/index.ts`:
|
||||
|
||||
```typescript
|
||||
export { MyNew } from "./mynewtype"
|
||||
export type { MyNewType } from "./mynewtype"
|
||||
```
|
||||
|
||||
Update `package.json` exports:
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
"./mynewtype": {
|
||||
"import": "./dist/esm/mynewtype/index.js",
|
||||
"require": "./dist/cjs/mynewtype/index.js",
|
||||
"types": "./dist/types/mynewtype/index.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**6. Write comprehensive tests**
|
||||
|
||||
```typescript
|
||||
// test/mynewtype.spec.ts
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { MyNew } from "@/mynewtype"
|
||||
|
||||
describe("MyNewType", () => {
|
||||
describe("Construction", () => {
|
||||
it("should create from value", () => {
|
||||
const value = MyNew(5)
|
||||
expect(value.getValue()).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Functor", () => {
|
||||
it("should map over values", () => {
|
||||
const result = MyNew(5).map((x) => x * 2)
|
||||
expect(result.getValue()).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
// More tests...
|
||||
})
|
||||
```
|
||||
|
||||
**7. Update Feature Matrix**
|
||||
|
||||
Add your type to `docs/FUNCTYPE_FEATURE_MATRIX.md` showing which interfaces it implements.
|
||||
|
||||
**8. Validate**
|
||||
|
||||
```bash
|
||||
pnpm validate
|
||||
```
|
||||
|
||||
### Interface Implementation Checklist
|
||||
|
||||
When implementing interfaces, refer to this checklist:
|
||||
|
||||
**Functor:**
|
||||
|
||||
- [ ] `map<B>(f: (value: A) => B): Functor<B>`
|
||||
|
||||
**Applicative (extends Functor):**
|
||||
|
||||
- [ ] `ap<B>(ff: Applicative<(value: A) => B>): Applicative<B>`
|
||||
|
||||
**Monad (extends Applicative):**
|
||||
|
||||
- [ ] `flatMap<B>(f: (value: A) => Monad<B>): Monad<B>`
|
||||
|
||||
**Foldable:**
|
||||
|
||||
- [ ] `fold<B>(onEmpty: () => B, onValue: (value: A) => B): B`
|
||||
- [ ] `foldLeft<B>(z: B): (op: (b: B, a: A) => B) => B`
|
||||
- [ ] `foldRight<B>(z: B): (op: (a: A, b: B) => B) => B`
|
||||
|
||||
See `references/architecture.md` for complete interface definitions.
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Test Structure
|
||||
|
||||
Use Vitest with describe/it pattern:
|
||||
|
||||
```typescript
|
||||
describe("TypeName", () => {
|
||||
describe("Feature Group", () => {
|
||||
it("should do specific thing", () => {
|
||||
// Arrange
|
||||
const input = createInput()
|
||||
|
||||
// Act
|
||||
const result = performOperation(input)
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Property-Based Testing
|
||||
|
||||
Use fast-check for property tests:
|
||||
|
||||
```typescript
|
||||
import { fc, test } from "@fast-check/vitest"
|
||||
|
||||
test.prop([fc.integer()])("should always return positive", (n) => {
|
||||
const result = Math.abs(n)
|
||||
expect(result).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
```
|
||||
|
||||
### Edge Cases to Test
|
||||
|
||||
Always test:
|
||||
|
||||
- Empty/null/undefined inputs
|
||||
- Type inference correctness
|
||||
- Method chaining
|
||||
- Error cases
|
||||
- Immutability (original unchanged)
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Imports
|
||||
|
||||
```typescript
|
||||
// Type-only imports when possible
|
||||
import type { Type } from "@/functor"
|
||||
import { Option } from "@/option"
|
||||
|
||||
// Organized with simple-import-sort (automatic)
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
// ✅ Use Type for constraints
|
||||
function process<T extends Type>(value: T): void
|
||||
|
||||
// ✅ Prefer types over interfaces
|
||||
export type MyType<T> = {
|
||||
value: T
|
||||
}
|
||||
|
||||
// ✅ Explicit type annotations
|
||||
function transform<T>(input: T): MyType<T> {
|
||||
return { value: input }
|
||||
}
|
||||
```
|
||||
|
||||
### Naming
|
||||
|
||||
```typescript
|
||||
// PascalCase for types
|
||||
type OptionType<T> = { ... }
|
||||
|
||||
// camelCase for functions/variables
|
||||
const someValue = Option(5)
|
||||
function mapOption<T>(opt: OptionType<T>) { ... }
|
||||
|
||||
// Constructor functions are PascalCase
|
||||
Option(value)
|
||||
Either(value)
|
||||
```
|
||||
|
||||
### Functional Style
|
||||
|
||||
```typescript
|
||||
// ✅ Immutability
|
||||
const newList = list.append(item)
|
||||
|
||||
// ❌ Mutation
|
||||
list.push(item)
|
||||
|
||||
// ✅ Pure functions
|
||||
function double(x: number): number {
|
||||
return x * 2
|
||||
}
|
||||
|
||||
// ❌ Side effects
|
||||
function double(x: number): number {
|
||||
console.log(x) // side effect
|
||||
return x * 2
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern Matching
|
||||
|
||||
```typescript
|
||||
// ✅ Use Cond for conditionals
|
||||
import { Cond } from "@/cond"
|
||||
|
||||
Cond.start<string>()
|
||||
.case(x > 10, "big")
|
||||
.case(x > 5, "medium")
|
||||
.otherwise("small")
|
||||
|
||||
// ✅ Use Match for switches
|
||||
import { Match } from "@/match"
|
||||
|
||||
Match(status)
|
||||
.case("success", () => handleSuccess())
|
||||
.case("error", () => handleError())
|
||||
.done()
|
||||
|
||||
// ❌ Avoid early returns
|
||||
// Use Cond or Option instead
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a Helper Method
|
||||
|
||||
```typescript
|
||||
// Add to type definition
|
||||
export type MyType<T> = {
|
||||
// existing methods...
|
||||
|
||||
// New helper
|
||||
isEmpty(): boolean
|
||||
}
|
||||
|
||||
// Implement in constructor
|
||||
export function MyType<T>(value: T): MyType<T> {
|
||||
return Base("MyType", {
|
||||
// existing methods...
|
||||
|
||||
isEmpty: () => value == null,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Implementing Serialization
|
||||
|
||||
```typescript
|
||||
import { createSerializable } from "@/core/serializable"
|
||||
|
||||
export function MyType<T>(value: T): MyType<T> {
|
||||
return Base("MyType", {
|
||||
// other methods...
|
||||
|
||||
serialize: () =>
|
||||
createSerializable({
|
||||
type: "MyType",
|
||||
value: value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Do-Notation Support
|
||||
|
||||
```typescript
|
||||
export const MyTypeCompanion = {
|
||||
Do: <T>(gen: () => Generator<MyType<any>, T, any>): MyType<T> => {
|
||||
// Implementation here
|
||||
// See Option or Either for reference
|
||||
},
|
||||
}
|
||||
|
||||
export const MyType = Object.assign(MyTypeConstructor, MyTypeCompanion)
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### TypeScript Errors
|
||||
|
||||
**"Type instantiation is excessively deep"**
|
||||
|
||||
- Check for circular type references
|
||||
- Simplify nested generic types
|
||||
- Use type aliases to break up complex types
|
||||
|
||||
**"Property 'X' does not exist on type 'Y'"**
|
||||
|
||||
- Ensure all interfaces are properly implemented
|
||||
- Check that types are correctly exported
|
||||
- Verify Base pattern includes all required methods
|
||||
|
||||
### Test Failures
|
||||
|
||||
```bash
|
||||
# Run specific test with verbose output
|
||||
pnpm vitest run test/mytype.spec.ts --reporter=verbose
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm test:watch
|
||||
|
||||
# Check test coverage
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
### Build Issues
|
||||
|
||||
```bash
|
||||
# Clean and rebuild
|
||||
pnpm clean && pnpm build
|
||||
|
||||
# Check TypeScript compilation only
|
||||
pnpm compile
|
||||
|
||||
# Analyze bundle size
|
||||
pnpm analyze:size
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
### scripts/
|
||||
|
||||
- `new-type-template.sh` - Generate boilerplate for new types
|
||||
- `validate.sh` - Run the full validation workflow
|
||||
|
||||
### references/
|
||||
|
||||
- `architecture.md` - Detailed architecture and patterns
|
||||
- `adding-types.md` - Complete guide for adding new data structures
|
||||
- `testing-patterns.md` - Testing strategies and examples
|
||||
|
||||
### External Links
|
||||
|
||||
- **GitHub**: https://github.com/jordanburke/functype
|
||||
- **Docs**: https://jordanburke.github.io/functype/
|
||||
- **Feature Matrix**: `docs/FUNCTYPE_FEATURE_MATRIX.md`
|
||||
376
skills/functype-developer/references/adding-types.md
Normal file
376
skills/functype-developer/references/adding-types.md
Normal file
@@ -0,0 +1,376 @@
|
||||
# Adding New Data Structures
|
||||
|
||||
Step-by-step guide for adding new types to functype.
|
||||
|
||||
## Complete Workflow
|
||||
|
||||
### 1. Planning
|
||||
|
||||
Before coding, answer these questions:
|
||||
|
||||
- **What problem does this type solve?**
|
||||
- **Which functional interfaces should it implement?** (Check Feature Matrix)
|
||||
- **What are the key operations?**
|
||||
- **Are there similar types to reference?** (Option, Either, List)
|
||||
|
||||
### 2. Create Module Structure
|
||||
|
||||
```bash
|
||||
# Create directories
|
||||
mkdir -p src/mynewtype
|
||||
mkdir -p test
|
||||
|
||||
# Create files
|
||||
touch src/mynewtype/index.ts
|
||||
touch test/mynewtype.spec.ts
|
||||
```
|
||||
|
||||
### 3. Define Type Interface
|
||||
|
||||
```typescript
|
||||
// src/mynewtype/index.ts
|
||||
import type { Functor } from "@/functor"
|
||||
import type { Foldable } from "@/foldable"
|
||||
|
||||
export type MyNewType<T> = Functor<T> &
|
||||
Foldable<T> & {
|
||||
// Core operations
|
||||
getValue(): T
|
||||
isEmpty(): boolean
|
||||
|
||||
// Additional methods
|
||||
filter(predicate: (value: T) => boolean): MyNewType<T>
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Implement Constructor
|
||||
|
||||
```typescript
|
||||
import { Base } from "@/core/base"
|
||||
|
||||
export function MyNew<T>(value: T): MyNewType<T> {
|
||||
return Base("MyNewType", {
|
||||
// Functor
|
||||
map: <B>(f: (val: T) => B): MyNewType<B> => {
|
||||
return MyNew(f(value))
|
||||
},
|
||||
|
||||
// Foldable
|
||||
fold: <B>(onEmpty: () => B, onValue: (val: T) => B): B => {
|
||||
return value == null ? onEmpty() : onValue(value)
|
||||
},
|
||||
|
||||
foldLeft:
|
||||
<B>(z: B) =>
|
||||
(op: (b: B, a: T) => B): B => {
|
||||
return value == null ? z : op(z, value)
|
||||
},
|
||||
|
||||
foldRight:
|
||||
<B>(z: B) =>
|
||||
(op: (a: T, b: B) => B): B => {
|
||||
return value == null ? z : op(value, z)
|
||||
},
|
||||
|
||||
// Custom methods
|
||||
getValue: () => value,
|
||||
isEmpty: () => value == null,
|
||||
filter: (predicate: (val: T) => boolean) => {
|
||||
return predicate(value) ? MyNew(value) : MyNew(null as any)
|
||||
},
|
||||
|
||||
// Pipe for composition
|
||||
pipe: () => ({
|
||||
map: (f: (val: T) => any) => MyNew(value).map(f),
|
||||
filter: (p: (val: T) => boolean) => MyNew(value).filter(p),
|
||||
}),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add Companion Methods
|
||||
|
||||
```typescript
|
||||
// Static factory methods
|
||||
MyNew.empty = <T>(): MyNewType<T> => MyNew<T>(null as any)
|
||||
|
||||
MyNew.from = <T>(value: T | null | undefined): MyNewType<T> => {
|
||||
return MyNew(value ?? (null as any))
|
||||
}
|
||||
|
||||
MyNew.of = <T>(value: T): MyNewType<T> => MyNew(value)
|
||||
```
|
||||
|
||||
### 6. Add Exports
|
||||
|
||||
**Update `src/index.ts`:**
|
||||
|
||||
```typescript
|
||||
export { MyNew } from "./mynewtype"
|
||||
export type { MyNewType } from "./mynewtype"
|
||||
```
|
||||
|
||||
**Update `package.json` exports:**
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
"./mynewtype": {
|
||||
"import": "./dist/esm/mynewtype/index.js",
|
||||
"require": "./dist/cjs/mynewtype/index.js",
|
||||
"types": "./dist/types/mynewtype/index.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Write Tests
|
||||
|
||||
```typescript
|
||||
// test/mynewtype.spec.ts
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { MyNew } from "@/mynewtype"
|
||||
|
||||
describe("MyNewType", () => {
|
||||
describe("Construction", () => {
|
||||
it("should create from value", () => {
|
||||
const value = MyNew(5)
|
||||
expect(value.getValue()).toBe(5)
|
||||
expect(value.isEmpty()).toBe(false)
|
||||
})
|
||||
|
||||
it("should create empty", () => {
|
||||
const empty = MyNew.empty<number>()
|
||||
expect(empty.isEmpty()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Functor", () => {
|
||||
it("should map over values", () => {
|
||||
const result = MyNew(5).map((x) => x * 2)
|
||||
expect(result.getValue()).toBe(10)
|
||||
})
|
||||
|
||||
it("should satisfy identity law", () => {
|
||||
const value = MyNew(5)
|
||||
expect(value.map((x) => x)).toEqual(value)
|
||||
})
|
||||
|
||||
it("should satisfy composition law", () => {
|
||||
const value = MyNew(5)
|
||||
const f = (x: number) => x * 2
|
||||
const g = (x: number) => x + 1
|
||||
|
||||
expect(value.map(f).map(g)).toEqual(value.map((x) => g(f(x))))
|
||||
})
|
||||
})
|
||||
|
||||
describe("Foldable", () => {
|
||||
it("should fold with value", () => {
|
||||
const result = MyNew(5).fold(
|
||||
() => "empty",
|
||||
(x) => `value: ${x}`,
|
||||
)
|
||||
expect(result).toBe("value: 5")
|
||||
})
|
||||
|
||||
it("should fold when empty", () => {
|
||||
const result = MyNew.empty<number>().fold(
|
||||
() => "empty",
|
||||
(x) => `value: ${x}`,
|
||||
)
|
||||
expect(result).toBe("empty")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Custom Methods", () => {
|
||||
it("should filter values", () => {
|
||||
const value = MyNew(5)
|
||||
expect(value.filter((x) => x > 3).getValue()).toBe(5)
|
||||
expect(value.filter((x) => x > 10).isEmpty()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle null", () => {
|
||||
const value = MyNew(null)
|
||||
expect(value.isEmpty()).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle undefined", () => {
|
||||
const value = MyNew(undefined)
|
||||
expect(value.isEmpty()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 8. Update Documentation
|
||||
|
||||
**Add to Feature Matrix (`docs/FUNCTYPE_FEATURE_MATRIX.md`):**
|
||||
|
||||
```markdown
|
||||
| **MyNewType<T>** | ✓ | ✓ | ✓ | ... |
|
||||
```
|
||||
|
||||
**Add help file (optional):**
|
||||
|
||||
```json
|
||||
// src/mynewtype/mynewtype.help.json
|
||||
{
|
||||
"name": "MyNewType",
|
||||
"description": "Brief description of the type",
|
||||
"examples": [
|
||||
{
|
||||
"title": "Basic Usage",
|
||||
"code": "const value = MyNew(5)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Validate
|
||||
|
||||
```bash
|
||||
pnpm validate
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
- Format check
|
||||
- Lint check
|
||||
- All tests
|
||||
- Production build
|
||||
|
||||
## Interface Implementation Guide
|
||||
|
||||
### Implementing Monad
|
||||
|
||||
If your type should support `flatMap`:
|
||||
|
||||
```typescript
|
||||
export type MyNewType<T> = Functor<T> &
|
||||
Monad<T> & {
|
||||
// ...
|
||||
}
|
||||
|
||||
export function MyNew<T>(value: T): MyNewType<T> {
|
||||
return Base("MyNewType", {
|
||||
// ... other methods
|
||||
|
||||
flatMap: <B>(f: (val: T) => MyNewType<B>): MyNewType<B> => {
|
||||
return value == null ? MyNew.empty<B>() : f(value)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Implementing Serializable
|
||||
|
||||
```typescript
|
||||
import { createSerializable } from "@/core/serializable"
|
||||
|
||||
export function MyNew<T>(value: T): MyNewType<T> {
|
||||
return Base("MyNewType", {
|
||||
// ... other methods
|
||||
|
||||
serialize: () =>
|
||||
createSerializable({
|
||||
type: "MyNewType",
|
||||
value: value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Implementing Traversable (for collections)
|
||||
|
||||
```typescript
|
||||
export function MyNewCollection<T>(items: T[]): MyNewCollectionType<T> {
|
||||
return Base("MyNewCollection", {
|
||||
// ... other methods
|
||||
|
||||
size: items.length,
|
||||
isEmpty: items.length === 0,
|
||||
contains: (value: T) => items.includes(value),
|
||||
reduce: <B>(f: (acc: B, val: T) => B, initial: B): B => {
|
||||
return items.reduce(f, initial)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Handling Multiple Cases
|
||||
|
||||
```typescript
|
||||
export function MyNew<T>(value: T): MyNewType<T> {
|
||||
// Empty case
|
||||
if (value == null) {
|
||||
return Base("Empty", {
|
||||
map: <B>(_f: (val: T) => B) => MyNew.empty<B>(),
|
||||
// ... empty implementations
|
||||
})
|
||||
}
|
||||
|
||||
// Value case
|
||||
return Base("Value", {
|
||||
map: <B>(f: (val: T) => B) => MyNew(f(value)),
|
||||
// ... value implementations
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Type Guards
|
||||
|
||||
```typescript
|
||||
export type MyNewType<T> = {
|
||||
// ... other methods
|
||||
isEmpty(): boolean
|
||||
hasValue(): boolean
|
||||
}
|
||||
|
||||
export function MyNew<T>(value: T): MyNewType<T> {
|
||||
return Base("MyNewType", {
|
||||
// ... other methods
|
||||
isEmpty: () => value == null,
|
||||
hasValue: () => value != null,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Companion Pattern with TypeScript
|
||||
|
||||
```typescript
|
||||
import { Companion } from "@/core/companion"
|
||||
|
||||
function MyNewConstructor<T>(value: T): MyNewType<T> {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
const MyNewCompanion = {
|
||||
empty: <T>() => MyNewConstructor<T>(null as any),
|
||||
of: <T>(value: T) => MyNewConstructor(value),
|
||||
from: <T>(source: any) => MyNewConstructor<T>(/* convert source */),
|
||||
}
|
||||
|
||||
export const MyNew = Companion(MyNewConstructor, MyNewCompanion)
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
Before submitting:
|
||||
|
||||
- [ ] Type interface defined with all required methods
|
||||
- [ ] Constructor implemented using Base pattern
|
||||
- [ ] All functional interfaces implemented correctly
|
||||
- [ ] Companion methods added (empty, from, of, etc.)
|
||||
- [ ] Exports added to src/index.ts
|
||||
- [ ] Package.json exports configured
|
||||
- [ ] Comprehensive tests written
|
||||
- [ ] Edge cases tested (null, undefined, empty)
|
||||
- [ ] Functor/Monad laws tested
|
||||
- [ ] Feature Matrix updated
|
||||
- [ ] `pnpm validate` passes
|
||||
- [ ] Documentation added (optional help file)
|
||||
555
skills/functype-developer/references/architecture.md
Normal file
555
skills/functype-developer/references/architecture.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# Functype Architecture
|
||||
|
||||
Complete guide to functype's architectural patterns and design principles.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Functype follows Scala's functional programming model adapted for TypeScript:
|
||||
|
||||
1. **Constructor functions** instead of classes
|
||||
2. **Immutable** data structures
|
||||
3. **Type-safe** operations with strict TypeScript
|
||||
4. **Composable** APIs via function chaining
|
||||
5. **Consistent** patterns across all types
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### Constructor Pattern
|
||||
|
||||
All types use constructor functions that return objects with methods:
|
||||
|
||||
```typescript
|
||||
// Pattern structure
|
||||
export function TypeName<T>(value: T): TypeNameType<T> {
|
||||
return Base("TypeName", {
|
||||
// Interface methods
|
||||
map: <B>(f: (val: T) => B) => TypeName(f(value)),
|
||||
flatMap: <B>(f: (val: T) => TypeNameType<B>) => f(value),
|
||||
|
||||
// Custom methods
|
||||
getValue: () => value,
|
||||
|
||||
// Pipe for composition
|
||||
pipe: () => ({
|
||||
map: (f: (val: T) => any) => TypeName(value).map(f),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// Companion methods
|
||||
TypeName.empty = <T>() => TypeName<T>(/* empty value */)
|
||||
TypeName.from = <T>(source: T[]) => TypeName<T>(/* construct */)
|
||||
```
|
||||
|
||||
**Why not classes?**
|
||||
|
||||
- Enables better tree-shaking
|
||||
- Simpler type inference
|
||||
- Aligns with functional programming principles
|
||||
- More flexible composition
|
||||
|
||||
### Base Pattern
|
||||
|
||||
The `Base` function from `core/base` adds common functionality:
|
||||
|
||||
```typescript
|
||||
import { Base } from "@/core/base"
|
||||
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
if (value == null) {
|
||||
return Base("None", {
|
||||
map: <B>(_f: (val: T) => B) => Option.none<B>(),
|
||||
// ... other methods
|
||||
})
|
||||
}
|
||||
|
||||
return Base("Some", {
|
||||
map: <B>(f: (val: T) => B) => Option(f(value)),
|
||||
// ... other methods
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Base provides:**
|
||||
|
||||
- `Typeable` interface with type metadata
|
||||
- Standard `toString()` method
|
||||
- Consistent object structure
|
||||
- Type-safe access to internal state
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
export function Base<T extends string, B extends Record<string, any>>(type: T, body: B): Typeable & B {
|
||||
return {
|
||||
...body,
|
||||
getType: () => type,
|
||||
toString: () => `${type}(${JSON.stringify(body)})`,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Companion Pattern
|
||||
|
||||
Use the `Companion` utility to create function-objects:
|
||||
|
||||
```typescript
|
||||
import { Companion } from "@/core/companion"
|
||||
|
||||
// Define constructor
|
||||
function OptionConstructor<T>(value: T | null | undefined): OptionType<T> {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
// Define companion methods
|
||||
const OptionCompanion = {
|
||||
none: <T>() => OptionConstructor<T>(null),
|
||||
some: <T>(value: T) => OptionConstructor(value),
|
||||
from: <T>(value: T | null | undefined) => OptionConstructor(value),
|
||||
}
|
||||
|
||||
// Combine constructor and companion
|
||||
export const Option = Companion(OptionConstructor, OptionCompanion)
|
||||
```
|
||||
|
||||
**Result:**
|
||||
|
||||
```typescript
|
||||
// Constructor usage
|
||||
const opt = Option(5)
|
||||
|
||||
// Companion method usage
|
||||
const none = Option.none()
|
||||
const some = Option.some(10)
|
||||
```
|
||||
|
||||
## Type System
|
||||
|
||||
### Base Type Constraint
|
||||
|
||||
```typescript
|
||||
import type { Type } from "@/functor"
|
||||
|
||||
// Use Type for generic constraints
|
||||
function process<T extends Type>(value: T): void {
|
||||
// T can be any type
|
||||
}
|
||||
```
|
||||
|
||||
**Never use `any`:**
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong
|
||||
function process(value: any): void
|
||||
|
||||
// ✅ Correct
|
||||
function process<T extends Type>(value: T): void
|
||||
|
||||
// ✅ Or for uncertain types
|
||||
function process(value: unknown): void
|
||||
```
|
||||
|
||||
### Higher-Kinded Types (HKT)
|
||||
|
||||
Functype implements HKT to enable generic programming:
|
||||
|
||||
```typescript
|
||||
// HKT allows abstracting over type constructors
|
||||
type Functor<F, A> = {
|
||||
map<B>(f: (a: A) => B): Functor<F, B>
|
||||
}
|
||||
|
||||
// This allows writing generic functions that work with any Functor
|
||||
function double<F, A extends number>(fa: Functor<F, A>): Functor<F, A> {
|
||||
return fa.map((a) => a * 2)
|
||||
}
|
||||
|
||||
// Works with Option<number>, Either<E, number>, List<number>, etc.
|
||||
```
|
||||
|
||||
### Branded Types
|
||||
|
||||
Use the `Brand` module for nominal typing:
|
||||
|
||||
```typescript
|
||||
import { Brand } from "@/brand"
|
||||
|
||||
// Create a branded type
|
||||
type UserId = Brand<string, "UserId">
|
||||
|
||||
// Constructor with validation
|
||||
const UserId = Brand<string, "UserId">(
|
||||
(value: string): Either<string, string> => (value.length > 0 ? Right(value) : Left("UserId cannot be empty")),
|
||||
)
|
||||
|
||||
// Usage
|
||||
const userId = UserId.from("user-123").fold(
|
||||
(error) => console.error(error),
|
||||
(id) => processUser(id), // id has type UserId, not string
|
||||
)
|
||||
```
|
||||
|
||||
## Functional Interfaces
|
||||
|
||||
### Functor
|
||||
|
||||
Maps over contained values while preserving structure:
|
||||
|
||||
```typescript
|
||||
export interface Functor<T> {
|
||||
map<B>(f: (value: T) => B): Functor<B>
|
||||
}
|
||||
|
||||
// Example implementation
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
if (value == null) {
|
||||
return Base("None", {
|
||||
map: <B>(_f: (val: T) => B) => Option.none<B>(),
|
||||
})
|
||||
}
|
||||
|
||||
return Base("Some", {
|
||||
map: <B>(f: (val: T) => B) => Option(f(value)),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Laws:**
|
||||
|
||||
1. Identity: `fa.map(x => x) === fa`
|
||||
2. Composition: `fa.map(f).map(g) === fa.map(x => g(f(x)))`
|
||||
|
||||
### Applicative
|
||||
|
||||
Applies wrapped functions to wrapped values:
|
||||
|
||||
```typescript
|
||||
export interface Applicative<T> extends Functor<T> {
|
||||
ap<B>(ff: Applicative<(value: T) => B>): Applicative<B>
|
||||
}
|
||||
|
||||
// Example implementation
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
return Base("Option", {
|
||||
map: /* ... */,
|
||||
ap: <B>(ff: OptionType<(value: T) => B>): OptionType<B> => {
|
||||
return ff.flatMap(f => Option(value).map(f))
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Laws:**
|
||||
|
||||
1. Identity: `v.ap(pure(x => x)) === v`
|
||||
2. Homomorphism: `pure(x).ap(pure(f)) === pure(f(x))`
|
||||
3. Interchange: `u.ap(pure(y)) === pure(f => f(y)).ap(u)`
|
||||
|
||||
### Monad
|
||||
|
||||
Sequences operations that return wrapped values:
|
||||
|
||||
```typescript
|
||||
export interface Monad<T> extends Applicative<T> {
|
||||
flatMap<B>(f: (value: T) => Monad<B>): Monad<B>
|
||||
}
|
||||
|
||||
// Example implementation
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
return Base("Option", {
|
||||
map: /* ... */,
|
||||
ap: /* ... */,
|
||||
flatMap: <B>(f: (val: T) => OptionType<B>): OptionType<B> => {
|
||||
return value == null ? Option.none<B>() : f(value)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Laws:**
|
||||
|
||||
1. Left identity: `pure(a).flatMap(f) === f(a)`
|
||||
2. Right identity: `m.flatMap(pure) === m`
|
||||
3. Associativity: `m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))`
|
||||
|
||||
### Foldable
|
||||
|
||||
Extracts values via pattern matching:
|
||||
|
||||
```typescript
|
||||
export interface Foldable<T> {
|
||||
fold<B>(onEmpty: () => B, onValue: (value: T) => B): B
|
||||
foldLeft<B>(z: B): (op: (b: B, a: T) => B) => B
|
||||
foldRight<B>(z: B): (op: (a: T, b: B) => B) => B
|
||||
}
|
||||
|
||||
// Example implementation
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
return Base("Option", {
|
||||
fold: <B>(onEmpty: () => B, onValue: (val: T) => B): B => {
|
||||
return value == null ? onEmpty() : onValue(value)
|
||||
},
|
||||
foldLeft:
|
||||
<B>(z: B) =>
|
||||
(op: (b: B, a: T) => B): B => {
|
||||
return value == null ? z : op(z, value)
|
||||
},
|
||||
foldRight:
|
||||
<B>(z: B) =>
|
||||
(op: (a: T, b: B) => B): B => {
|
||||
return value == null ? z : op(value, z)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Traversable
|
||||
|
||||
Collections with size, iteration, and reduction:
|
||||
|
||||
```typescript
|
||||
export interface Traversable<T> {
|
||||
size: number
|
||||
isEmpty: boolean
|
||||
contains(value: T): boolean
|
||||
reduce<B>(f: (acc: B, value: T) => B, initial: B): B
|
||||
reduceRight<B>(f: (value: T, acc: B) => B, initial: B): B
|
||||
}
|
||||
|
||||
// Example implementation
|
||||
export function List<T>(items: T[]): ListType<T> {
|
||||
return Base("List", {
|
||||
size: items.length,
|
||||
isEmpty: items.length === 0,
|
||||
contains: (value: T) => items.includes(value),
|
||||
reduce: <B>(f: (acc: B, val: T) => B, initial: B): B => {
|
||||
return items.reduce(f, initial)
|
||||
},
|
||||
reduceRight: <B>(f: (val: T, acc: B) => B, initial: B): B => {
|
||||
return items.reduceRight((acc, val) => f(val, acc), initial)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Serializable
|
||||
|
||||
JSON/YAML/binary serialization:
|
||||
|
||||
```typescript
|
||||
export interface Serializable {
|
||||
serialize(): SerializationMethods<T>
|
||||
}
|
||||
|
||||
export interface SerializationMethods<T> {
|
||||
toJSON(): string
|
||||
toYAML(): string
|
||||
toBinary(): Uint8Array
|
||||
}
|
||||
|
||||
// Example implementation
|
||||
import { createSerializable } from "@/core/serializable"
|
||||
|
||||
export function Option<T>(value: T | null | undefined): OptionType<T> {
|
||||
return Base("Option", {
|
||||
serialize: () =>
|
||||
createSerializable({
|
||||
type: "Option",
|
||||
value: value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Module Organization
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── core/ # Base patterns and utilities
|
||||
│ ├── base.ts
|
||||
│ ├── companion.ts
|
||||
│ └── serializable.ts
|
||||
├── functor/ # Functor type class
|
||||
├── monad/ # Monad type class
|
||||
├── option/ # Option type
|
||||
│ └── index.ts
|
||||
├── either/ # Either type
|
||||
│ └── index.ts
|
||||
└── index.ts # Main exports
|
||||
|
||||
test/
|
||||
├── option.spec.ts
|
||||
├── either.spec.ts
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Index Exports
|
||||
|
||||
Each module has an `index.ts` that re-exports its main type:
|
||||
|
||||
```typescript
|
||||
// src/option/index.ts
|
||||
export { Option } from "./option"
|
||||
export type { OptionType } from "./option"
|
||||
```
|
||||
|
||||
Main index re-exports everything:
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
export { Option } from "./option"
|
||||
export type { OptionType } from "./option"
|
||||
export { Either, Left, Right } from "./either"
|
||||
export type { EitherType } from "./either"
|
||||
// ...
|
||||
```
|
||||
|
||||
### Package Exports
|
||||
|
||||
`package.json` supports selective imports:
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/esm/index.js",
|
||||
"require": "./dist/cjs/index.js",
|
||||
"types": "./dist/types/index.d.ts"
|
||||
},
|
||||
"./option": {
|
||||
"import": "./dist/esm/option/index.js",
|
||||
"require": "./dist/cjs/option/index.js",
|
||||
"types": "./dist/types/option/index.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// Import from main bundle
|
||||
import { Option, Either } from "functype"
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Tree-Shaking
|
||||
|
||||
Functype is optimized for tree-shaking:
|
||||
|
||||
```json
|
||||
{
|
||||
"sideEffects": false
|
||||
}
|
||||
```
|
||||
|
||||
This means unused exports are eliminated during bundling.
|
||||
|
||||
### Lazy Evaluation
|
||||
|
||||
Use `Lazy` and `LazyList` for deferred computation:
|
||||
|
||||
```typescript
|
||||
import { Lazy } from "functype"
|
||||
|
||||
const expensive = Lazy(() => heavyComputation())
|
||||
// Not computed yet
|
||||
|
||||
const value = expensive.value() // Computed once
|
||||
const value2 = expensive.value() // Cached
|
||||
```
|
||||
|
||||
### Immutability Cost
|
||||
|
||||
Immutable operations have overhead. For performance-critical code:
|
||||
|
||||
```typescript
|
||||
// ❌ Slow for large lists
|
||||
let result = List([])
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
result = result.append(i) // Creates new list each time
|
||||
}
|
||||
|
||||
// ✅ Fast with native array, then convert
|
||||
const items = []
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
items.push(i)
|
||||
}
|
||||
const result = List(items) // Single conversion
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ Use Option for nullable values
|
||||
Option(value).orElse(default)
|
||||
|
||||
// ✅ Use Either for errors with context
|
||||
validateEmail(email)
|
||||
.fold(
|
||||
error => console.error(error),
|
||||
validEmail => send(validEmail)
|
||||
)
|
||||
|
||||
// ✅ Use Try for exceptions
|
||||
Try(() => JSON.parse(str))
|
||||
.recover(error => defaultValue)
|
||||
|
||||
// ✅ Use Throwable for unexpected errors
|
||||
throw new Throwable("Unexpected error", { context })
|
||||
```
|
||||
|
||||
### ErrorFormatter
|
||||
|
||||
Use for structured error output:
|
||||
|
||||
```typescript
|
||||
import { ErrorFormatter } from "@/core/error"
|
||||
|
||||
const error = new Error("Something went wrong")
|
||||
const formatted = ErrorFormatter.format(error)
|
||||
|
||||
console.error(formatted.toString())
|
||||
// Error: Something went wrong
|
||||
// at myFunction (file.ts:10:5)
|
||||
// ...
|
||||
```
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
### Test Organization
|
||||
|
||||
```
|
||||
test/
|
||||
├── option.spec.ts # Option tests
|
||||
├── either.spec.ts # Either tests
|
||||
├── integration/ # Integration tests
|
||||
│ └── composition.spec.ts
|
||||
└── property/ # Property-based tests
|
||||
└── functor.spec.ts
|
||||
```
|
||||
|
||||
### Test Patterns
|
||||
|
||||
```typescript
|
||||
describe("Option", () => {
|
||||
describe("Construction", () => {
|
||||
it("should create Some from non-null value", () => {
|
||||
const opt = Option(5)
|
||||
expect(opt.isSome()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Functor Laws", () => {
|
||||
it("should satisfy identity", () => {
|
||||
const opt = Option(5)
|
||||
expect(opt.map((x) => x)).toEqual(opt)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
394
skills/functype-developer/references/testing-patterns.md
Normal file
394
skills/functype-developer/references/testing-patterns.md
Normal file
@@ -0,0 +1,394 @@
|
||||
# Testing Patterns
|
||||
|
||||
Comprehensive testing strategies for functype development.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Standard Pattern
|
||||
|
||||
```typescript
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { MyType } from "@/mytype"
|
||||
|
||||
describe("MyType", () => {
|
||||
describe("Construction", () => {
|
||||
it("should create from value", () => {
|
||||
// Arrange
|
||||
const value = 5
|
||||
|
||||
// Act
|
||||
const result = MyType(value)
|
||||
|
||||
// Assert
|
||||
expect(result.getValue()).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Interface Implementation", () => {
|
||||
// Group tests by interface
|
||||
})
|
||||
|
||||
describe("Custom Methods", () => {
|
||||
// Test type-specific methods
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
// Test null, undefined, empty
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Testing Functional Interfaces
|
||||
|
||||
### Functor Laws
|
||||
|
||||
```typescript
|
||||
describe("Functor Laws", () => {
|
||||
it("should satisfy identity law", () => {
|
||||
const fa = MyType(5)
|
||||
const identity = (x: any) => x
|
||||
|
||||
expect(fa.map(identity)).toEqual(fa)
|
||||
})
|
||||
|
||||
it("should satisfy composition law", () => {
|
||||
const fa = MyType(5)
|
||||
const f = (x: number) => x * 2
|
||||
const g = (x: number) => x + 1
|
||||
|
||||
const left = fa.map(f).map(g)
|
||||
const right = fa.map((x) => g(f(x)))
|
||||
|
||||
expect(left).toEqual(right)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Monad Laws
|
||||
|
||||
```typescript
|
||||
describe("Monad Laws", () => {
|
||||
it("should satisfy left identity", () => {
|
||||
const a = 5
|
||||
const f = (x: number) => MyType(x * 2)
|
||||
|
||||
expect(MyType(a).flatMap(f)).toEqual(f(a))
|
||||
})
|
||||
|
||||
it("should satisfy right identity", () => {
|
||||
const m = MyType(5)
|
||||
|
||||
expect(m.flatMap((x) => MyType(x))).toEqual(m)
|
||||
})
|
||||
|
||||
it("should satisfy associativity", () => {
|
||||
const m = MyType(5)
|
||||
const f = (x: number) => MyType(x * 2)
|
||||
const g = (x: number) => MyType(x + 1)
|
||||
|
||||
const left = m.flatMap(f).flatMap(g)
|
||||
const right = m.flatMap((x) => f(x).flatMap(g))
|
||||
|
||||
expect(left).toEqual(right)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Applicative Laws
|
||||
|
||||
```typescript
|
||||
describe("Applicative Laws", () => {
|
||||
it("should satisfy identity", () => {
|
||||
const v = MyType(5)
|
||||
const pure = MyType((x: number) => x)
|
||||
|
||||
expect(v.ap(pure)).toEqual(v)
|
||||
})
|
||||
|
||||
it("should satisfy homomorphism", () => {
|
||||
const f = (x: number) => x * 2
|
||||
const x = 5
|
||||
|
||||
const left = MyType(x).ap(MyType(f))
|
||||
const right = MyType(f(x))
|
||||
|
||||
expect(left).toEqual(right)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Property-Based Testing
|
||||
|
||||
### Using fast-check
|
||||
|
||||
```typescript
|
||||
import { fc, test } from "@fast-check/vitest"
|
||||
|
||||
test.prop([fc.integer()])("map preserves structure", (n) => {
|
||||
const opt = Option(n)
|
||||
const result = opt.map((x) => x * 2)
|
||||
|
||||
if (opt.isSome()) {
|
||||
expect(result.isSome()).toBe(true)
|
||||
} else {
|
||||
expect(result.isNone()).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test.prop([fc.integer(), fc.integer()])("flatMap is associative", (a, b) => {
|
||||
const m = List([a, b])
|
||||
const f = (x: number) => List([x, x * 2])
|
||||
const g = (x: number) => List([x + 1])
|
||||
|
||||
const left = m.flatMap(f).flatMap(g)
|
||||
const right = m.flatMap((x) => f(x).flatMap(g))
|
||||
|
||||
expect(left.toArray()).toEqual(right.toArray())
|
||||
})
|
||||
```
|
||||
|
||||
### Custom Arbitraries
|
||||
|
||||
```typescript
|
||||
import { fc } from "@fast-check/vitest"
|
||||
|
||||
// Generate Options
|
||||
const optionArb = <T>(valueArb: fc.Arbitrary<T>): fc.Arbitrary<Option<T>> =>
|
||||
fc.oneof(
|
||||
fc.constant(Option.none()),
|
||||
valueArb.map((v) => Option(v)),
|
||||
)
|
||||
|
||||
// Generate Lists
|
||||
const listArb = <T>(valueArb: fc.Arbitrary<T>): fc.Arbitrary<List<T>> => fc.array(valueArb).map((arr) => List(arr))
|
||||
|
||||
// Usage
|
||||
test.prop([optionArb(fc.integer())])("option property", (opt) => {
|
||||
// Test with generated Options
|
||||
})
|
||||
```
|
||||
|
||||
## Edge Case Testing
|
||||
|
||||
### Null/Undefined
|
||||
|
||||
```typescript
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle null", () => {
|
||||
const value = Option(null)
|
||||
expect(value.isNone()).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle undefined", () => {
|
||||
const value = Option(undefined)
|
||||
expect(value.isNone()).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle zero", () => {
|
||||
const value = Option(0)
|
||||
expect(value.isSome()).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle empty string", () => {
|
||||
const value = Option("")
|
||||
expect(value.isSome()).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Type Inference
|
||||
|
||||
```typescript
|
||||
describe("Type Inference", () => {
|
||||
it("should infer correct types", () => {
|
||||
const num: Option<number> = Option(5)
|
||||
const str: Option<string> = num.map((x) => x.toString())
|
||||
|
||||
// TypeScript should not error
|
||||
const _check: string = str.orElse("")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Immutability
|
||||
|
||||
```typescript
|
||||
describe("Immutability", () => {
|
||||
it("should not mutate original", () => {
|
||||
const original = List([1, 2, 3])
|
||||
const modified = original.append(4)
|
||||
|
||||
expect(original.toArray()).toEqual([1, 2, 3])
|
||||
expect(modified.toArray()).toEqual([1, 2, 3, 4])
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
### Type Composition
|
||||
|
||||
```typescript
|
||||
describe("Type Composition", () => {
|
||||
it("should compose Option and Either", () => {
|
||||
const validate = (x: number): Either<string, number> => (x > 0 ? Right(x) : Left("Must be positive"))
|
||||
|
||||
const result = Option(5)
|
||||
.map(validate)
|
||||
.fold(
|
||||
() => Left("No value"),
|
||||
(either) => either,
|
||||
)
|
||||
|
||||
expect(result.isRight()).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Pipeline Testing
|
||||
|
||||
```typescript
|
||||
describe("Pipelines", () => {
|
||||
it("should compose operations", () => {
|
||||
const result = List([1, 2, 3, 4, 5])
|
||||
.filter((x) => x > 2)
|
||||
.map((x) => x * 2)
|
||||
.foldLeft(0)((sum, x) => sum + x)
|
||||
|
||||
expect(result).toBe(24) // (3 + 4 + 5) * 2 = 24
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Benchmarks
|
||||
|
||||
```typescript
|
||||
import { bench, describe } from "vitest"
|
||||
|
||||
describe("Performance", () => {
|
||||
bench("Option creation", () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
Option(i)
|
||||
}
|
||||
})
|
||||
|
||||
bench("List operations", () => {
|
||||
const list = List(Array.from({ length: 1000 }, (_, i) => i))
|
||||
list.filter((x) => x % 2 === 0).map((x) => x * 2)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
test/
|
||||
├── option.spec.ts # Option tests
|
||||
├── either.spec.ts # Either tests
|
||||
├── list.spec.ts # List tests
|
||||
├── integration/ # Integration tests
|
||||
│ ├── composition.spec.ts
|
||||
│ └── pipelines.spec.ts
|
||||
└── property/ # Property-based tests
|
||||
├── functor-laws.spec.ts
|
||||
└── monad-laws.spec.ts
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
```typescript
|
||||
// ✅ Good test names
|
||||
it("should create Some from non-null value")
|
||||
it("should return None when mapping over None")
|
||||
it("should satisfy functor identity law")
|
||||
|
||||
// ❌ Poor test names
|
||||
it("works")
|
||||
it("test1")
|
||||
it("should work correctly")
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
### Running Coverage
|
||||
|
||||
```bash
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
### Coverage Goals
|
||||
|
||||
- **Line coverage**: >90%
|
||||
- **Branch coverage**: >85%
|
||||
- **Function coverage**: >90%
|
||||
|
||||
### Uncovered Code
|
||||
|
||||
Some code is intentionally not covered:
|
||||
|
||||
- Error handling for impossible states
|
||||
- Debug utilities
|
||||
- Deprecated code
|
||||
|
||||
Mark with comments:
|
||||
|
||||
```typescript
|
||||
/* istanbul ignore next */
|
||||
if (impossibleCondition) {
|
||||
throw new Error("This should never happen")
|
||||
}
|
||||
```
|
||||
|
||||
## Continuous Testing
|
||||
|
||||
### Watch Mode
|
||||
|
||||
```bash
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
### Pre-commit Hook
|
||||
|
||||
```bash
|
||||
# .git/hooks/pre-commit
|
||||
#!/bin/sh
|
||||
pnpm test || exit 1
|
||||
```
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### Verbose Output
|
||||
|
||||
```bash
|
||||
pnpm vitest run test/mytype.spec.ts --reporter=verbose
|
||||
```
|
||||
|
||||
### Focused Tests
|
||||
|
||||
```typescript
|
||||
// Run only this test
|
||||
it.only("should do something", () => {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Skip this test
|
||||
it.skip("should do something else", () => {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### Test Timeout
|
||||
|
||||
```typescript
|
||||
it(
|
||||
"should handle slow operation",
|
||||
async () => {
|
||||
// ...
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
) // 10 second timeout
|
||||
```
|
||||
153
skills/functype-developer/scripts/new-type-template.sh
Executable file
153
skills/functype-developer/scripts/new-type-template.sh
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to generate boilerplate for a new functype data structure
|
||||
# Usage: ./new-type-template.sh TypeName
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 TypeName"
|
||||
echo "Example: $0 Result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TYPE_NAME=$1
|
||||
TYPE_NAME_LOWER=$(echo "$TYPE_NAME" | tr '[:upper:]' '[:lower:]')
|
||||
PROJECT_ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
|
||||
|
||||
echo "Creating new type: $TYPE_NAME"
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
# Create source directory
|
||||
SRC_DIR="$PROJECT_ROOT/src/$TYPE_NAME_LOWER"
|
||||
mkdir -p "$SRC_DIR"
|
||||
|
||||
# Create type implementation
|
||||
cat > "$SRC_DIR/index.ts" << EOF
|
||||
import { Base } from "@/core/base"
|
||||
import type { Functor } from "@/functor"
|
||||
import type { Foldable } from "@/foldable"
|
||||
|
||||
/**
|
||||
* $TYPE_NAME type - [TODO: Add description]
|
||||
*/
|
||||
export type ${TYPE_NAME}Type<T> = Functor<T> & Foldable<T> & {
|
||||
// Core methods
|
||||
getValue(): T
|
||||
isEmpty(): boolean
|
||||
|
||||
// TODO: Add additional methods
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a $TYPE_NAME from a value
|
||||
*/
|
||||
export function $TYPE_NAME<T>(value: T): ${TYPE_NAME}Type<T> {
|
||||
return Base("$TYPE_NAME", {
|
||||
// Functor
|
||||
map: <B>(f: (val: T) => B): ${TYPE_NAME}Type<B> => {
|
||||
return $TYPE_NAME(f(value))
|
||||
},
|
||||
|
||||
// Foldable
|
||||
fold: <B>(onEmpty: () => B, onValue: (val: T) => B): B => {
|
||||
return value == null ? onEmpty() : onValue(value)
|
||||
},
|
||||
|
||||
foldLeft: <B>(z: B) => (op: (b: B, a: T) => B): B => {
|
||||
return value == null ? z : op(z, value)
|
||||
},
|
||||
|
||||
foldRight: <B>(z: B) => (op: (a: T, b: B) => B): B => {
|
||||
return value == null ? z : op(value, z)
|
||||
},
|
||||
|
||||
// Core methods
|
||||
getValue: () => value,
|
||||
isEmpty: () => value == null,
|
||||
|
||||
// Pipe
|
||||
pipe: () => ({
|
||||
map: (f: (val: T) => any) => $TYPE_NAME(value).map(f),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// Companion methods
|
||||
$TYPE_NAME.empty = <T>(): ${TYPE_NAME}Type<T> => $TYPE_NAME<T>(null as any)
|
||||
|
||||
$TYPE_NAME.of = <T>(value: T): ${TYPE_NAME}Type<T> => $TYPE_NAME(value)
|
||||
EOF
|
||||
|
||||
# Create test file
|
||||
TEST_FILE="$PROJECT_ROOT/test/$TYPE_NAME_LOWER.spec.ts"
|
||||
cat > "$TEST_FILE" << EOF
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { $TYPE_NAME } from "@/$TYPE_NAME_LOWER"
|
||||
|
||||
describe("$TYPE_NAME", () => {
|
||||
describe("Construction", () => {
|
||||
it("should create from value", () => {
|
||||
const value = $TYPE_NAME(5)
|
||||
expect(value.getValue()).toBe(5)
|
||||
expect(value.isEmpty()).toBe(false)
|
||||
})
|
||||
|
||||
it("should create empty", () => {
|
||||
const empty = $TYPE_NAME.empty<number>()
|
||||
expect(empty.isEmpty()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Functor", () => {
|
||||
it("should map over values", () => {
|
||||
const result = $TYPE_NAME(5).map(x => x * 2)
|
||||
expect(result.getValue()).toBe(10)
|
||||
})
|
||||
|
||||
it("should satisfy identity law", () => {
|
||||
const value = $TYPE_NAME(5)
|
||||
expect(value.map(x => x)).toEqual(value)
|
||||
})
|
||||
|
||||
it("should satisfy composition law", () => {
|
||||
const value = $TYPE_NAME(5)
|
||||
const f = (x: number) => x * 2
|
||||
const g = (x: number) => x + 1
|
||||
|
||||
expect(value.map(f).map(g)).toEqual(value.map(x => g(f(x))))
|
||||
})
|
||||
})
|
||||
|
||||
describe("Foldable", () => {
|
||||
it("should fold with value", () => {
|
||||
const result = $TYPE_NAME(5).fold(
|
||||
() => "empty",
|
||||
x => \`value: \${x}\`
|
||||
)
|
||||
expect(result).toBe("value: 5")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle null", () => {
|
||||
const value = $TYPE_NAME(null)
|
||||
expect(value.isEmpty()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "✅ Created files:"
|
||||
echo " - $SRC_DIR/index.ts"
|
||||
echo " - $TEST_FILE"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Update src/index.ts to export $TYPE_NAME"
|
||||
echo "2. Update package.json exports to include ./$TYPE_NAME_LOWER"
|
||||
echo "3. Implement additional methods in $SRC_DIR/index.ts"
|
||||
echo "4. Add more tests in $TEST_FILE"
|
||||
echo "5. Update docs/FUNCTYPE_FEATURE_MATRIX.md"
|
||||
echo "6. Run: pnpm validate"
|
||||
EOF
|
||||
61
skills/functype-developer/scripts/validate.sh
Executable file
61
skills/functype-developer/scripts/validate.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the full functype validation workflow
|
||||
# This script should be run before committing any changes
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "======================================"
|
||||
echo " Functype Validation Workflow"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Step 1: Format
|
||||
echo "📝 Step 1/4: Formatting code with Prettier..."
|
||||
pnpm format || {
|
||||
echo "❌ Formatting failed!"
|
||||
echo "Run 'pnpm format' to fix formatting issues"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Formatting complete"
|
||||
echo ""
|
||||
|
||||
# Step 2: Lint
|
||||
echo "🔍 Step 2/4: Linting code with ESLint..."
|
||||
pnpm lint || {
|
||||
echo "❌ Linting failed!"
|
||||
echo "Run 'pnpm lint' to fix linting issues"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Linting complete"
|
||||
echo ""
|
||||
|
||||
# Step 3: Test
|
||||
echo "🧪 Step 3/4: Running tests with Vitest..."
|
||||
pnpm test || {
|
||||
echo "❌ Tests failed!"
|
||||
echo "Fix failing tests and try again"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Tests complete"
|
||||
echo ""
|
||||
|
||||
# Step 4: Build
|
||||
echo "🔨 Step 4/4: Building project..."
|
||||
pnpm build || {
|
||||
echo "❌ Build failed!"
|
||||
echo "Fix build errors and try again"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Build complete"
|
||||
echo ""
|
||||
|
||||
echo "======================================"
|
||||
echo " ✅ All validation checks passed!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "Your code is ready to commit."
|
||||
EOF
|
||||
Reference in New Issue
Block a user