Skip to content

Production-grade hardening: publishable packages, Changesets, CI matrix, coverage gate#1

Merged
danfry1 merged 42 commits into
mainfrom
production-grade-concerns
Jun 14, 2026
Merged

Production-grade hardening: publishable packages, Changesets, CI matrix, coverage gate#1
danfry1 merged 42 commits into
mainfrom
production-grade-concerns

Conversation

@danfry1

@danfry1 danfry1 commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Brings faultline to a production-grade, trustworthy state across packaging, CI, tests, release process, docs, and a round of adversarial hardening.

Packaging & publishability

  • faultline-cli is now npm-publishable and runs on plain Node (built dist/ via tsdown, #!/usr/bin/env node bin, typescript as a runtime dep, files/metadata/README/LICENSE).
  • eslint-plugin-faultline: added the missing README.md (was referenced by files → broken publish), meta.version now inlined from package.json at build time (no more drift), tests run in prepublishOnly.
  • faultline-vscode marked private/experimental (Marketplace, not npm).

Release process — Changesets

  • Adopted Changesets: per-package versioning + provenance publishing. release.yml rewritten (version PR → publish). VS Code ignored.
  • Committed bun.lock (was gitignored despite CI using --frozen-lockfile).
  • Changesets staged for all three publishable packages' unreleased work.

CI

  • Build all publishable packages (was core-only); test step enforces the coverage gate.
  • New Node 18/20/22 matrix that builds the artifacts and smoke-loads them under real Node (faultline CJS+ESM, eslint-plugin require, CLI bin execution).

Tests & coverage

  • Core coverage gate (90% line/func) in bunfig.toml.
  • result.ts 47%→100%, task-result.ts100%, dedicated attempt/from-unknown suites. ~196 core tests.

Adversarial hardening (pressure test)

Built a realistic service and tried to break the thin paths. Core held up (no crashes/corruption/injection, 25k-deep chains, graceful on untrusted input). Fixed the sharp edges found:

  • Redaction fail-open (security): added a ** globstar so **.password redacts a key at any depth; documented that paths fail open. Previously a naive ['password'] config silently leaked everything.
  • boundary.try(): non-throwing variant returning Result<To, BoundaryViolation> for use at a transport edge.
  • Docs: clarified that TaskResult/attemptAsync cancellation is cooperative (doesn't preempt in-flight work) and that boundaries throw on an unmapped tag.

Repo hygiene

CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, issue forms + PR template, README badges + Contributing/packages section.

Notes for the reviewer

  • Versions are staged, not bumped (harden-now / version-later). Merging → Changesets opens a "Version Packages" PR; merging that publishes faultline + eslint-plugin-faultline (0.3.0) and the first faultline-cli 0.1.0.
  • Release workflow needs the npm environment + NPM_TOKEN secret (already referenced by the prior workflow).
  • 1.0 decision intentionally deferred.

All green locally: typecheck, ~216 tests across packages, lint, build, Node smoke tests.

danfry1 added 30 commits May 22, 2026 14:48
Captures the design for four scoped workstreams:
1. README positioning + tagline
2. Explicit error-code escape hatch (docs gap)
3. TaskResult production-grade pass (cancellation, pruning, docs)
4. ESLint plugin robustness via fixture corpus

Sequenced 1 -> 2 -> 3 -> 4; small/safe items first.
30 bite-sized tasks across four phases:
- Phase 1 (T1-2): README tagline + positioning
- Phase 2 (T3-5): explicit error-code escape hatch docs + tests
- Phase 3 (T6-12): TaskResult cancellation, pruning, fromPromise fix, docs, changelog
- Phase 4 (T13-28): ESLint torture corpus, triage, three rule fixes, LIMITATIONS.md, changelog
- Wrap-up (T29-30): green-build verification + PR
BREAKING CHANGE: TaskResult.toPromise() has been removed. Call .run() instead — it has the same signature and behavior.
Previously, a rejection from the factory bubbled up as an uncaught promise rejection.
Now it's wrapped as System.Unexpected with the original throw preserved as .cause.

BREAKING CHANGE: fromPromise return type now widens to include System.Unexpected.
Records expected behavior. Triage results follow in Task 23.
Replace the three `callSigs[0]!` sites in throw-type-mismatch with a
union loop over all signatures so overloaded functions are fully
inspected. Update the overloads fixture to use TypedPromise return
types and expect a transitiveError report for the undeclared E.B error
that was previously invisible.
danfry1 added 12 commits May 22, 2026 16:31
…scope

Add Identifier to the undeclaredThrow filter in ThrowStatement handler.
Previously, `throw someVar` where the variable's type was unknown/any was
silently ignored even inside a TypedPromise-returning function. Now reports
undeclaredThrow for Identifier throws alongside NewExpression and Literal.

Adds variable-throw torture fixture demonstrating the blind spot.
… try-block throws

Throws inside async function literals (e.g. setTimeout callbacks, .map
handlers) are wrapped in rejected promises and cannot synchronously escape
to the enclosing catch block. Previously, walkNode would descend into these
async closures and misattribute their TypedPromise-annotated calls as throws
that the outer catch must handle.

Fix: update walkNode's callback signature to support a boolean skip signal
(return false halts descent), then return false when the try-block walk
encounters an async FunctionExpression or ArrowFunctionExpression.
Synchronous inline lambdas are intentionally NOT skipped — their throws can
propagate synchronously (see the generics fixture).

Adds a forward-looking torture fixture (nested-try case 3) that confirms
async-closure throws are not attributed to the outer catch.
- build to dist/ via tsdown (ESM, shebang preserved, typescript external)
- bin -> ./dist/index.js with #!/usr/bin/env node (was raw .ts + bun shebang)
- declare typescript as a runtime dependency; drop unused faultline dep
- add files, repository, homepage, author, keywords, engines, publishConfig
- add README and LICENSE
- add README.md (was referenced by files field but missing -> broken publish)
- inline meta.version from package.json at build time (was hardcoded 0.1.0, drifted from 0.2.0)
- run tests in prepublishOnly (was typecheck + build only)
…gate

- result.test.ts: every Ok/Err method, match/catchTag/all combinators (result.ts 47%->100% funcs)
- attempt.test.ts: async mapUnknown/mapAbort, pre-abort race, happy paths
- from-unknown.test.ts: full context frames, AppError passthrough, detail preservation
- serialize.test.ts: standalone AppError JSON.stringify uses toJSON
- bunfig.toml: enforce 90% line/function coverage gate
- add @changesets/cli + .changeset config (public access, vscode ignored)
- rewrite release.yml: Changesets action opens a version PR, then publishes
  to npm with provenance (replaces manual tag-triggered publish)
- root scripts: changeset, version-packages, release (builds the 3 publishable
  packages then changeset publish)
- commit bun.lock (was gitignored despite CI using --frozen-lockfile)
- mark faultline-vscode private (ships to VS Code marketplace, not npm)
- changesets capturing unreleased work: core TaskResult changes (minor),
  eslint-plugin rule fixes + packaging (minor), CLI publishability (minor)
- root CHANGELOG now points to per-package changelogs owned by Changesets
- quality job now builds faultline + eslint-plugin + cli (was faultline only)
- test step enforces the coverage gate
- new node-compat matrix builds artifacts and smoke-loads them under real
  Node 18/20/22: faultline CJS+ESM, eslint-plugin require, cli bin execution
- keeps the are-the-types-wrong export validation for faultline
- README: npm/CI/license/zero-deps badges, Contributing section with package
  status table and dev commands, mark VS Code extension experimental
- CONTRIBUTING.md: monorepo workflow, test/coverage/changeset expectations
- SECURITY.md: private vulnerability reporting policy
- CODE_OF_CONDUCT.md: Contributor Covenant 2.1
- issue forms (bug/feature) + config, pull request template
Addresses sharp edges found while adversarially pressure-testing:

- redaction: add '**' globstar segment so a sensitive key can be redacted at
  any depth (e.g. '**.password' covers root, nested objects, and array
  elements). redactPaths fail open (a non-matching path silently redacts
  nothing), so '**.<key>' is now the recommended form; documented in README
  and SECURITY.md. Previously a naive ['password'] config leaked everything.
- attemptAsync/TaskResult: document that cancellation is cooperative — it
  short-circuits subsequent steps and stops the caller waiting, but does not
  preempt in-flight work unless the function threads the signal through.
- defineBoundary: document that it throws System.BoundaryViolation on an
  unmapped tag and overwrites a handler-set cause (both already tested).

Behavior change is additive (new '**' wildcard); the rest is docs.
…to 100%

- defineBoundary: add boundary.try(error) returning Result<To, BoundaryViolation>
  instead of throwing on an unmapped tag — safe to use at a transport edge.
  Shares mapping logic with the throwing call so behavior stays in lock-step.
  Exposes the BoundaryViolationError type.
- task-result: tests for andThen (ok/err/short-circuit/async), catchTag with a
  TaskResult-returning handler, and the instance match() — task-result.ts now
  100% line/function coverage.
@danfry1 danfry1 merged commit 7a010cc into main Jun 14, 2026
4 checks passed
@danfry1 danfry1 deleted the production-grade-concerns branch June 14, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant