{"_id":"@bupkis/property-testing","_rev":"5-867acd241cf002c58ebff775dbae31ec","name":"@bupkis/property-testing","dist-tags":{"latest":"1.0.1"},"versions":{"0.1.0":{"name":"@bupkis/property-testing","version":"0.1.0","keywords":["bupkis","property-testing","fast-check","testing","assertions"],"author":{"name":"Christopher Hiller","email":"boneskull@boneskull.com"},"license":"BlueOak-1.0.0","_id":"@bupkis/property-testing@0.1.0","maintainers":[{"name":"boneskull","email":"boneskull@boneskull.com"}],"homepage":"https://bupkis.zip","bugs":{"url":"https://github.com/boneskull/bupkis/issues"},"dist":{"shasum":"591e2ac7d7d8afe995edc5088702e9bc8cd4bae5","tarball":"https://registry.npmjs.org/@bupkis/property-testing/-/property-testing-0.1.0.tgz","fileCount":56,"integrity":"sha512-fn35Gc2qeyKOIOmRnJednm1BSQSneDgBAtAHuixF19AASl+lI1adzBiBDrFjVAzbfjc4FirUgSo0lpaV9BVwfA==","signatures":[{"sig":"MEUCIQDN9FSIapmjiybBiTQvqI33EU8W27wX0t2WG2qndkMK2gIgHh/VM8cRZXat/8MWfv6P4SFMS2ywHxICt/3KycQ8NZw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":329981},"main":"./dist/index.cjs","type":"module","zshy":{"exports":{".":"./src/index.ts","./package.json":"./package.json"}},"types":"./dist/index.d.cts","module":"./dist/index.js","engines":{"node":"^20.19.0 || ^22.12.0 || >=23"},"exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"ca2eb98497de0200e5861cb8b7de9d48ca26439d","scripts":{"test":"npm run test:base -- \"test/*.test.ts\"","build":"zshy","test:base":"node --test --test-reporter=spec --import tsx","test:types":"echo \"No type tests yet\"","test:node20":"npm run test:base -- test/*.test.ts"},"_npmUser":{"name":"boneskull","email":"boneskull@boneskull.com"},"prettier":{"tsdoc":true,"plugins":["prettier-plugin-jsdoc","prettier-plugin-pkg","prettier-plugin-sort-json"],"singleQuote":true,"jsdocPreferCodeFences":true,"jsdocCommentLineStrategy":"keep"},"repository":{"url":"git+https://github.com/boneskull/bupkis.git","type":"git","directory":"packages/property-testing"},"_npmVersion":"11.6.2","description":"Property-based testing harness for bupkis assertions","directories":{},"_nodeVersion":"24.12.0","dependencies":{"zod":"4.3.5"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"fast-check":"4.5.2"},"peerDependencies":{"bupkis":">=0.15.0","fast-check":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/property-testing_0.1.0_1768432865554_0.6319121472120257","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bupkis/property-testing","version":"0.1.1","keywords":["bupkis","property-testing","fast-check","testing","assertions"],"author":{"name":"Christopher Hiller","email":"boneskull@boneskull.com"},"license":"BlueOak-1.0.0","_id":"@bupkis/property-testing@0.1.1","maintainers":[{"name":"boneskull","email":"boneskull@boneskull.com"}],"homepage":"https://bupkis.zip","bugs":{"url":"https://github.com/boneskull/bupkis/issues"},"dist":{"shasum":"47d00ce5ba5e04381825b8993daea356fe5af0ab","tarball":"https://registry.npmjs.org/@bupkis/property-testing/-/property-testing-0.1.1.tgz","fileCount":56,"integrity":"sha512-S0DQabOdQF4WqCZrWXVLQErUBQcTlJYrQkm+AFR6OxFZ6Q8nIZmIMDy9N4PPswf2s4AOdYAvJ6zgQUUCb0Oieg==","signatures":[{"sig":"MEUCIAupfATt0RVczKKXc78Ewe1xTBGJCDfQ0SrZorptJAZqAiEAw8PCMzEWMUISNYXnR6S1gPvGuXoi5sRkM/ILMus5ZuM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":332912},"main":"./dist/index.cjs","type":"module","zshy":{"exports":{".":"./src/index.ts","./package.json":"./package.json"}},"types":"./dist/index.d.cts","module":"./dist/index.js","engines":{"node":"^20.19.0 || ^22.12.0 || >=23"},"exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"f09ff1cfb21ceb03dee850971611a3d1e98086b4","scripts":{"test":"npm run test:base -- \"test/*.test.ts\"","build":"zshy","test:base":"node --test --test-reporter=spec --import tsx","test:types":"echo \"No type tests yet\"","test:node20":"npm run test:base -- test/*.test.ts"},"_npmUser":{"name":"boneskull","email":"boneskull@boneskull.com"},"prettier":{"tsdoc":true,"plugins":["prettier-plugin-jsdoc","prettier-plugin-pkg"],"singleQuote":true,"jsdocPreferCodeFences":true,"jsdocCommentLineStrategy":"keep"},"repository":{"url":"git+https://github.com/boneskull/bupkis.git","type":"git","directory":"packages/property-testing"},"_npmVersion":"11.6.2","description":"Property-based testing harness for bupkis assertions","directories":{},"_nodeVersion":"24.12.0","dependencies":{"zod":"4.3.5"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"fast-check":"4.5.2"},"peerDependencies":{"bupkis":">=0.15.0","fast-check":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/property-testing_0.1.1_1768604263491_0.10181581017310437","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@bupkis/property-testing","version":"0.2.0","keywords":["bupkis","property-testing","fast-check","testing","assertions"],"author":{"name":"Christopher Hiller","email":"boneskull@boneskull.com"},"license":"BlueOak-1.0.0","_id":"@bupkis/property-testing@0.2.0","maintainers":[{"name":"boneskull","email":"boneskull@boneskull.com"}],"homepage":"https://bupkis.zip","bugs":{"url":"https://github.com/boneskull/bupkis/issues"},"dist":{"shasum":"940659d0b05eba7b87bce53b1525f9f573b2b98f","tarball":"https://registry.npmjs.org/@bupkis/property-testing/-/property-testing-0.2.0.tgz","fileCount":8,"integrity":"sha512-2YBmdecme3cOLuxsUnbviHzK+ZagdnEPH6HIV8qIJ7GxV08ZDMgryyeKBx8SMq9aLyLE0i3mbCSpAeZyRK3IXg==","signatures":[{"sig":"MEQCIDZnjZxD8JdSN2y+5ZScXkokPO7Wm5tE82NWzOhafodeAiBzGK97zZqpGUmrgffEpNwR3D3JWZrKXtKq14+3fqB6Xg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":78339},"main":"./dist/index.cjs","type":"module","zshy":{"exports":{".":"./src/index.ts","./package.json":"./package.json"}},"types":"./dist/index.d.cts","module":"./dist/index.js","engines":{"node":"^20.19.0 || ^22.12.0 || >=23"},"exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"64ef732a58e91cc6c0808149500c8eb3de56c4e1","scripts":{"test":"npm run test:base -- \"test/*.test.ts\"","build":"zshy","test:base":"node --test --test-reporter=spec --import tsx","test:types":"echo \"No type tests yet\"","test:node20":"npm run test:base -- test/*.test.ts"},"_npmUser":{"name":"boneskull","email":"boneskull@boneskull.com"},"prettier":{"tsdoc":true,"plugins":["prettier-plugin-jsdoc","prettier-plugin-pkg"],"singleQuote":true,"jsdocPreferCodeFences":true,"jsdocCommentLineStrategy":"keep"},"repository":{"url":"git+https://github.com/boneskull/bupkis.git","type":"git","directory":"packages/property-testing"},"_npmVersion":"11.16.0","description":"Property-based testing harness for bupkis assertions","directories":{},"_nodeVersion":"24.18.0","dependencies":{"zod":"4.4.3"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"peerDependencies":{"bupkis":">=0.15.0","fast-check":">=4.0.0 <4.6.0"},"_npmOperationalInternal":{"tmp":"tmp/property-testing_0.2.0_1782785770984_0.003803302597630065","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@bupkis/property-testing","version":"1.0.0","keywords":["bupkis","property-testing","fast-check","testing","assertions"],"author":{"name":"Christopher Hiller","email":"boneskull@boneskull.com"},"license":"BlueOak-1.0.0","_id":"@bupkis/property-testing@1.0.0","maintainers":[{"name":"boneskull","email":"boneskull@boneskull.com"}],"homepage":"https://bupkis.zip","bugs":{"url":"https://github.com/boneskull/bupkis/issues"},"dist":{"shasum":"29700a26a62b3ac4586711ee60b8ad73636a215f","tarball":"https://registry.npmjs.org/@bupkis/property-testing/-/property-testing-1.0.0.tgz","fileCount":56,"integrity":"sha512-8WWe4JpcWMYoY6rXm0u1RjwatQtB+0R6oCphl6s1qzuCu/zES4BtLmo4Q+lKZ6tLLQqtkL/+VcS5RgXKDdLyyw==","signatures":[{"sig":"MEUCIQCbIK2UIhufmPYCEwpsd5BCqzJD+UsN/i0ZNWPat0VJsAIgJd+JdHmVng6NSnYPntvvVYCObz2kkrN56mnvIzL3FYI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":350247},"main":"./dist/index.cjs","type":"module","zshy":{"exports":{".":"./src/index.ts","./package.json":"./package.json"}},"types":"./dist/index.d.cts","module":"./dist/index.js","engines":{"node":"^22.12.0 || >=23"},"exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"c0f3fcd74ca0f7e307d2a68ce12bba81b4059eb5","scripts":{"test":"npm run test:base -- \"test/*.test.ts\"","build":"zshy","test:ci":"npm run test -- --test-reporter=node-test-github-reporter","test:dev":"npm run test -- --watch","test:base":"node --test --test-reporter=spec --import tsx","test:coverage":"c8 --reporter=lcov --reporter=text npm test"},"_npmUser":{"name":"boneskull","email":"boneskull@boneskull.com"},"prettier":{"tsdoc":true,"plugins":["prettier-plugin-jsdoc","prettier-plugin-pkg"],"singleQuote":true,"jsdocPreferCodeFences":true,"jsdocCommentLineStrategy":"keep"},"repository":{"url":"git+https://github.com/boneskull/bupkis.git","type":"git","directory":"packages/property-testing"},"_npmVersion":"11.16.0","description":"Property-based testing harness for bupkis assertions","directories":{},"_nodeVersion":"24.18.0","dependencies":{"zod":"4.4.3"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"peerDependencies":{"bupkis":">=0.15.0","fast-check":">=4.0.0 <4.6.0"},"_npmOperationalInternal":{"tmp":"tmp/property-testing_1.0.0_1783650189441_0.3868736149215437","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@bupkis/property-testing","version":"1.0.1","type":"module","description":"Property-based testing harness for bupkis assertions","repository":{"directory":"packages/property-testing","type":"git","url":"git+https://github.com/boneskull/bupkis.git"},"homepage":"https://bupkis.zip","author":{"name":"Christopher Hiller","email":"boneskull@boneskull.com"},"license":"BlueOak-1.0.0","engines":{"node":"^22.12.0 || >=23"},"main":"./dist/index.cjs","types":"./dist/index.d.cts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"keywords":["bupkis","property-testing","fast-check","testing","assertions"],"scripts":{"build":"zshy","test":"npm run test:base -- \"test/*.test.ts\"","test:base":"node --test --test-reporter=spec --import tsx","test:ci":"npm run test -- --test-reporter=node-test-github-reporter","test:coverage":"c8 --reporter=lcov --reporter=text npm test","test:dev":"npm run test -- --watch"},"peerDependencies":{"bupkis":">=0.15.0","fast-check":">=4.0.0 <4.6.0"},"dependencies":{"zod":"4.4.3"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"prettier":{"jsdocCommentLineStrategy":"keep","jsdocPreferCodeFences":true,"plugins":["prettier-plugin-jsdoc","prettier-plugin-pkg"],"singleQuote":true,"tsdoc":true},"zshy":{"exports":{".":"./src/index.ts","./package.json":"./package.json"}},"_id":"@bupkis/property-testing@1.0.1","bugs":{"url":"https://github.com/boneskull/bupkis/issues"},"_integrity":"sha512-4magfaYzL3cd92TLqfb8Sk2TbAzgpH+3o5IQvidhbpzp5i1cHqAEgkMqvz0+OSBVK98BBWxM9OL8+zldAjqBYA==","_resolved":"/home/runner/work/_temp/bupkis-tarballs/bupkis-property-testing-1.0.1.tgz","_from":"file:/home/runner/work/_temp/bupkis-tarballs/bupkis-property-testing-1.0.1.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-4magfaYzL3cd92TLqfb8Sk2TbAzgpH+3o5IQvidhbpzp5i1cHqAEgkMqvz0+OSBVK98BBWxM9OL8+zldAjqBYA==","shasum":"826f16483b19eea707627f4792f7209eeaa21f59","tarball":"https://registry.npmjs.org/@bupkis/property-testing/-/property-testing-1.0.1.tgz","fileCount":56,"unpackedSize":353950,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bupkis%2fproperty-testing@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCrkCB+CD6VI8++nmg0jkzyi6rGu988VMUzp29j7nfj3QIgLSeI4Nr4JcKI9qJG+7XpdvU1l9BakFobUN+2KTLuwps="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:534de2c3-0113-42cd-a3d3-fa43a958de66"}},"directories":{},"maintainers":[{"name":"boneskull","email":"boneskull@boneskull.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/property-testing_1.0.1_1785881515710_0.7526360833218333"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-14T23:21:05.373Z","modified":"2026-08-04T22:11:56.252Z","0.1.0":"2026-01-14T23:21:05.732Z","0.1.1":"2026-01-16T22:57:43.675Z","0.2.0":"2026-06-30T02:16:11.139Z","1.0.0":"2026-07-10T02:23:09.592Z","1.0.1":"2026-08-04T22:11:55.851Z"},"bugs":{"url":"https://github.com/boneskull/bupkis/issues"},"author":{"name":"Christopher Hiller","email":"boneskull@boneskull.com"},"license":"BlueOak-1.0.0","homepage":"https://bupkis.zip","keywords":["bupkis","property-testing","fast-check","testing","assertions"],"repository":{"directory":"packages/property-testing","type":"git","url":"git+https://github.com/boneskull/bupkis.git"},"description":"Property-based testing harness for bupkis assertions","maintainers":[{"name":"boneskull","email":"boneskull@boneskull.com"}],"readme":"# @bupkis/property-testing\n\nProperty-based testing harness for [bupkis](https://bupkis.zip) assertions.\n\nThis package provides utilities for systematically testing bupkis assertions using [fast-check](https://fast-check.dev). It handles the boilerplate of testing all four assertion variants (valid, invalid, valid-negated, invalid-negated) so you can focus on defining your generators.\n\n## Installation\n\n```bash\nnpm install @bupkis/property-testing --save-dev\n```\n\n**Peer dependencies:**\n\n- `bupkis` >= 0.15.0\n- `fast-check` >= 4.0.0 < 4.6.0\n\n## Quick Start\n\n```ts\nimport {\n  createPropertyTestHarness,\n  extractPhrases,\n  filteredAnything,\n  getVariants,\n  type PropertyTestConfig,\n} from '@bupkis/property-testing';\nimport fc from 'fast-check';\nimport { describe, it } from 'node:test';\n\nimport { expect, expectAsync } from './my-assertions.js';\nimport { myAssertion } from './my-assertion.js';\n\n// Create the harness with your expect functions\nconst { runVariant } = createPropertyTestHarness({ expect, expectAsync });\n\n// Define test configuration\nconst testConfig: PropertyTestConfig = {\n  valid: {\n    generators: [\n      fc.integer().filter((n) => n % 2 === 0), // even numbers\n      fc.constantFrom(...extractPhrases(myAssertion)),\n    ],\n  },\n  invalid: {\n    generators: [\n      fc.integer().filter((n) => n % 2 !== 0), // odd numbers\n      fc.constantFrom(...extractPhrases(myAssertion)),\n    ],\n  },\n};\n\ndescribe('myAssertion', () => {\n  const { variants, params } = getVariants(testConfig);\n\n  for (const [variantName, variant] of variants) {\n    it(`should handle ${variantName} inputs`, async () => {\n      await runVariant(variant, {}, params, variantName, myAssertion);\n    });\n  }\n});\n```\n\n## Core Concepts\n\n### The Four Variants\n\nEvery bupkis assertion can be tested in four ways:\n\n| Variant          | Description                                  |\n| ---------------- | -------------------------------------------- |\n| `valid`          | Input that should pass the assertion         |\n| `invalid`        | Input that should fail the assertion         |\n| `validNegated`   | Input that should pass the negated assertion |\n| `invalidNegated` | Input that should fail the negated assertion |\n\nFor most assertions, `validNegated` defaults to `invalid` and `invalidNegated` defaults to `valid` (since negation inverts the logic). You only need to specify them explicitly when the negated behavior differs.\n\n### PropertyTestConfig\n\n```ts\ninterface PropertyTestConfig {\n  valid: PropertyTestConfigVariant;\n  invalid: PropertyTestConfigVariant;\n  validNegated?: PropertyTestConfigVariant; // defaults to invalid\n  invalidNegated?: PropertyTestConfigVariant; // defaults to valid\n  runSize?: 'small' | 'medium' | 'large'; // controls numRuns\n}\n```\n\n### Variant Types\n\nThere are several ways to define a variant:\n\n#### Generator Tuple (Most Common)\n\n```ts\n{\n  generators: [\n    fc.string(), // subject\n    fc.constantFrom('to be a string', 'to be str'), // phrase\n    // ... additional params for parametric assertions\n  ];\n}\n```\n\n#### Single Generator\n\n```ts\n{\n  generators: fc.tuple(fc.string(), fc.constantFrom('to be a string'));\n}\n```\n\n#### Async Generators\n\n```ts\n{\n  async: true,\n  generators: [\n    fc.constant(Promise.resolve('value')),\n    fc.constantFrom('to resolve to', 'to fulfill with'),\n    fc.string(),\n  ]\n}\n```\n\n#### Custom Property\n\n```ts\n{\n  property: () =>\n    fc.property(fc.string(), (s) => {\n      expect(s, 'to be a string');\n    });\n}\n```\n\n## API Reference\n\n### `createPropertyTestHarness(context)`\n\nCreates a property test harness with dependency-injected expect functions.\n\n```ts\nconst { runVariant } = createPropertyTestHarness({\n  expect: myExpect,\n  expectAsync: myExpectAsync,\n});\n```\n\n**Returns:**\n\n- `runVariant(variant, defaults, params, variantName, assertion)` - Runs a single variant test\n- Plus individual expectation helpers for advanced use cases\n\n### `expectUsing(assertion, args, options?)`\n\nDirectly executes a sync assertion, bypassing phrase matching. This is useful for:\n\n- Verifying that generated inputs actually work with the target assertion\n- Testing assertion logic independently of the phrase-matching system\n- Catching generator bugs that produce invalid inputs\n\n```ts\nimport {\n  expectUsing,\n  PropertyTestGeneratorError,\n} from '@bupkis/property-testing';\nimport { myAssertion } from './my-assertion.js';\n\n// Execute the assertion directly\nexpectUsing(myAssertion, [42, 'to be even']);\n\n// Test negated behavior\nexpectUsing(myAssertion, [43, 'to be even'], { negated: true });\n```\n\n**Throws:**\n\n- `PropertyTestGeneratorError` - If arguments don't parse for the assertion (generator bug)\n- `AssertionError` - If assertion fails (in non-negated mode)\n- `NegatedAssertionError` - If assertion passes (in negated mode)\n\n### `expectUsingAsync(assertion, args, options?)`\n\nAsync version of `expectUsing` for testing async assertions.\n\n```ts\nimport { expectUsingAsync } from '@bupkis/property-testing';\nimport { myAsyncAssertion } from './my-assertion.js';\n\nawait expectUsingAsync(myAsyncAssertion, [promise, 'to resolve to', 42]);\n```\n\n### `extractPhrases(assertion)`\n\nExtracts phrase literals from an assertion definition for use with `fc.constantFrom()`.\n\n```ts\nimport { myAssertion } from './my-assertion.js';\n\nconst phrases = extractPhrases(myAssertion);\n// e.g., ['to be even', 'to be an even number']\n\nconst phraseGen = fc.constantFrom(...phrases);\n```\n\n### `getVariants(config)`\n\nExtracts variants and parameters from a `PropertyTestConfig`, applying defaults for negated variants.\n\n```ts\nconst { variants, params } = getVariants(testConfig);\n\nfor (const [name, variant] of variants) {\n  // name: 'valid' | 'invalid' | 'validNegated' | 'invalidNegated'\n  // variant: PropertyTestConfigVariant\n}\n```\n\n### Utility Functions\n\n#### `filteredAnything`\n\nA `fc.anything()` generator that filters out objects with problematic keys (`__proto__`, `valueOf`, `toString`) and empty objects that could break Zod validation.\n\n```ts\n{\n  generators: [\n    filteredAnything.filter((v) => typeof v !== 'string'),\n    fc.constantFrom('to not be a string'),\n  ];\n}\n```\n\n#### `filteredObject`\n\nLike `filteredAnything` but only generates objects.\n\n#### `objectFilter(value)`\n\nThe filter function used by `filteredAnything` and `filteredObject`. Use it to filter your own generators:\n\n```ts\nfc.array(fc.anything()).filter(objectFilter);\n```\n\n#### `hasKeyDeep(obj, key)`\n\nRecursively searches for a key in a nested structure. Handles circular references.\n\n```ts\nhasKeyDeep({ a: { b: { c: 1 } } }, 'c'); // true\nhasKeyDeep({ a: { b: 1 } }, 'c'); // false\n```\n\n#### `hasValueDeep(obj, value)`\n\nRecursively searches for a value in a nested structure. Uses strict equality with special handling for empty objects.\n\n```ts\nhasValueDeep({ a: { b: 42 } }, 42); // true\nhasValueDeep({ a: { b: {} } }, {}); // true (empty objects match)\n```\n\n#### `safeRegexStringFilter(str)`\n\nRemoves regex metacharacters from a string. Useful when generating strings that will be used in regex patterns.\n\n```ts\nfc.string().map(safeRegexStringFilter);\n```\n\n#### `rejectPassing(expectFn)`\n\nBuilds a filter predicate that drops any `[subject, phrase, ...params]` tuple the assertion _accepts_. Apply it to invalid generators for subset-style assertions (`to satisfy`, `to be like`) where independently-generated subject/params pairs can coincidentally satisfy — and where `fast-check`'s shrinker will actively hunt for that overlap.\n\n```ts\nimport { expect } from 'bupkis';\nimport { rejectPassing } from '@bupkis/property-testing';\n\nconst invalidArgs = fc\n  .oneof(objectsThatUsuallyDoNotSatisfy, arraysThatUsuallyDoNotSatisfy)\n  .filter(rejectPassing(expect));\n```\n\nThrows a `TypeError` if `expectFn` returns a `PromiseLike` — `fc.Arbitrary.filter` is synchronous, so async expect functions are not supported.\n\nThis function does not catch rejections from any `PromiseLike` erroneously returned by `expectFn`.\n\n#### `calculateNumRuns(runSize?)`\n\nCalculates the number of test runs based on the environment:\n\n- **Wallaby**: 1/10th of base (fast feedback)\n- **CI**: 1/5th of base (balanced)\n- **Local**: Full base runs\n\n```ts\nconst numRuns = calculateNumRuns('medium'); // 250 locally, 50 in CI, 25 in Wallaby\n```\n\nRun sizes:\n\n- `small`: 50 base runs\n- `medium`: 250 base runs (default)\n- `large`: 500 base runs\n\n### Error Types\n\n#### `PropertyTestGeneratorError`\n\nThrown when `expectUsing` or `expectUsingAsync` receives arguments that don't parse for the assertion. This indicates a bug in your property generator—the generated inputs don't match the assertion's schema.\n\n```ts\nimport { PropertyTestGeneratorError } from '@bupkis/property-testing';\n\ntry {\n  expectUsing(numberAssertion, ['not a number', 'to be positive']);\n} catch (error) {\n  if (error instanceof PropertyTestGeneratorError) {\n    console.log(error.assertionId); // The assertion that rejected the input\n    console.log(error.args); // The invalid arguments\n  }\n}\n```\n\n#### `WrongAssertionError`\n\nThrown when testing `invalid` or `invalidNegated` variants and a different assertion than expected handles the error. This catches cases where your generator produces inputs that match a different assertion.\n\n```ts\nimport { WrongAssertionError } from '@bupkis/property-testing';\n\n// If testing stringAssertion but numberAssertion catches the error instead:\n// WrongAssertionError: Wrong assertion failed: expected 'string-assertion',\n// but 'number-assertion' failed instead.\n```\n\n## Assertion Applicability Registry\n\nFor testing compositional assertions (like `'and'` chains), the package provides an **applicability registry** system. This maps runtime values to assertions that would pass or fail for them, enabling data-first generation of valid/invalid assertion chains.\n\n### Core Concept\n\nInstead of generating random assertions and hoping they match random values, the registry lets you:\n\n1. Generate a diverse value\n2. Query which assertions would pass/fail for that value\n3. Build valid or invalid assertion chains accordingly\n\n### Creating a Registry\n\n```ts\nimport {\n  createApplicabilityRegistry,\n  type ApplicabilityAssertionMap,\n} from '@bupkis/property-testing';\nimport { assertions } from 'bupkis';\n\n// assertions object must have properties like stringAssertion, numberAssertion, etc.\nconst registry = createApplicabilityRegistry(\n  assertions as ApplicabilityAssertionMap,\n);\n```\n\nOr use the lazy-loaded default registry:\n\n```ts\nimport { getApplicabilityRegistry } from '@bupkis/property-testing';\n\nconst registry = await getApplicabilityRegistry();\n```\n\n### Querying the Registry\n\n```ts\nimport {\n  getApplicableAssertions,\n  getInapplicableAssertions,\n} from '@bupkis/property-testing';\n\nconst value = 42;\n\n// Get assertions that would PASS for this value\nconst applicable = getApplicableAssertions(value, registry);\n// e.g., [numberAssertion, integerAssertion, positiveAssertion, ...]\n\n// Get assertions that would FAIL for this value\nconst inapplicable = getInapplicableAssertions(value, registry);\n// e.g., [stringAssertion, booleanAssertion, nullAssertion, ...]\n```\n\n### Chain Generators\n\nFor testing `'and'` chains, use the built-in chain arbitraries:\n\n```ts\nimport {\n  diverseValueArbitrary,\n  validChainArbitrary,\n  invalidChainArbitrary,\n  validNegatedChainArbitrary,\n  invalidNegatedChainArbitrary,\n} from '@bupkis/property-testing';\n\n// Generate values covering many type categories\nconst valueGen = diverseValueArbitrary();\n\n// Generate valid 'and' chains (all assertions pass)\nconst validChainGen = validChainArbitrary(registry, { maxChainLength: 4 });\n\n// Generate invalid 'and' chains (at least one assertion fails)\nconst invalidChainGen = invalidChainArbitrary(registry);\n\n// For negated assertions\nconst validNegatedGen = validNegatedChainArbitrary(registry);\nconst invalidNegatedGen = invalidNegatedChainArbitrary(registry);\n```\n\nEach chain generator returns `ChainArgs`:\n\n```ts\ninterface ChainArgs {\n  args: readonly unknown[]; // [subject, phrase1, 'and', phrase2, ...]\n  chainLength: number; // Number of assertions in the chain\n  subject: unknown; // The generated subject value\n}\n\n// Use with expect:\nconst { args } = validChainGen.generate(fc.random(42)).value;\nexpect(...args);\n```\n\n### AssertionApplicability Interface\n\nEach registry entry has this shape:\n\n```ts\ninterface AssertionApplicability {\n  appliesTo: (value: unknown) => boolean; // Predicate for this assertion\n  assertion: AnySyncAssertion; // The assertion object\n  phrases: readonly [string, ...string[]]; // Phrase literals\n}\n```\n\n### Extending the Registry\n\nThe registry covers non-parametric sync-basic assertions. To add custom assertions:\n\n```ts\nimport { extractPhrases } from '@bupkis/property-testing';\n\nconst customEntries = [\n  {\n    appliesTo: (v) => typeof v === 'string' && v.startsWith('http'),\n    assertion: myUrlAssertion,\n    phrases: extractPhrases(myUrlAssertion),\n  },\n];\n\nconst extendedRegistry = [...registry, ...customEntries];\n```\n\n## Environment Variables\n\n- `WALLABY` - Set when running in [Wallaby.js][] (reduces runs by 10x)\n- `CI` - Set in CI environments (reduces runs by 5x)\n- `NUM_RUNS` - Override the number of runs directly\n\n## License\n\nCopyright © 2026 [Christopher \"boneskull\" Hiller][boneskull]. Licensed under [BlueOak-1.0.0](https://blueoakcouncil.org/license/1.0.0).\n\n[boneskull]: https://github.com/boneskull\n[Wallaby.js]: https://wallabyjs.com\n","readmeFilename":"README.md"}