Skip to content

Path to V3#101

Merged
baxyz merged 99 commits into
mainfrom
v3
Jul 17, 2026
Merged

Path to V3#101
baxyz merged 99 commits into
mainfrom
v3

Conversation

@baxyz

@baxyz baxyz commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Description

Please include a summary of what this PR does and why it's needed.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring
  • Test improvement

Related Issues

Closes #(issue number)

How Has This Been Tested?

Describe the tests you ran and how to reproduce them:

  • Test A
  • Test B

Checklist

  • My code follows the code style of this project
  • I have updated the documentation accordingly
  • I have added tests for my changes
  • All new and existing tests passed locally
  • My commits follow the conventional commit format

Screenshots (if applicable)

Add screenshots for UI changes.

Additional Context

Add any other context about the PR here.

@baxyz
baxyz force-pushed the v3 branch 2 times, most recently from 5d96742 to c1767b3 Compare June 27, 2026 13:48
@baxyz
baxyz force-pushed the v3 branch 2 times, most recently from f25251a to 4301f38 Compare July 8, 2026 20:04
Comment thread scopes.json Outdated
Comment thread AGENTS.md Outdated
Comment thread helpers/date/difference.ts
Comment thread helpers/type/DeepSet.ts
@baxyz

baxyz commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Additional findings (not anchorable inline — files outside this PR's diff)

Security gap: helpers/object/compact.ts and helpers/object/pick.ts are missing the prototype-pollution guard this PR gave their siblings.

This PR's _unsafeKeys extraction into helpers/_shared/ is well-adopted — cloneDeep.ts, mergeDeep.ts, groupBy.ts, map.ts, set.ts, invert.ts, and array/countBy.ts all guard against __proto__/constructor/prototype keys when copying object keys dynamically. compact.ts and pick.ts do the identical category of dynamic key-copy (for (const key of Object.keys(obj)) result[key] = obj[key]) with no such guard.

Confirmed exploitable, not theoretical:

const malicious = JSON.parse('{"a":1,"__proto__":{"polluted":"yes"}}');
// JSON.parse creates an own enumerable "__proto__" key (doesn't touch the real prototype)
compact(malicious).polluted // => "yes", via the result object's silently-swapped prototype
Object.keys(compact(malicious)) // => ['a'] — "polluted" isn't even an own key, just resolves through the chain

So compact()'s output gets its own [[Prototype]] silently swapped whenever the input has an own __proto__ key (e.g. from parsing untrusted JSON) — downstream code doing if (result.someFlag) can be spoofed via prototype-chain lookup even though someFlag was never an own property. Same pattern applies to pick().

Recommend adding the same UNSAFE_KEYS skip to both, for consistency with the rest of this PR's hardening pass.


README.md's @helpers4/type row (not touched by this diff, so not inline-commentable) is stale after the helpers/typehelpers/guard rename. It still describes @helpers4/type as "Type guards and type-checking utilities" — that's now @helpers4/guard, which has no row at all. Worth fixing alongside the AGENTS.md doc comment above.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

baxyz and others added 19 commits July 14, 2026 00:30
Deleted `isEmpty` and all associated tests/examples from the `type`
category. The per-category replacements (`array/isEmpty`, `string/isEmpty`,
`object/isEmpty`) have been available since 2.0.0 and carry the same JSDoc.

Breaking change for v3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted `safeDate` and `dateToISOString` (both in `date/safeDate.ts`)
along with their test file. Replacements available since v1.9.0:
- `safeDate` → `ensureDate`
- `dateToISOString` → `toISO8601` (date/format)

Breaking change for v3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the `daysDifference` function from `difference.ts`, deleted its
dedicated example file, and stripped all daysDifference describe blocks
from `.test.ts` and `.spec.ts`. The replacement `difference` (available
since 2.0.0) covers the same use-case with a richer API (unit, sign).

Breaking change for v3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the `deepClone` re-export alias from `cloneDeep.ts`. Replacement
`cloneDeep` has been the canonical name since the symbol was deprecated
(marked `@deprecated` since v1.9.0).

Breaking change for v3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the `deepMerge` re-export alias from `mergeDeep.ts`. Replacement
`mergeDeep` has been the canonical name since the symbol was deprecated
(marked `@deprecated` since v1.9.0).

Breaking change for v3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Created `helpers/_shared/_unsafeKeys.ts` as the single source of truth for
the prototype-pollution guard keys. All consumers (`array/countBy`,
`object/map|cloneDeep|mergeDeep|groupBy|set|invert|_types`) now import from
`'../_shared/_unsafeKeys.js'`; the file is inlined into each category bundle
at compile time so there is no runtime cross-package dependency.

Deleted the two duplicated copies `array/_unsafeKeys.ts` and
`object/_unsafeKeys.ts`. The build script now explicitly skips any directory
whose name starts with `_` to prevent accidental packaging of internal
shared directories.

Breaking change for v3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moved all 141 runtime type-guard files (`is*`) from `helpers/type/` to the
new `helpers/guard/` category. The compile-time utility types (`DeepPartial`,
`DeepWritable`, `Maybe`) remain in `helpers/type/`, which now has a focused
purpose.

- New package: `@helpers4/guard` (was `@helpers4/type` for runtime guards)
- `helpers/guard/config.json` created
- `helpers/type/config.json` updated to reflect compile-time-only purpose

Breaking change for v3: consumers of runtime guards must update their
import from `@helpers4/type` to `@helpers4/guard`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The `helpers/type/` category is now exclusively compile-time TypeScript
utility types — zero runtime footprint, always imported with `import type`.

Existing types kept:
- `DeepPartial`, `DeepWritable`, `Maybe`

Promoted from internal `object/_types.ts` (standalone, no cross-package dep):
- `UnionToIntersection<U>` — collapses A | B | C → A & B & C
- `DeepGet<T, Path>` — value type at a path within T
- `DeepSet<T, Path, V>` — produces T with value at Path replaced by V
  (`ParsePath` stays internal to object/ — too path-specific)

New utility types added:
- `Brand<T, B>` — nominal typing via phantom tag
- `Prettify<T>` — flattens intersections for IDE readability
- `Nullable<T>` / `Nullish<T>` — `T | null` / `T | null | undefined`
- `ValueOf<T>` — union of all value types of an object type
- `KeysOfType<T, V>` — keys whose values extend V
- `PickByValue<T, V>` / `OmitByValue<T, V>` — filter by value type
- `RequiredKeys<T>` / `OptionalKeys<T>` — required/optional key sets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The inner callbacks of `truthyPromiseOrThrow`, `falsyPromiseOrThrow`, and
`meaningPromiseOrThrow` previously typed `data` as `unknown` and returned
it with `as T` — an unsound cast invisible at the call site.

Fix: type `data` as `T` directly in the returned closure (matching the
outer overload signature). No cast needed; TypeScript infers the return
correctly. The now-redundant `as T` / `as any[]` / `as object` annotations
and their `eslint-disable` suppressions are removed.

`meaningPromiseOrThrow` additionally extracts the predicate into a private
`isMeaningless(value: unknown): boolean` helper, eliminating all remaining
casts in the function body. Only `functional/no-throw-statement` comments
remain — they suppress a rule about `throw` style, not a type issue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
helpers/_shared/ is an internal utility folder — not a publishable category
and has no config.json by design.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- simplify repository purpose and stack
- refine key commands and rules
- remove critical restrictions and organization context
scopes.json had a missing comma making the whole file invalid JSON,
breaking any tooling that parses it for commit-scope validation.

Also reconciles the list against `git log main..v3 --format=%s`: adds
agents, coherency, guard, release, scorecard, security, shared — all
used by commits on this branch but absent from the list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The helper-placement rule still said runtime type predicates go to
type/, contradicting the guard/type category split this PR
introduces (type predicates -> guard/, compile-time-only utility
types -> type/).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The removed daysDifference always rounded to a whole number of days.
Its replacement, difference() with the default 'days' unit, returns
the exact fractional value instead — silently breaking `=== 0`-style
migrations from daysDifference. Documents this explicitly in the
JSDoc and locks it in with a test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
baxyz and others added 28 commits July 14, 2026 21:41
Add MIGRATION.md documenting the six breaking changes already merged to
v3 (helpers/type -> helpers/guard package split, and five removed
deprecated aliases). None of the original refactor commits used the
Conventional Commits BREAKING CHANGE footer syntax, so git-cliff's
CHANGELOG automation was silently missing all of them - this commit
carries the correctly-formatted footer so the next changelog
regeneration renders them with the [**BREAKING**] marker.

BREAKING CHANGE: @helpers4/type -> @helpers4/guard for runtime type
guards (isString, isArray, isDefined, ...); object.deepMerge ->
object.mergeDeep; object.deepClone -> object.cloneDeep;
date.daysDifference -> date.difference; date.safeDate ->
date.ensureDate; date.dateToISOString -> date.toISO8601; type.isEmpty
-> array.isEmpty / string.isEmpty / object.isEmpty. See MIGRATION.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Unreleased had been stale since 2.0.0-alpha.11 (2026-04-13) - every
release from 2.0.0-alpha.14 through 2.1.0, plus the whole v3 branch's
work, was missing. Regenerated via the project's own 'pnpm run
changelog' (git-cliff), picking up the properly-tagged BREAKING CHANGE
footer from the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… finding

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a root llms.txt (llmstxt.org format) summarizing packages,
conventions, and doc links for LLM/agent consumption.

While rebuilding the package list from helpers/*/config.json, found
the README's package table was missing 4 already-published packages
(@helpers4/ci, @helpers4/commit, @helpers4/id, @helpers4/markdown) —
added them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AGENTS.md said cross-package imports 'break tree-shaking' as the
reason not to merge compact/equalsShallow. That's false and
contradicted by 20+ existing cross-category imports already in the
codebase (array/sample.ts -> number/clamp, object/diff.ts ->
array/equalsDeep + date/compare, etc.) - Rollup inlines them
per-package, verified via build/<category>/package.json having no
dependencies field. Replaced with the real reason (genuinely different
implementations) and documented the actual safe pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operationalizes CONTRIBUTING.md's 'Creating a new helper' walkthrough
as an invokable skill, plus bakes in conventions that live only in
AGENTS.md or in this session's history (naming: avoid lodash/math
jargon, cross-category imports are safe, per-turn commit
authorization).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…in listing)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also adds commit/ category benchmarks in the same commit (small batch,
same review pass).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also adds id/ and url/ category benchmarks in the same commit (small
batches, same review pass). Skipped observable/combine and
observable/combineLatest — thin RxJS operator wrappers, not
meaningfully sync-benchable, same exclusion as promise/* wrappers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y imports break per-package publishing

Tested every category with a real jsr publish --dry-run. The slow
types check (id/uuid7 passed cleanly) turned out to be the easy part.
The real blocker: 7/18 categories have cross-category imports that
Rollup inlines fine for npm but JSR (no bundler, raw source per
package) rejects as excluded-module. Documented the exact file list
per category and the two viable fix paths (duplicate transitive deps
per package vs real JSR-to-JSR deps + import rewriting). No jsr.json
or pipeline changes committed - investigation only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@baxyz
baxyz merged commit de7746f into main Jul 17, 2026
15 checks passed
@baxyz
baxyz deleted the v3 branch July 17, 2026 16:58
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