Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
37a660e
fix(publish): detect version bump in merge commit instead of changesets
github-actions[bot] Aug 11, 2026
fddc4b0
chore: version packages
github-actions[bot] Aug 10, 2026
59b9d45
chore: version packages
github-actions[bot] Aug 10, 2026
3241892
chore: bump version to 1.2.2 (test publish trigger)
github-actions[bot] Aug 11, 2026
9f8c553
chore: bump version to 1.2.3 (final publish trigger test)
github-actions[bot] Aug 11, 2026
6ee0c59
chore: bump version to 1.2.4 (final publish test)
github-actions[bot] Aug 11, 2026
12ed6ba
chore: bump version to 1.2.5 (validate force-tag fix from #418)
github-actions[bot] Aug 12, 2026
3c48405
chore: bump version to 1.2.6 (validate tag_name fix from PR #421)
github-actions[bot] Aug 12, 2026
24374fe
chore: bump version to 1.2.7 (validate quoted resolve-version fix)
github-actions[bot] Aug 12, 2026
7f41924
chore: bump version to 1.2.8 (final e2e test of resolve-version fix)
github-actions[bot] Aug 12, 2026
c9d9850
chore: bump version to 1.2.9 (final e2e test of v-prefix fix)
github-actions[bot] Aug 13, 2026
36fe052
docs(architecture): mirror @deessejs/errors rules and decisions folder
martyy-code Aug 13, 2026
6e1501d
fix(fp): honour architecture rules — typed factories, Ok.filter contr…
martyy-code Aug 13, 2026
7db0e6f
ci(workflows): split test and coverage into separate jobs
martyy-code Aug 14, 2026
961899d
chore(lockfile): regenerate pnpm-lock.yaml after @deessejs/errors drop
martyy-code Aug 14, 2026
956a602
chore(turbo): add test:coverage task so CI can resolve it
martyy-code Aug 14, 2026
ee1ae01
fix(ci): add test:coverage script + filter turbo to @deessejs/fp
martyy-code Aug 14, 2026
4757e94
chore(deps): add @vitest/coverage-v8 + regen lockfile
martyy-code Aug 14, 2026
be79ef2
fix(ci): wire coverage reporter + working render-coverage.mjs
martyy-code Aug 14, 2026
16405ce
fix(ci): post coverage comment via file path, not stdout
martyy-code Aug 14, 2026
4ad12c1
chore(changeset): add ci-coverage-comment changeset
martyy-code Aug 14, 2026
36f35a0
fix(workflows): restore publish.yml overwritten by rebase error
martyy-code Aug 14, 2026
a051126
test(fp): move index.test.ts from src/ to tests/
martyy-code Aug 14, 2026
269a2d3
test(fp): relocate index.test.ts and resolve via package alias
martyy-code Aug 14, 2026
d60f1c1
Merge pull request #429 from deessejs/refactor/classes
codewizdave Aug 14, 2026
7e9b7fd
refactor(fp): internal classes for Result and Maybe, deliver pipeables
martyy-code Aug 17, 2026
dacd05e
test(fp): 100% coverage for Result and Maybe, frozen at 100% threshold
martyy-code Aug 17, 2026
5f18d8a
Merge pull request #431 from deessejs/architecture/classes
codewizdave Aug 17, 2026
2a05140
feat(fp): add function utilities (pipe, flow, identity, constant, fli…
martyy-code Aug 17, 2026
3bedc89
feat(fp): expand function utilities with compose, predicate, tuple, t…
martyy-code Aug 19, 2026
80847ce
feat(fp): add and / or combinators for Predicate
martyy-code Aug 19, 2026
a937c26
Merge pull request #433 from deessejs/function/pipe-and-friends
codewizdave Aug 19, 2026
4f631ac
merge: sync main into staging
martyy-code Aug 19, 2026
726fb94
chore(changeset): add changeset for main → staging sync
martyy-code Aug 19, 2026
37f2245
Merge pull request #435 from deessejs/t3code/sync-main-into-staging
codewizdave Aug 19, 2026
a1a564e
feat(fp): add Try module (try_, tryPromise, attempt, withReporting, c…
martyy-code Aug 20, 2026
b505e2e
refactor(fp): move attempt implementation into internal class
martyy-code Aug 20, 2026
aae1039
refactor(fp): unify error handling on Result, retire the Try type
martyy-code Aug 20, 2026
0e96614
refactor(fp): delete the Try facade, keep only Result
martyy-code Aug 20, 2026
d2a1bc8
Merge pull request #439 from deessejs/feat/try-module
codewizdave Aug 20, 2026
ec0cd92
chore: version packages
github-actions[bot] Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/scripts/render-coverage.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* Render coverage-summary.json into a markdown table for the PR
* comment. Reads packages/fp/coverage/coverage-summary.json (produced
* by the json-summary reporter) and emits a markdown document to
* stdout. The CI workflow captures that output and posts it as a
* sticky PR comment.
*
* Total row plus one row per file, sorted by file path. Per-file
* thresholds are 100% on statements / branches / functions / lines
* (rule 0001 / ADR 0002).
*/

import { readFileSync } from 'node:fs';
import { resolve, relative, sep } from 'node:path';

const summaryPath = resolve('packages/fp/coverage/coverage-summary.json');
const summary = JSON.parse(readFileSync(summaryPath, "utf8"));
const repoRoot = resolve(".");

const fmt = (entry) => (entry && typeof entry.pct === "number" ? `${entry.pct.toFixed(2)}%` : "—");
const branchCell = (entry) => {
if (!entry || typeof entry.total !== "number") return "—";
if (entry.total === 0) return "n/a";
return fmt(entry);
};

const lines = [];
lines.push("## Coverage report");
lines.push("");
lines.push("| File | % Stmts | % Branch | % Funcs | % Lines |");
lines.push("| --- | ---: | ---: | ---: | ---: |");

const total = summary.total ?? {};
lines.push(`| **Total** | **${fmt(total.statements)}** | **${fmt(total.branches)}** | **${fmt(total.functions)}** | **${fmt(total.lines)}** |`);

const fileKeys = Object.keys(summary).filter((k) => k !== 'total').sort();
for (const key of fileKeys) {
const file = summary[key];
const rel = relative(repoRoot, key).split(sep).join("/");
lines.push(`| ${rel} | ${fmt(file.statements)} | ${branchCell(file.branches)} | ${fmt(file.functions)} | ${fmt(file.lines)} |`);
}

lines.push("");
lines.push("_Per-file thresholds: 100% on statements / branches / functions / lines (ADR 0002). Files with no branches render `n/a` in the Branch column. The threshold gate is disabled in this PR and lands with the full method × variant test matrix in a follow-up._");
lines.push("");

import { writeFileSync } from 'node:fs';
// Write the comment to a known path so the workflow can pass it
// via `path:` to the sticky-pull-request-comment action.
writeFileSync('coverage-comment.md', lines.join('\n'));
49 changes: 47 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,53 @@ jobs:

- run: pnpm install --frozen-lockfile
- run: pnpm turbo build
- run: pnpm turbo test
- run: pnpm turbo test --filter=@deessejs/fp

coverage:
name: Coverage
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: pnpm turbo build
# --filter=@deessejs/fp limits the run to this package. The other
# workspace packages (apps/web) don't have a test:coverage script,
# so an unfiltered turbo run would refuse to execute any task.
- run: pnpm turbo test:coverage --filter=@deessejs/fp

- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-${{ github.sha }}
path: packages/fp/coverage
retention-days: 14

- name: Render coverage table
if: success()
id: render
run: node .github/scripts/render-coverage.mjs

- name: Post coverage comment
if: success()
uses: marocchino/sticky-pull-request-comment@v2
with:
header: coverage
path: coverage-comment.md

changeset-check:
name: Changeset check
Expand Down Expand Up @@ -113,7 +159,6 @@ jobs:
run: |
BASE=${{ github.event.pull_request.base.ref }}
git fetch origin "$BASE"
# Sanity: the PR head must be downstream of the base ref.
if ! git merge-base --is-ancestor "origin/$BASE" HEAD; then
echo "::error::PR head is not ahead of origin/$BASE. Refusing to diff."
exit 1
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ temp/

# Changesets (keep for CI)
# .changeset/ is NOT ignored - needed for release workflow

# Build artifacts
coverage-comment.md
61 changes: 61 additions & 0 deletions docs/engineering/plans/architecture-classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Architecture classes

**Status**: Implemented (PR #TBD).
**Date**: 2026-08-17.
**Branch**: `architecture/classes`.

## Goal

Replace the plain-object + closure factories currently used by `Result` and `Maybe` with internal classes (`OkImpl`, `ErrImpl`, `SomeImpl`, `NoneImpl`) hidden behind the public factory functions. The public API surface stays byte-for-byte identical; the internal implementation gains type inference, removes a class of chained casts, and aligns with the patterns spelled out in rule 0014 ("Functions Over Classes for Public API").

In the same PR, deliver the pipeable functions that the `TODO` comments in `result/index.ts` and `maybe/index.ts` have been signalling since v1.0.

## Decisions

1. **Public surface is unchanged.** `ok`, `err`, `some`, `none`, `maybe`, `Unit`, `isResult`, `isMaybe`, `isUnit`, and the type names (`Ok`, `Err`, `Result`, `Some`, `None`, `Maybe`, `Unit`, `OkType`, `ErrType`, `SomeType`) keep their signatures and exported names. No new exports are *required* by this refactor; the pipeables are additive.
2. **Classes are internal.** `OkImpl`, `ErrImpl`, `SomeImpl`, `NoneImpl` live in `result/internal/` and `maybe/internal/` respectively. They are not re-exported. Per rule 0014, the only public construction point is the factory function.
3. **Type aliases over `interface`.** Per rule 0012, the public types are `type Ok<T,E> = OkImpl<T,E>` (and equivalents). The former `interface` declarations become type aliases pointing at the class. This removes the ambiguity of the rule 0012 exception list: classes are the open shape; `type` is the public contract.
4. **Private fields via `#`.** State is stored in `#value` / `#error` (or equivalent) using ECMAScript private fields. No `readonly` placeholder, no `private` TS keyword that compiles to public. Rule 0014 asks for true encapsulation; `#` delivers it.
5. **`none` is a static singleton.** `NoneImpl.NONE` is a single instance; `none` exports it. Mirrors the current behaviour with the same identity guarantees (`some(10) === some(10)` is intentionally false; `none === none` is true).
6. **Discrimination via `_tag` field.** The `_tag` field is public on the class instances (because `_tag` is part of the public type contract — `isResult` and `isMaybe` rely on it). Consumers inspect `_tag` for their own guards; the class does not expose `instanceof` checks.
7. **No `as unknown as ...` in the implementation.** The previous `constants.ts` relied on chained casts (rule 0008 violation) to convince the compiler that `return this` inside an `Ok` literal was typed as `Ok<T,E>`. Classes infer `this` correctly. The refactor removes every chained cast inside the refactored modules.
8. **Pipeables are pure functions.** Each pipeable is a function from a value to a function of the operation: `map<B>(fn: (value: T) => B): (result: Result<T,E>) => Result<B,E>`. They compose through `pipe`. They do not capture `this`.
9. **Unit is untouched.** `Unit` is a one-property singleton. Converting it to a class is ceremony without value. The rule of three (rule 0001, invariant 4) does not apply.

## File map

```
packages/fp/src/
├── index.ts # unchanged barrel (no new public exports outside pipeables)
├── types.ts # unchanged
├── result/
│ ├── types.ts # Ok/Err/Result as type aliases to OkImpl/ErrImpl
│ ├── constants.ts # ok()/err() factories use the internal classes
│ ├── internal/
│ │ ├── ok-impl.ts # OkImpl class (not exported)
│ │ └── err-impl.ts # ErrImpl class (not exported)
│ ├── functions.ts # NEW — pipeable map, flatMap, mapError, ...
│ └── index.ts # re-exports types, factories, AND pipeables
├── maybe/
│ ├── types.ts # Some/None/Maybe as type aliases
│ ├── constants.ts # some()/none()/maybe() factories
│ ├── internal/
│ │ ├── some-impl.ts # SomeImpl class (not exported)
│ │ └── none-impl.ts # NoneImpl class with NONE singleton
│ ├── functions.ts # NEW — pipeable map, flatMap, filter, ...
│ └── index.ts # re-exports types, factories, AND pipeables
└── unit/ # unchanged
```

## Out of scope

- Behaviour changes. Every public method keeps its current semantics.
- Test changes. `tests/index.test.ts` exercises the public API and should pass without edits.
- Documentation site (`apps/web/`). Doc updates land in a follow-up PR.
- `Try`, `pipe`, `flow`, `AsyncResult`, `Queue`, `Sequence`, `Collection`, `gen`. These are not in the current `src/`. If they exist in feature branches, they are merged independently.

## Rollout

- Single PR, single changeset.
- Changeset: `minor` if pipeables are considered a new feature; `patch` if we treat them as completion of an existing TODO. Proposal: `minor` (new exports).
- After merge to `staging`, the next release will exercise the new internal classes via the existing smoke test before publishing.
57 changes: 57 additions & 0 deletions docs/engineering/plans/function-utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Function utilities

**Status**: Implemented (PR #TBD).
**Date**: 2026-08-17.
**Branch**: `function/pipe-and-friends`.

## Goal

Deliver the function utilities that the documentation has been promising since v1.0 but the code has never shipped:

- `pipe` — left-to-right function composition with a starting value.
- `flow` — left-to-right function composition that returns a function.
- `identity` — the identity function.
- `constant` — wraps a value into a function that ignores its argument.
- `flip` — swaps the first two arguments of a binary function.
- `tupled` — converts a function whose first argument is a tuple into a function that takes the tuple.
- `untupled` — inverse of `tupled`.

These are the seven exports announced in `docs/internal/product/features/function-utilities.md` and in the top-level `README.md`.

## Decisions

1. **ESM-only, package-local.** The module lives at `packages/fp/src/function/`. No runtime dependencies. Pure functions, no state.
2. **Variadic overloads, not arrays.** `pipe(...args)` and `flow(...fns)` accept up to nine steps. Beyond that, the type system widens to `Function`-equivalent and the caller is on their own — this matches the spec in `function-utilities.md`.
3. **`identity` and `constant` are arrow functions, not classes.** Rule 0014 — classes are not exports. Style preference: arrow functions because they show the closure more clearly for these trivially-sized functions.
4. **`flip` works on two-arg functions only.** Three+ argument `flip` is a different shape (permutation); out of scope. Documented in JSDoc.
5. **`tupled` / `untupled` are inverses.** `untupled(tupled(f))` returns the same function shape as `f`. Tests assert both directions.
6. **No `any`.** All overloads are typed. The variadic tail collapses to `(...args: unknown[]) => unknown` only when the call site widens — the supplied overloads cover the documented arities (1-9).
7. **The new exports do not collide with the existing pipeables.** The barrel already disambiguates Maybe/Result pipeables by suffixing. The function utilities (`pipe`, `flow`, `identity`, `constant`, `flip`, `tupled`, `untupled`) keep their bare names because none of them clash with a `Result` or `Maybe` export.

## File map

```
packages/fp/src/
├── function/
│ ├── pipe.ts
│ ├── flow.ts
│ ├── identity.ts
│ ├── constant.ts
│ ├── flip.ts
│ ├── tupled.ts
│ ├── untupled.ts
│ └── index.ts # re-exports the seven functions
└── index.ts # extends the public barrel
```

## Out of scope

- `gen()` — generator composition. Larger feature, separate PR.
- `Try`, `sleep`, `retry`, `timeout`, `Queue`, `Predicate`, `Refinement`, `Context`, `Sequence`, `Collection` — listed in the README but unimplemented. Out of scope for this PR.
- `pipeAsync` — async pipeline variant. Can be a follow-up.

## Rollout

- Single PR, single changeset.
- Changeset: `minor` (new public exports).
- After merge to staging, the next release will surface the new exports via the existing smoke test.
43 changes: 19 additions & 24 deletions docs/internal/product/README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,61 @@
# @deessejs/fp — Functional Programming Utilities
# @deessejs/fp - Functional Programming Utilities

A lightweight TypeScript library of functional programming primitives. Designed to be simple, composable, and dependency-free.

## Philosophy

> **Simple by default.** No over-engineering, no fancy type gymnastics. Just the primitives you need to write cleaner code.

This library is the core of an ecosystem — intentionally minimal, fast to learn, and easy to extend.
This library is the core of an ecosystem - intentionally minimal, fast to learn, and easy to extend.

## Quick Start

```typescript
import { Result, ok, err, pipe } from '@deessejs/fp';
import { Result, ok, err, pipe, fromThrowable, attempt } from "@deessejs/fp";

// Result: represent values that may have failed
const divide = (a: number, b: number): Result<number, string> =>
b === 0 ? err('Division by zero') : ok(a / 2);
b === 0 ? err("Division by zero") : ok(a / b);

// Maybe: represent optional values
const findUser = (id: string): Maybe<User> => db.get(id);

// Chain operations
const result = pipe(
ok(5),
Result.map(n => n * 2),
Result.flatMap(n => n > 10 ? ok(n) : err('too small')),
);
// Wrap a throwing function
const readFile = fromThrowable({
onSuccess: () => fs.readFileSync("config.json", "utf-8"),
onError: (e) => (e instanceof Error ? e : new Error(String(e))),
});
```

## Features

### Core Primitives

- **[Result](features/result.md)** — `Ok | Err` pattern for type-safe error handling
- **[Maybe](features/maybe.md)** — `Some | None` pattern for optional values
- **[Try](features/try.md)** — Wrap sync/async operations that may throw
- **[Unit](features/unit.md)** — The unit type for void-returning functions
- **[Result](features/result.md)** - `Ok | Err` pattern for type-safe error handling, including wrapping throwing functions
- **[Maybe](features/maybe.md)** - `Some | None` pattern for optional values
- **[Unit](features/unit.md)** - The unit type for void-returning operations

### Function Utilities

- **[Function Utilities](features/function-utilities.md)** — `pipe`, `flow`, `identity`, `constant`, `flip`, `tupled`
- **[Function Utilities](features/function-utilities.md)** - `pipe`, `flow`, `identity`, `constant`, `flip`

### Async Utilities

- **[Async Utilities](features/async-utilities.md)** — `sleep`, `retry`, `timeout`, `Queue`
- **[Async Utilities](features/async-utilities.md)** - `sleep`, `retry`, `timeout`, `Queue`

### Predicate Utilities

- **[Predicate Utilities](features/predicate-utilities.md)** — `Predicate`, `Refinement`, `not`, `and`, `or`
- **[Predicate Utilities](features/predicate-utilities.md)** - `Predicate`, `Refinement`, `not`, `and`, `or`

### Collection Types

- **[Collection Types](features/collection-types.md)** — `Context`, `Sequence`, `Collection`, AsyncIterator utils
- **[Collection Types](features/collection-types.md)** - `Context`, `Sequence`, `Collection`, AsyncIterator utils

### Advanced

- **[Generator Composition](features/generator-composition.md)** — `gen()` with `yield*` for clean async flows
- **[Serialization](features/serialization.md)** — `serialize`/`deserialize` for RPC
- **[Generator Composition](features/generator-composition.md)** - `gen()` with `yield*` for clean async flows
- **[Serialization](features/serialization.md)** - `serialize`/`deserialize` for RPC

### Ecosystem

- **[Ecosystem Integration](features/ecosystem-integration.md)** — First-class `@deessejs/errors` support
- **[Ecosystem Integration](features/ecosystem-integration.md)** - First-class `@deessejs/errors` support

## Installation

Expand Down
Loading
Loading