{"_id":"@inixiative/transitions","_rev":"5-64bef7eef6b240af5c348373697e4f0e","name":"@inixiative/transitions","dist-tags":{"latest":"0.1.0"},"versions":{"0.0.1":{"name":"@inixiative/transitions","version":"0.0.1","keywords":["transition","state","lifecycle","guard","json-rules","rebac","typescript"],"author":{"name":"Aron Greenspan"},"license":"MIT","_id":"@inixiative/transitions@0.0.1","maintainers":[{"name":"inixiative","email":"aron.greenspan@inixiative.com"}],"homepage":"https://github.com/inixiative/transitions#readme","bugs":{"url":"https://github.com/inixiative/transitions/issues"},"dist":{"shasum":"122b6dd109704627ac45a726e2d4f75beb2b60da","tarball":"https://registry.npmjs.org/@inixiative/transitions/-/transitions-0.0.1.tgz","fileCount":9,"integrity":"sha512-fdcUYux4ZQyS+bvi+xAoYssUT2CQIJZvp2q9lQfWxihTwxnZhyQ6a4s3rXSh66XjUJ2vf4dQO1jb/MeZkYOQrg==","signatures":[{"sig":"MEUCIBK0W4Fgeo8weaaPtJBEvB4pWAcwnNxDr0Hd7lUnMyFZAiEApp0vQx+kpLveL64mFLo5KJPwK+HLjlQiYyf9AY2CnEQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":98775},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"3e3e4d98f87cf27da9c6f2bc9702a0a2ff382664","scripts":{"lint":"biome check .","test":"bun test","build":"tsup","check":"bun run typecheck && bun run lint","format":"biome format --write .","prepare":"lefthook install","typecheck":"tsc --noEmit","prepublishOnly":"bun run check && bun run test && bun run build"},"_npmUser":{"name":"inixiative","email":"aron.greenspan@inixiative.com"},"repository":{"url":"git+https://github.com/inixiative/transitions.git","type":"git"},"_npmVersion":"10.9.3","description":"Declarative, serializable transition guard + affordance layer on top of @inixiative/json-rules","directories":{},"_nodeVersion":"22.19.0","dependencies":{"lodash-es":"^4.17.21","@inixiative/json-rules":"^2.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","lefthook":"^2.1.2","@types/bun":"^1.3.10","typescript":"^5.0.0","@biomejs/biome":"^2.4.6","@types/lodash-es":"^4.17.12"},"peerDependencies":{"typescript":"^5.0.0"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/transitions_0.0.1_1782234923002_0.48158960733245326","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@inixiative/transitions","version":"0.0.2","keywords":["transition","state","lifecycle","guard","json-rules","rebac","typescript"],"author":{"name":"Aron Greenspan"},"license":"MIT","_id":"@inixiative/transitions@0.0.2","maintainers":[{"name":"inixiative","email":"aron.greenspan@inixiative.com"}],"homepage":"https://github.com/inixiative/transitions#readme","bugs":{"url":"https://github.com/inixiative/transitions/issues"},"dist":{"shasum":"71f229c7a40673e39f28f7a6c0b50d090af8918d","tarball":"https://registry.npmjs.org/@inixiative/transitions/-/transitions-0.0.2.tgz","fileCount":9,"integrity":"sha512-YUSQOa7QPezXA4fMPFStvvc4vIgFlcyvHOV27bC2CZpMX38jolQAKrnxEF2/hhZxsJ+qLl3NWF1FK2WLPkgX/w==","signatures":[{"sig":"MEQCIHDOK2O/iaEOuK4VS4tNTI6NbbuQULSNTlvssGGqLtjtAiB51dea11UJcYhpMcbegRPO5ATZ4TPqEk8XbXgaHrAGQg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":95369},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"3e3e4d98f87cf27da9c6f2bc9702a0a2ff382664","scripts":{"lint":"biome check .","test":"bun test","build":"tsup","check":"bun run typecheck && bun run lint","format":"biome format --write .","prepare":"lefthook install","typecheck":"tsc --noEmit","prepublishOnly":"bun run check && bun run test && bun run build"},"_npmUser":{"name":"inixiative","email":"aron.greenspan@inixiative.com"},"repository":{"url":"git+https://github.com/inixiative/transitions.git","type":"git"},"_npmVersion":"10.9.3","description":"Declarative, serializable transition guard + affordance layer on top of @inixiative/json-rules","directories":{},"_nodeVersion":"22.19.0","dependencies":{"lodash-es":"^4.17.21","@inixiative/json-rules":"^2.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","lefthook":"^2.1.2","@types/bun":"^1.3.10","typescript":"^5.0.0","@biomejs/biome":"^2.4.6","@types/lodash-es":"^4.17.12"},"peerDependencies":{"typescript":"^5.0.0"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/transitions_0.0.2_1782240383139_0.17461686941339227","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@inixiative/transitions","version":"0.0.3","keywords":["transition","state","lifecycle","guard","json-rules","rebac","typescript"],"author":{"name":"Aron Greenspan"},"license":"MIT","_id":"@inixiative/transitions@0.0.3","maintainers":[{"name":"inixiative","email":"aron.greenspan@inixiative.com"}],"homepage":"https://github.com/inixiative/transitions#readme","bugs":{"url":"https://github.com/inixiative/transitions/issues"},"dist":{"shasum":"71823215783576a5b970eeda5db9177ea8df34ec","tarball":"https://registry.npmjs.org/@inixiative/transitions/-/transitions-0.0.3.tgz","fileCount":9,"integrity":"sha512-p0ZGc/8I9N01EGd4H2IEsHwi5jQG49jrcGbITu2VZWMdhr3BqnBUdpni23kLrGfcTYN6o6SLUjpWWhxMj9Z2wg==","signatures":[{"sig":"MEYCIQDaNexDOVqHpPmcaUIDxpZljWEjzEMNhjm/qcYBvZwLeAIhAPVc8ArBWfrVtoSVUZZfQpbhzAwY8i+gOR1iIVTCiRdn","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":97341},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"781be9db938f1df5b3fe6ca62458718d5890421d","scripts":{"lint":"biome check .","test":"bun test","build":"tsup","check":"bun run typecheck && bun run lint","format":"biome format --write .","prepare":"lefthook install","typecheck":"tsc --noEmit","prepublishOnly":"bun run check && bun run test && bun run build"},"_npmUser":{"name":"inixiative","email":"aron.greenspan@inixiative.com"},"repository":{"url":"git+https://github.com/inixiative/transitions.git","type":"git"},"_npmVersion":"10.9.3","description":"Declarative, serializable transition guard + affordance layer on top of @inixiative/json-rules","directories":{},"_nodeVersion":"22.19.0","dependencies":{"lodash-es":"^4.17.21","@inixiative/json-rules":"^2.10.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","lefthook":"^2.1.2","@types/bun":"^1.3.10","typescript":"^5.0.0","@biomejs/biome":"^2.4.6","@types/lodash-es":"^4.17.12"},"peerDependencies":{"typescript":"^5.0.0"},"peerDependenciesMeta":{"typescript":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/transitions_0.0.3_1782740232969_0.9952559754625039","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@inixiative/transitions","version":"0.1.0","description":"Declarative, serializable transition guard + affordance layer on top of @inixiative/json-rules","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","type":"module","scripts":{"build":"tsup","test":"bun test","typecheck":"tsc --noEmit","lint":"biome check .","format":"biome format --write .","check":"bun run typecheck && bun run lint && bun run test","prepare":"lefthook install","prepublishOnly":"bun run check && bun run build"},"keywords":["transition","state","lifecycle","guard","json-rules","rebac","typescript"],"author":{"name":"Aron Greenspan"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/inixiative/transitions.git"},"publishConfig":{"access":"public"},"devDependencies":{"@biomejs/biome":"2.5.2","@types/bun":"1.3.14","@types/lodash-es":"^4.17.12","lefthook":"2.1.9","tsup":"8.5.1","typescript":"6.0.3","@inixiative/config":"^0.2.1"},"peerDependencies":{"typescript":"^5.0.0"},"peerDependenciesMeta":{"typescript":{"optional":true}},"dependencies":{"@inixiative/json-rules":"^2.13.0","@inixiative/permissions":"^0.3.1","lodash-es":"^4.18.1","zod":"^4.4.3"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"packageManager":"bun@1.3.14","_id":"@inixiative/transitions@0.1.0","gitHead":"414c28269985c81e166271de61abcee67c954786","bugs":{"url":"https://github.com/inixiative/transitions/issues"},"homepage":"https://github.com/inixiative/transitions#readme","_nodeVersion":"22.19.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-V74Ud4s4AEmomI/q0E64zWhAmMrCtbj2CmBQ8E7g5c9wWaAZgbo2EQSupoqmnaov7QEVk3UYYgAUxmzOgSq04Q==","shasum":"bb660874925e5fb48385ecd8644a4e9018081f95","tarball":"https://registry.npmjs.org/@inixiative/transitions/-/transitions-0.1.0.tgz","fileCount":9,"unpackedSize":94645,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCwj1J0qZMZBywMFveqeA0zN7MAsa6J5qOMaxW476dp/QIhAKoNoJIUGmL0ZR0ci0r8BrG2BDt1HZsOiJ7p/7vAw9JV"}]},"_npmUser":{"name":"aron.inixiative","email":"aron.greenspan@inixiative.com"},"directories":{},"maintainers":[{"name":"aron.inixiative","email":"aron.greenspan@inixiative.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/transitions_0.1.0_1783046293510_0.6438908976833209"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-23T17:15:22.897Z","modified":"2026-07-03T02:38:13.781Z","0.0.1":"2026-06-23T17:15:23.148Z","0.0.2":"2026-06-23T18:46:23.287Z","0.0.3":"2026-06-29T13:37:13.122Z","0.1.0":"2026-07-03T02:38:13.649Z"},"bugs":{"url":"https://github.com/inixiative/transitions/issues"},"author":{"name":"Aron Greenspan"},"license":"MIT","homepage":"https://github.com/inixiative/transitions#readme","keywords":["transition","state","lifecycle","guard","json-rules","rebac","typescript"],"repository":{"type":"git","url":"git+https://github.com/inixiative/transitions.git"},"description":"Declarative, serializable transition guard + affordance layer on top of @inixiative/json-rules","maintainers":[{"name":"aron.inixiative","email":"aron.greenspan@inixiative.com"}],"readme":"# @inixiative/transitions\n\nA small, **stateless** primitive on top of [`@inixiative/json-rules`](https://github.com/inixiative/json-rules)\nthat answers two questions about an entity's lifecycle, declaratively:\n\n- **\"Can this change happen?\"** — guard a proposed update (`checkTransition`).\n- **\"What changes can happen?\"** — list available actions for a record (`available`), and via\n  `toPrisma`, every record currently eligible for an action (`eligible`).\n\nIt is **not** a state machine (no statecharts/actors/hierarchy), does **not** own state, and does\n**not** execute the change or run side effects. It is a guard + affordance layer. A transition is\n**pure data** → serializable → tenant-configurable at runtime (stored in the DB, edited in a UI,\nno deploy to change a lifecycle).\n\n> Full design: [`docs/FEAT-018-transitions.md`](./docs/FEAT-018-transitions.md).\n\n## Model\n\n```\nresource → action → Action { paths: Transition[], label? }\n                             Transition { from: Side, to: Side & { merge? } }\n                             Side       { predicate, permission?, requires? }\n```\n\nA `resource` key is map-qualified (`map:Model`, e.g. `db:Inquiry`) — the same convention\n`@inixiative/permissions` uses, so a transition's `permission` can delegate into the rebac schema.\n\nA `Transition` is one atomic `from → to` edge. An **Action** (a named verb) is the OR of its edges —\ndisjunction lives at the action level, never inside a `Transition`, so the kernel stays atomic and\nserializable.\n\nThe `from`/`to` asymmetry is load-bearing and applies to **both** `predicate` (legality) and\n`permission` (authz): `from.*` is evaluated against the **current** record, `to.*` against the\n**resulting (merged)** record. Permission only ever lives on a side — every authz check reads a\nconcrete record, so there is no record-free action-level permission.\n\n## Check\n\n```ts\nimport { checkTransition } from '@inixiative/transitions';\n\nconst rules = {\n  'db:Inquiry': {\n    approve: {\n      paths: [\n        {\n          from: { predicate: { field: 'status', operator: 'equals', value: 'pending' } },\n          to: { predicate: { field: 'status', operator: 'equals', value: 'approved' } },\n        },\n      ],\n    },\n  },\n};\n\ncheckTransition(rules, 'db:Inquiry', 'approve', { status: 'pending' }, { status: 'approved' });\n// → true\n\ncheckTransition(rules, 'db:Inquiry', 'approve', { status: 'approved' }, { status: 'approved' });\n// → { paths: [{ from: { predicate: 'status must equal \"pending\"' } }] }\n```\n\n`checkTransition(rules, resource, action, record, changes, { actor, authorize })` returns **`true`** when\nallowed, else a structured **`Reason`** — never a bare bool:\n\n```ts\ntype SideReason = { predicate?: string; permission?: string }; // why one side failed\ntype PathReason = { from?: SideReason; to?: SideReason };       // why one candidate path failed\ntype Reason = { paths: PathReason[] };                          // one entry per path tried\n```\n\nEach side reports its `predicate` (legality) and `permission` (authz) failures independently, and\n`from`/`to` are kept separate — so the caller sees the whole picture, not just the first failure. To\nmap to HTTP: a path that fails purely on `permission` is a 403; any `predicate` failure is a 409.\n`describe(reason)` builds a human-readable message.\n\n`checkPath(transition, record, changes, options)` is the single-edge kernel underneath, returning\n`true | PathReason`; `checkTransition` walks an action's paths with it and returns the first `true`,\nelse every path's `PathReason`.\n\n## Permissions are injected (the seam)\n\nThe kernel never imports an authorization library. Per-side `permission` is a serializable\n`ActionRule` re-exported straight from [`@inixiative/permissions`](https://github.com/inixiative/permissions)\n(`string` delegation, `{ rel, action }`, `{ self }`, abac `{ rule }`, `any`/`all`, boolean terminals\n`true`/`false`, `null`), and you inject an `authorize` callback to evaluate it:\n\n```ts\nimport { checkTransition, createAuthorize } from '@inixiative/transitions';\n\nconst authorize = createAuthorize({ schema: rebacSchema })('db:Inquiry');\ncheckTransition(rules, 'db:Inquiry', 'approve', record, changes, { actor, authorize });\n```\n\nEffective authz ANDs the two side rules, each against the record it reads:\n\n```\nfrom.permission   AND   to.permission\n(current record)        (merged record)\n```\n\nAbsent = open; `null` (or `false`) = terminal deny. Omit `authorize` to check legality only.\n\n### `createAuthorize` — the `@inixiative/permissions` adapter\n\nAuthorization is **not** reimplemented here. `createAuthorize` is a thin adapter that bridges\n`@inixiative/permissions`' production rebac `check` onto the `Authorize` seam:\n`createAuthorize(options)(resource)` returns an `Authorize` bound to a (map-qualified) `resource`.\npermissions owns the whole evaluation — `string` delegation with **cycle detection**, intra-map `rel`\nwalks (via an injected `resolveRelation`), cross-map bridge walks, `{ self }`, abac `{ rule }`, boolean\nterminals, `any`/`all`, and per-row `permissionRules` overrides. It speaks the same map-qualified\n`resource` keys as this package.\n\n```ts\nimport type { RebacSchema } from '@inixiative/permissions';\nimport { createAuthorize } from '@inixiative/transitions';\n\nconst schema: RebacSchema = { permissions: { 'db:Inquiry': { actions: { /* … */ } } } };\nconst authorize = createAuthorize({\n  schema,\n  resolveRelation, // optional; default: the relation segment name is the resource key\n  isSuperadmin,    // optional; derived from the actor\n})('db:Inquiry');\n```\n\n`options`: `schema` (permissions' `{ bridges?, permissions }`), optional `resolveRelation`,\n`isSuperadmin`, and `data` (supplemental hydrated rows for bridge walks). A cyclic permission graph —\nwhich permissions surfaces by throwing — is caught and denied (fail closed), so the guard terminates\ncleanly.\n\n## Affordance + set query\n\n```ts\nimport { available, eligible } from '@inixiative/transitions';\n\navailable(rules, 'db:Inquiry', record, { actor, authorize });\n// → ['approve', 'reject', 'cancel']   (from-side only — `to` needs proposed changes, so it defers to checkTransition)\n\neligible(rules, 'db:Inquiry', 'approve');\n// → { OR: [{ status: { equals: 'pending' } }] }   (Prisma where for \"every record eligible for approve\")\n```\n\n## Merge strategies\n\n`to.merge` produces the resulting record. Keyword strategies are serializable; a callback is full-power\nbut not (`isSerializable(transition)` tells you which).\n\n- `spread` (default) — shallow overwrite\n- `deepMerge` — recursive, arrays replaced\n- `{ kind: 'append', path }` — concat at a field path\n- `{ kind: 'appendUnique', path }` — concat + dedupe\n\n## Authoring validation\n\n`validateTransition(t, { lens?, requireSerializable? })` validates predicates (via json-rules\n`validateRule`, plus `checkRuleAgainstLens` when a `lens` scopes referenceable fields), merge\nstrategy, and permission shape (via `@inixiative/permissions`' zod `actionRuleSchema`) — run it on\nsave before persisting a tenant config. It returns structured `{ ok, errors }` (never throws), even on\nmalformed input.\n\n## Not built yet (designed — see the plan)\n\n- **`requires` relation loader** — derive a Prisma `include` from a side's `requires`, auto-load, fail\n  loud on unloaded reads, respect tenancy. The kernel ignores `requires` (it's metadata).\n- **Permission-filtered `available()`** beyond the from-side static check.\n- **Lens-aware structural composition** check (does A's `to` lens line up with B's `from`).\n- **Non-goals:** statecharts, side effects / on-transition callbacks, a workflow engine, a config UI.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}