{"_id":"@aliqubit/core","name":"@aliqubit/core","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aliqubit/core","version":"0.1.0","description":"Deterministic fault-injection harness that classifies the honest retry-safety of an external side effect.","type":"module","license":"MIT","engines":{"node":">=20"},"bin":{"faultline":"dist/cli/index.js"},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsc -p tsconfig.json","prepack":"npm run build","typecheck":"tsc -p tsconfig.check.json","test":"vitest run","cli":"tsx src/cli/index.ts"},"peerDependencies":{"tsx":"^4.19.2"},"peerDependenciesMeta":{"tsx":{"optional":true}},"devDependencies":{"@types/node":"^22.10.5","fast-check":"^3.23.2","tsx":"^4.19.2","typescript":"^5.7.3","vitest":"^2.1.8"},"author":{"name":"Aliqubit"},"repository":{"type":"git","url":"git+https://github.com/aliqubit/faultline.git","directory":"packages/core"},"homepage":"https://github.com/aliqubit/faultline#readme","bugs":{"url":"https://github.com/aliqubit/faultline/issues"},"keywords":["idempotency","retry","retry-safety","exactly-once","fault-injection","distributed-systems","reliability","reconciliation","side-effects","determinism"],"_id":"@aliqubit/core@0.1.0","gitHead":"306aab40f07eb7640c15757359f28f64ad2bf77e","_nodeVersion":"22.17.1","_npmVersion":"10.9.2","dist":{"integrity":"sha512-XNlNZrB8hD1j5RnR9btaTwYtesRYcNG+FjN1IE2RGPoQx723B8JpyzbzSXcahEsyfv1JoLvPkobhB6QLNPM3aw==","shasum":"a6bc9d2a9eee8862aec61b84793901ddfebca44f","tarball":"https://registry.npmjs.org/@aliqubit/core/-/core-0.1.0.tgz","fileCount":42,"unpackedSize":134854,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCApEjW0aLCP6eROq6G3SdncAVP0TDc8KlLCiAWZtOwEwIgDDGZ5buTCY7fJIdmuP5pw305hRUy8h+P0PFP/oEt124="}]},"_npmUser":{"name":"aliqubit","email":"pearljin.systems@gmail.com"},"directories":{},"maintainers":[{"name":"aliqubit","email":"pearljin.systems@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/core_0.1.0_1785794489168_0.4035880611555078"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-03T22:01:28.981Z","0.1.0":"2026-08-03T22:01:29.352Z","modified":"2026-08-03T22:01:29.623Z"},"maintainers":[{"name":"aliqubit","email":"pearljin.systems@gmail.com"}],"description":"Deterministic fault-injection harness that classifies the honest retry-safety of an external side effect.","homepage":"https://github.com/aliqubit/faultline#readme","keywords":["idempotency","retry","retry-safety","exactly-once","fault-injection","distributed-systems","reliability","reconciliation","side-effects","determinism"],"repository":{"type":"git","url":"git+https://github.com/aliqubit/faultline.git","directory":"packages/core"},"author":{"name":"Aliqubit"},"bugs":{"url":"https://github.com/aliqubit/faultline/issues"},"license":"MIT","readme":"# Faultline (`@aliqubit/core`)\n\n**Retries are easy. Knowing whether an external side effect is *safe* to retry is not.**\n\nFaultline is a local-first, deterministic fault-injection harness. You wrap one\nexternal action — charge a card, create a customer, POST a webhook, upsert a CRM\nrecord — and Faultline drives it through the ten ways a distributed call actually\nfails, then classifies its **honest retry-safety** into one of five classes. No\nnetwork, no wall-clock, no flakiness: every run is reproducible from a seed.\n\n> ### The one thing Faultline will never tell you\n> Faultline does **not** provide, and does not claim to provide, generic\n> **exactly-once execution**. Exactly-once external effects **cannot** be\n> guaranteed across arbitrary independent systems — the network can always lose\n> the acknowledgement *after* the effect has committed. Faultline's job is the\n> opposite of over-claiming: it tells you precisely *which* weaker, real\n> guarantee your action actually has, and what you'd need to add to strengthen\n> it. Every report prints an explicit `NOT GUARANTEED: Exactly-once execution\n> across the external service boundary.` line, and there is a test that fails the\n> build if that phrase ever leaks into an honest-guarantee slot.\n\n---\n\n## The five honest classes\n\n| Class | Meaning |\n|---|---|\n| `safe_retry` | Idempotent **and** concurrency-protected: repeating converges to one correct state with no duplicate effect. |\n| `reconcile_before_retry` | At-least-once with convergence — but only if you reconcile the unknown-completion state before retrying. |\n| `compensatable` | Duplicates/partials can happen, but a **tested** compensation restores an acceptable state. |\n| `irreversible` | The effect cannot be confirmed or reversed by this contract. Do not auto-retry; gate it. |\n| `unsafe` | Completion is undeterminable, state is neither reconcilable nor compensatable, or concurrency is uncontrolled. |\n\nA class is only ever awarded on **positive evidence**. Silence is never read as\nsafety: an unobservable conflict is treated as an unprotected one, not a\nprotected one.\n\n---\n\n## Five-minute setup\n\n```bash\ngit clone <this-repo> && cd faultline\nnpm install\nnpm run build          # compile src -> dist\nnpm test               # 32 tests (this package); 117 across the full workspace\n```\n\nWrite an action file that default-exports a campaign. You wire `execute` (and the\noptional `reconcile`/`compare`/`compensate` hooks) through a provider Faultline\ncontrols — the in-process `MockProvider`. Faultline never reads `execute`'s body;\nit drives the action through the ten failure timings against that **modeled**\nprovider. It does **not** inject faults into a live client (see\n[Limitations](#limitations)):\n\n```ts\n// action.ts — model your integration against the provider Faultline controls.\n// Faultline injects the ten faults against THIS in-process provider (not a live\n// one), so every run is deterministic and offline.\nimport { defineAction, defineCampaign, MockProvider } from \"@aliqubit/core\";\nimport type { MockRecord } from \"@aliqubit/core\";\n\nconst provider = new MockProvider({ supportsIdempotency: true, eventualConsistency: false });\n\nconst action = defineAction<{ customerId: string; email: string }, MockRecord, MockRecord>({\n  name: \"create-customer\",\n\n  // A stable, deterministic key — the single most important safety lever.\n  idempotencyKey: ({ input }) => input.customerId,\n  concurrencyKey: ({ input }) => input.customerId,\n\n  execute: ({ input, idempotencyKey }) =>\n    provider.create(input.customerId, { email: input.email }, idempotencyKey),\n\n  // How Faultline observes the world after an ambiguous completion:\n  reconcile: ({ input }) => provider.findByInternalId(input.customerId),\n  compare:   ({ intended, observed }) => observed !== null && observed.data.email === intended.email,\n  compensate: async ({ input }) => { await provider.deleteByInternalId(input.customerId); },\n});\n\nexport default defineCampaign({\n  action,\n  observer: provider, // the ground-truth ledger AND the fault injector\n  input:    { customerId: \"cust_123\", email: \"person@example.com\" },\n  // An intentionally *conflicting* variant, to probe concurrent-write safety:\n  altInput: { customerId: \"cust_123\", email: \"conflicting@example.com\" },\n});\n```\n\nRun it:\n\n```bash\nnpx faultline test ./action.ts\n```\n\n### One injected failure → one visual trace → one classification → one fix\n\nScenario 3 injects the failure that breaks naïve retry logic: **the downstream\ncommits, but the response is lost before the client ever learns.**\n\n```\n$ npx faultline test ./action.ts --scenario response-lost\n\nSCENARIO 3: Downstream commits but response is lost  [WARN]\n\nTIMELINE\nintent.created  (stripe.create-customer :: response-lost)\n  └─ reconciliation.started\n       └─ committed_state.found\n            └─ retry.suppressed  (already committed; retry avoided)\n                 └─ final_state.matched\n\nattempts=1  observed_effects=1  duplicate=false\nfinal_matches_intended=true\nreconciliation_required=true  reconciliation_succeeded=true\n\nHONEST GUARANTEE: At-least-once with convergence after reconciliation.\nNOT GUARANTEED:   Exactly-once execution across the external service boundary.\nREMEDIATION:      Reconcile before retrying after an unknown completion state.\n```\n\nAnd the campaign-level verdict:\n\n```\nCLASSIFICATION\nRECONCILE BEFORE RETRY\n\nEVIDENCE\n✓ Stable idempotency key supplied\n✓ Reconciliation resolves ambiguous completion\n✓ Same-key retry converges\n✗ No-key retry creates duplicate effects\n✓ Concurrent identical calls converge\n✗ Conflicting concurrent updates are not proven protected\n✓ Tested compensation restores acceptable state\n\nRECOMMENDED FIX\nRequire a deterministic idempotency key and run reconciliation before retrying\nafter an unknown completion state.\n```\n\nThat's the whole loop: an injected failure, a readable trace of what happened, an\nhonest class, and the specific next action. The `✗ Conflicting concurrent\nupdates` line is *why* this action is `reconcile_before_retry` and not\n`safe_retry` — two conflicting writers under the same key can still silently lose\nan intent, and Faultline refuses to pretend otherwise.\n\n---\n\n## Why `runOnce(key, fn)` is not enough\n\nThe most common \"solution\" is a dedupe wrapper:\n\n```ts\nasync function runOnce(key: string, fn: () => Promise<void>) {\n  if (await store.has(key)) return;   // (A) already did it?\n  await fn();                          // (B) the external effect\n  await store.markDone(key);          // (C) remember we did it\n}\n```\n\nThis looks airtight and is not. There is a window between **(B)** and **(C)**:\n\n- The process crashes **after `fn()` commits the external effect** but **before\n  `markDone` records success.**\n- On restart, `store.has(key)` is `false` — the record was never written — so\n  `runOnce` calls `fn()` **again**. The external effect happens **twice**.\n\n`runOnce` has merely moved the exactly-once problem from the external system into\n*your* store, and left the same crash window open across the two systems. There\nis no ordering of (B) and (C) that closes it: swap them and a crash between them\nmeans you *recorded* success for an effect that never happened, and now\nsuppress it forever.\n\nThere is no local wrapper that makes an arbitrary external effect exactly-once.\nWhat actually works is weaker and honest, and it is exactly what Faultline\nclassifies for:\n\n1. A **deterministic idempotency key** the *provider* honors, so re-executing\n   (B) is a no-op downstream — not a second effect.\n2. A **reconcile** step so that after an unknown completion you *ask the provider*\n   what really happened instead of guessing from your own store.\n3. For effects with no provider idempotency, a **tested compensation** so a\n   duplicate can be reversed to an acceptable state.\n\n`runOnce` gives you none of these. Faultline tells you which ones your action has.\n\n---\n\n## The ten mandatory failure scenarios\n\nEvery campaign runs all ten, each in full isolation (fresh ledger, fresh logical\nclock, its own timing seed):\n\n| # | Scenario | The question it answers |\n|---|---|---|\n| 1 | Process failure **before** request dispatch | Did anything happen? (No — the safe case.) |\n| 2 | Process failure **after** dispatch | Completion unknown — did the effect land? |\n| 3 | Downstream commits, **response lost** | The classic: committed, but the client never learned. |\n| 4 | Timeout **before** downstream completion | Timeout ≠ failure. Was it really not done? |\n| 5 | Timeout **after** downstream completion | Timeout ≠ failure. It *was* done. |\n| 6 | Duplicate retry, **same** idempotency key | Does the provider actually dedupe? |\n| 7 | Duplicate retry, **no** key | How bad is a naïve retry with no key? |\n| 8 | Two concurrent **identical** calls | Does the key hold under concurrency? |\n| 9 | Two concurrent **conflicting** updates | Is a losing writer silently dropped? |\n| 10 | Reconcile/compensation **itself fails** | What happens when the safety net is down? |\n\nScenario 10 is the honesty backstop: an action that looks `reconcile_before_retry`\ndegrades to `unsafe` if its reconcile endpoint is unavailable, and the report\nsays so.\n\n### Coverage: the commit×observe matrix\n\nThe failure model is a 2×3 matrix — *did the effect commit?* × *what did the\nclient learn?* Every reachable cell maps to a test; no cell is claimed but\nuntested.\n\n| **commit ↓ / observe →** | **LEARNED-SUCCESS** | **LEARNED-NOTHING** | **LEARNED-FAILURE** |\n|---|---|---|---|\n| **NOT COMMITTED** | *impossible* — no ack without a commit | `scn1` crash-before-dispatch, `scn2` crash-after-dispatch, `scn4` timeout-before → `scenario-coverage.test.ts` | provider-declined (`commit:false`) → `scenario-coverage.test.ts` \"throws provider_declined…\" |\n| **COMMITTED** | control → `scenario-coverage.test.ts` \"commits normally when commit=true\" | `scn3` response-lost, `scn5` timeout-after → `ambiguous-completion.test.ts` | **committed-then-error** (`errorAfterCommit`) → `scenario-coverage.test.ts` \"commits the effect AND throws…\" |\n\nThe retry/concurrency/recovery scenarios are an orthogonal axis, each with a test:\n\n| Scenario | Test |\n|---|---|\n| `scn6` duplicate-retry-same-key | `scenario-coverage.test.ts` \"scn6 duplicate-retry-same-key\" (+ no-idempotency variant) |\n| `scn7` duplicate-retry-no-key | `ambiguous-completion.test.ts` \"recovers a duplicate via tested compensation\" |\n| `scn8` concurrent-identical | `concurrency.test.ts` \"deduplicates two concurrent identical…\" (+ no-idempotency variant) |\n| `scn9` concurrent-conflicting | `concurrency.test.ts` \"detects a silently-lost intent…\" |\n| `scn10` safety-net-failure | `ambiguous-completion.test.ts` \"degrades to failure when the safety net…\" |\n\n**Zero cells remain claimed-but-untested.** The one previously-unmodeled cell,\n(COMMITTED, LEARNED-FAILURE), is now modeled via the `errorAfterCommit` directive\nand asserted (the effect commits *and* the client reads an error — the double-charge\ntrap).\n\n---\n\n## Ground truth vs. inference (the honesty boundary)\n\nFaultline ships a deterministic in-process `MockProvider` that implements an\n`EffectObserver` — it keeps a ledger of every committed effect. You wire your\naction's `execute`/`reconcile`/`compensate` through it, and Faultline injects the\nten faults against **that modeled provider**. Duplicate counts and conflict\noutcomes are then **ground truth** (`\"groundTruth\": true` in the report).\n\nFaultline does **not** inject faults into a live provider. The `AbortController`\nit passes to `execute` is never aborted by the harness, and a real client never\nreads the fault directive — so if your `execute` calls a live client, no faults\nfire and the verdict is not meaningful (the report is labeled\n`\"groundTruth\": false` and the CLI prints a warning). To build confidence against\nreal providers, use **recorded verification** (`@aliqubit/verify`), which replays\ncaptured real responses through your hooks. Live-provider fault injection is\nfuture work — see [Limitations](#limitations).\n\n---\n\n## Determinism\n\n- No wall-clock and no real sleeps: a `LogicalClock` advances by injected logical\n  delays only.\n- The seeded PRNG (`mulberry32`) varies **timing only** — never *which* fault\n  fires. Same seed ⇒ byte-identical JSON report.\n- `--repeat N` re-runs the campaign and fails (exit 3) if any run diverges. A\n  fixed-seed test pins byte-identical output; the fast-check property additionally\n  asserts a stable class (and no exactly-once leak) across 60 seeds.\n\n---\n\n## CLI\n\n```\nfaultline test <action-file.ts> [options]\n\n  --json <path>     write the full machine-readable JSON report\n  --scenario <id>   detail one scenario's timeline (the full suite still runs,\n                    so the headline classification stays honest)\n  --repeat <n>      run n times and verify byte-identical reproducibility\n  --seed <n>        seed the timing-only PRNG (default 1)\n```\n\nExit codes: `0` if the class is retry-safe with the documented discipline\n(`safe_retry`, `reconcile_before_retry`, `compensatable`); `1` for `irreversible`\nor `unsafe`; `2` on usage/load error; `3` on a reproducibility violation. Drop\n`faultline test ./action.ts` into CI to fail the build when an action's\nretry-safety regresses.\n\n---\n\n## Limitations\n\n- **Fault injection is against a modeled provider, not a live one.** Faultline\n  injects the ten faults only against the in-process `MockProvider` your action's\n  `execute` is wired through. The harness never aborts a live client, and a real\n  client never reads the fault directive, so a verdict is meaningful only in this\n  modeled mode. **Live-provider fault injection (aborting the real client at\n  injected boundaries) is future work.**\n- **Recorded verification is not fault injection.** `@aliqubit/verify` replays\n  *captured real responses* through your hooks to check they handle real provider\n  behavior; it does not inject faults.\n- The modeling assumptions (idempotency-key TTL, eventual-consistency lag,\n  conflict observability, etc.) are documented and **unverified against live\n  providers** until a `recorded`/`live-verified` pass is run.\n\n---\n\n## Scope\n\nThe **core** (`@aliqubit/core`) is one deep primitive: it classifies the\nretry-safety of a single external action, and nothing else. It is **not** a\nworkflow engine, a hosted dashboard, a CRM, a schema/DDL tool, an auth system, or\nan \"AI policy engine.\"\n\nOptional, *separate* packages build **on** the core — a contract registry, a CI\ngate, an OpenAPI scaffolder, a recorded-verification harness, and a fail-closed\nproduction executor + gateway. Each is opt-in; the core is complete on its own and\ndepends on none of them.\n\n## License\n\nMIT.\n","readmeFilename":"README.md","_rev":"1-2414f4444615804f1f89ab358b500c21"}