{"_id":"@issuegraph/store","_rev":"3-c70d143e7333b405e71f48615f245918","name":"@issuegraph/store","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@issuegraph/store","version":"0.1.0","keywords":["issuegraph","dependency-graph","backlog","scheduling","optimistic-updates","store"],"author":{"name":"Autonomy LLC"},"license":"Apache-2.0","_id":"@issuegraph/store@0.1.0","maintainers":[{"name":"timlayton","email":"laytontm@gmail.com"}],"homepage":"https://github.com/autnmy/issuegraph#readme","bugs":{"url":"https://github.com/autnmy/issuegraph/issues"},"dist":{"shasum":"3e20c233bd1a6eb86f3471db8d36580339b84541","tarball":"https://registry.npmjs.org/@issuegraph/store/-/store-0.1.0.tgz","fileCount":47,"integrity":"sha512-njQnjb/bG4SbJhxCVwjj4V1azHl1M2XdUECEHHGobTEBkRvCNfJ1GyDfDFM7v2lw5T/ic9mA4rbBbFzuXAO5uw==","signatures":[{"sig":"MEQCIDyrz3Jrit3oxCOz0f98KP241jALg4z+ozwkC/zxDZOcAiBt5rNmubfWWy+JcNhURSGANqa3fGPkCdrtzbb8HFD6UA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@issuegraph%2fstore@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":158873},"type":"module","_from":"file:issuegraph-store-0.1.0.tgz","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"test":"node --test --test-timeout=20000 \"src/**/*.test.ts\"","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.test.json"},"_npmUser":{"name":"timlayton","email":"laytontm@gmail.com"},"_resolved":"/tmp/b90c5fb0460244d3032dc053470b0fe1/issuegraph-store-0.1.0.tgz","_integrity":"sha512-njQnjb/bG4SbJhxCVwjj4V1azHl1M2XdUECEHHGobTEBkRvCNfJ1GyDfDFM7v2lw5T/ic9mA4rbBbFzuXAO5uw==","repository":{"url":"git+https://github.com/autnmy/issuegraph.git","type":"git","directory":"packages/store"},"_npmVersion":"11.17.0","description":"A framework-free client store for an Issuegraph document, plus the data-source port a host plugs its tracker into. Renders edits optimistically, dispatches every mutation outward, and never re-evaluates the selection order until the write lands.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@issuegraph/core":"^0.1.0"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^6.0.3","@types/node":"^22.18.0"},"_npmOperationalInternal":{"tmp":"tmp/store_0.1.0_1787530068173_0.48883449656645794","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@issuegraph/store","version":"0.1.1","keywords":["issuegraph","dependency-graph","backlog","scheduling","optimistic-updates","store"],"author":{"name":"Autonomy LLC"},"license":"Apache-2.0","_id":"@issuegraph/store@0.1.1","maintainers":[{"name":"timlayton","email":"laytontm@gmail.com"}],"homepage":"https://github.com/autnmy/issuegraph#readme","bugs":{"url":"https://github.com/autnmy/issuegraph/issues"},"dist":{"shasum":"f13d7105fe7fd3b2cae28df0fd6da3542b23a46b","tarball":"https://registry.npmjs.org/@issuegraph/store/-/store-0.1.1.tgz","fileCount":47,"integrity":"sha512-xcFJEfd4Mj1caFlEjKRY/+Mr32mBZAWQCVcIYscGKDOI5AFvy5bKlc+mXAKzGVIEto4MVEj2y+hWmZ9zphdssA==","signatures":[{"sig":"MEYCIQDqPwXQeSWxJ6y1QqyoBkrxEWNYCq2b0rb7B02bOXHiqwIhAKEo8i3SiIHIZVZaTnvYdF0MAPGEye/WsRtffaRcYL0+","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@issuegraph%2fstore@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":158621},"type":"module","_from":"file:issuegraph-store-0.1.1.tgz","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"test":"node --test --test-timeout=20000 \"src/**/*.test.ts\"","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.test.json"},"_npmUser":{"name":"timlayton","email":"laytontm@gmail.com"},"_resolved":"/tmp/854c222673f9b23041f65a2bc9405fbf/issuegraph-store-0.1.1.tgz","_integrity":"sha512-xcFJEfd4Mj1caFlEjKRY/+Mr32mBZAWQCVcIYscGKDOI5AFvy5bKlc+mXAKzGVIEto4MVEj2y+hWmZ9zphdssA==","repository":{"url":"git+https://github.com/autnmy/issuegraph.git","type":"git","directory":"packages/store"},"_npmVersion":"11.17.0","description":"A framework-free client store for an Issuegraph document, plus the data-source port a host plugs its tracker into. Renders edits optimistically, dispatches every mutation outward, and never re-evaluates the selection order until the write lands.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@issuegraph/core":"^0.1.2"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^6.0.3","@types/node":"^22.18.0"},"_npmOperationalInternal":{"tmp":"tmp/store_0.1.1_1788046703370_0.6129991661501104","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@issuegraph/store","version":"0.2.0","description":"A framework-free client store for an Issuegraph document, plus the data-source port a host plugs its tracker into. Renders edits optimistically, dispatches every mutation outward, and never re-evaluates the selection order until the write lands.","keywords":["issuegraph","dependency-graph","backlog","scheduling","optimistic-updates","store"],"license":"Apache-2.0","author":{"name":"Autonomy LLC"},"type":"module","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"sideEffects":false,"publishConfig":{"access":"public","provenance":true},"repository":{"type":"git","url":"git+https://github.com/autnmy/issuegraph.git","directory":"packages/store"},"homepage":"https://github.com/autnmy/issuegraph#readme","bugs":{"url":"https://github.com/autnmy/issuegraph/issues"},"engines":{"node":">=18"},"dependencies":{"@issuegraph/core":"^0.1.2"},"devDependencies":{"@types/node":"^22.18.0","typescript":"^6.0.3"},"scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.test.json","test":"node --test --test-timeout=20000 \"src/**/*.test.ts\""},"_id":"@issuegraph/store@0.2.0","_integrity":"sha512-P1A5207OVjbHLF9gBsW4hJFiRrYeE2aDEHKQQ0NGN/sG3dl2q/f+C1L9Mk5RMnnYg9ERZpWj7jcyXSQZD4HqjA==","_resolved":"/tmp/288f15fb3b93d8e7295d93589b41543b/issuegraph-store-0.2.0.tgz","_from":"file:issuegraph-store-0.2.0.tgz","_nodeVersion":"24.20.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-P1A5207OVjbHLF9gBsW4hJFiRrYeE2aDEHKQQ0NGN/sG3dl2q/f+C1L9Mk5RMnnYg9ERZpWj7jcyXSQZD4HqjA==","shasum":"35c6dfe087e9d188b6701e63d66a0de80dd1b2b2","tarball":"https://registry.npmjs.org/@issuegraph/store/-/store-0.2.0.tgz","fileCount":47,"unpackedSize":174065,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@issuegraph%2fstore@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD4RFUqz6FBLbWaJTq8ipJjGbj/OZt8Mj3YtcUs2FBhrAIgZJk9IOwKTWuKQ8FkkZq/vME8wNp6nC/XxS8vJ+LWWxc="}]},"_npmUser":{"name":"timlayton","email":"laytontm@gmail.com"},"directories":{},"maintainers":[{"name":"timlayton","email":"laytontm@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/store_0.2.0_1788538115040_0.5015849285855227"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-24T00:07:48.022Z","modified":"2026-09-04T16:08:35.637Z","0.1.0":"2026-08-24T00:07:48.339Z","0.1.1":"2026-08-29T23:38:23.500Z","0.2.0":"2026-09-04T16:08:35.234Z"},"bugs":{"url":"https://github.com/autnmy/issuegraph/issues"},"author":{"name":"Autonomy LLC"},"license":"Apache-2.0","homepage":"https://github.com/autnmy/issuegraph#readme","keywords":["issuegraph","dependency-graph","backlog","scheduling","optimistic-updates","store"],"repository":{"type":"git","url":"git+https://github.com/autnmy/issuegraph.git","directory":"packages/store"},"description":"A framework-free client store for an Issuegraph document, plus the data-source port a host plugs its tracker into. Renders edits optimistically, dispatches every mutation outward, and never re-evaluates the selection order until the write lands.","maintainers":[{"name":"timlayton","email":"laytontm@gmail.com"}],"readme":"# @issuegraph/store\n\nA framework-free client store for an [Issuegraph](https://github.com/autnmy/issuegraph)\ndocument, and the data-source port a host plugs its own tracker in through.\n\n**The package fetches nothing, authenticates nothing and persists nothing.** It holds the\ndocument, renders edits optimistically, dispatches every one of them outward, and refuses to\nre-evaluate the selection order until a write has actually landed.\n\n```\nnpm install @issuegraph/store\n```\n\nApache-2.0. `0.x`, and the API is unstable until `1.0` — a published package is a public\ncommitment, and this one is not making it yet.\n\n---\n\n## The five-minute version\n\n```ts\nimport { createMemorySource, createStore } from '@issuegraph/store';\n\nconst store = createStore({\n  source: createMemorySource({\n    issues: [\n      { ref: '1', title: 'Ship the thing', state: 'open' },\n      { ref: '2', title: 'Build the thing', state: 'open' },\n    ],\n    edges: [],\n  }),\n  derive: myOrderDeriver, // see \"The deriver\" below\n});\n\nawait store.hydrate();\n\nconst handle = store.propose({ op: 'create', kind: 'blocked-by', from: '1', to: '2' });\n// The edge is already drawn, marked `pending-write`.\n// The order has NOT moved, and reports `status: 'held'`.\n\nawait handle.settled;\n// Now the order has re-derived, and `snapshot.lastChange` says what moved.\n```\n\nRead state with `getSnapshot()` and react to it with `subscribe(listener)` — the pair\n`useSyncExternalStore` wants, so a React host needs no adapter and a non-React host is not\nasked to pretend it is one.\n\n**Every array reachable from a snapshot is frozen** — nested ones included, so a conflict\nrecord's `upstream.edges` and an edge's `states` are as protected as `landed`. They are the\nstore's own state, so a `.sort()` on one would reorder that state with no dispatch and no\nnotification; `[...rows].sort()` is the form to use. The store copies before freezing, so\nhanding it an array never freezes an adapter out of its own storage. The *elements* are left\nas you made them — the store owns which edges and rows it holds, you own what each one is.\n\n---\n\n## The data-source port\n\nOne interface, two methods. Everything else an adapter needs — a token, a retry policy, a\nrate limiter, a cache — stays on its own side of it.\n\n```ts\ninterface DataSource {\n  hydrate(): Promise<GraphDocument>;\n  dispatch(mutation: Mutation): Promise<DispatchResult>;\n}\n```\n\n`dispatch` answers with one of four outcomes:\n\n| outcome | what it means | what the store does |\n|---|---|---|\n| `applied` | the edit landed; carries the **whole** resulting document | adopts it, re-derives the order, emits a change summary |\n| `unchanged` | the edit changed nothing; carries the **whole** document | adopts it, re-derives, emits **no** summary |\n| `rejected` | the write was refused; carries a reason | marks the edge `failed` and offers `retry` |\n| `conflict` | the document moved upstream mid-edit; carries the current one | marks the edge `conflict` and holds **both** versions |\n\nThree contracts an adapter has to honour:\n\n- **`applied` and `unchanged` both carry the authoritative full document — issues included —\n  not a patch.** A partial answer would need merge rules, and merge rules are where an\n  optimistic store goes wrong. Both, because both are the same kind of claim about the\n  document; they differ only in whether *this* edit caused the difference, which is what\n  decides whether a change summary is emitted. `unchanged` matters more than it looks: an\n  adapter often has nothing to apply precisely *because* your store is out of date — another\n  client got there first — and a bare \"nothing to do\" would leave you without an edge that\n  genuinely exists. An adapter that changed no issues returns the ones it holds.\n- **The store dispatches once per mutation and never retries on its own.** A retry is a user\n  act, so nothing here assumes an idempotency you have not been given.\n- **The store runs one authoritative operation at a time**, queueing the rest, so `dispatch`\n  is never re-entered and never overlaps a `hydrate`. Both answer with an unversioned\n  authoritative document, and two of those in flight cannot be ordered by anything the store\n  can observe — whichever answers second wins, and if that is the older one it silently rolls\n  an edge back out. Rather than push a version onto every adapter, the store declines to\n  create the situation. A queued edit still renders `pending-write` immediately; only the\n  round trip waits, and `rehydrate()` resolves once it has had its turn.\n\nBecause of that last one, an edit reaches the adapter some time *after* it is proposed.\n`createScriptedSource` exposes `whenPending(mutationId?)` for exactly this — await the\nhand-off rather than guessing how many microtasks separate the two.\n\nTwo adapters ship with the package. `createMemorySource` holds a document in a variable and\napplies every edit — it is the reference implementation, and the one a demo runs on with no\ntracker at all. `createScriptedSource` settles only when told to, which is what lets a test\nobserve the store mid-flight and induce a rejection or a conflict on purpose; write your own\nadapter's tests against it.\n\n---\n\n## The deriver, and why you supply it\n\n```ts\ntype OrderDeriver = (document: GraphDocument) => readonly OrderRow[];\n```\n\nRequired, with no default. The selection order is its own concern with its own package, and a\ndefault here would be a second implementation of it — which is exactly the duplication this\npackage family exists to remove.\n\nThe division of labour: **the store owns *when* the order is recomputed; the deriver owns\n*what* it is.**\n\nAn optional `EdgeGuard` sits beside it for refusals that need to see the graph — a\n`blocked-by` that would close a cycle:\n\n```ts\ntype EdgeGuard = (context: { mutation; current; next }) => InvalidReason | undefined;\n```\n\n`current` is the **newest document the source has answered with**, which is the landed one\nexcept after a conflict: the conflict's `upstream` is newer than anything landed, so it is\nwhat the guard is shown until the next answer that lands replaces it. A verdict reached on\nthe pre-conflict document would admit an edit that closes a loop on the source's own document\n(the edge the upstream added, with the one queued behind it), and an adapter applies what it\nis handed. Only the guard reads the newer copy: the order and the drawn edges keep reading\n`landed`, and so do the store's own structural refusals — a duplicate or a vanished edge is a\nquestion the source answers itself, with a document the store adopts, so letting that\ndispatch through is what repairs the store's copy.\n\n---\n\n## The order can be trusted, structurally\n\nThe store keeps two edge sets:\n\n- **`landed`** — what the data source has confirmed. **The only input to the deriver.**\n- **`projected`** — `landed` plus every unsettled edit, each carrying its states. What a\n  viewer draws.\n\nOptimistic rendering is allowed; optimistic **re-ordering** is not. The rail's whole value is\nbeing what a picker will actually do, and an order reflecting an unlanded edit is a ranking\nthat exists nowhere. Keeping the two sets apart makes that a property of the structure rather\nthan a rule every reader has to remember.\n\nWhile any edit is in flight, `order.status` is `'held'` and the rows are the previous ones,\nunchanged — a stale-but-labelled order beats a half-computed one. Same principle if your\nderiver throws: the last order that *was* derived stands, and `orderError` says why it is\nstale.\n\n## Host callbacks that throw cannot break the store\n\nThree of them — the deriver, the guard, and a subscriber — and one rule for all three, because\nthey are all your code and the store already reads a throwing *adapter* as a rejection.\n\nA guard that throws reached **no verdict**, and an unknown verdict is not permission to write:\nthe edit is refused `invalid` with code `guard-failed`. A deriver that throws follows a write\nthat already landed, so it cannot become a failed write; the order goes stale with\n`orderError` set. A subscriber that throws is isolated and rethrown asynchronously, so it\nstill surfaces without taking the notification loop down.\n\nNone of them may escape into the dispatch queue, where they would strand every edit behind\nthem with the order held for ever.\n\n## Edge states are overlays, not variants\n\nAn edge always keeps its kind. State is carried beside it, as a list, because the five states\nare orthogonal and combine:\n\n`selected` · `pending-write` · `invalid` · `failed` · `conflict`\n\n`invalid` is **refused before any write** — the adapter is never called. Two things produce\nit, split by what the answer needs to see:\n\n- the store itself, for what is visible in the edit — a self-edge, a reference the document\n  does not hold, an exact duplicate, a retype to the kind the edge already has, a flip on a\n  symmetric relationship, and a second reference on a single-valued field (`cardinality`:\n  every relationship field but `blocked-by` holds one, §4.3 — the one writer rule of the\n  format that needs no graph walk, so it is refused here rather than once per host);\n- your `EdgeGuard`, for anything needing the graph.\n\n## A failed write is marked, never reverted\n\nThe user's work stays on the canvas. `retry` re-dispatches it, and on a conflict you get the\nresolutions below. **Nothing auto-merges, nothing auto-reverts, and nothing\ntimes out** — `discardMine` is the only call that removes an optimistic edit, and a person has\nto make it.\n\n### Resolving a conflict\n\nA conflict's `upstream` is **for display, and is never adopted** — it is the \"view diff\" half\nof the choice, and a reading of the past: a conflict can sit on screen for as long as a person\ntakes to read it. The one other thing it is used for is the guard: it is the newest document\nthe source has answered with, so an edit proposed or queued after the conflict is shown to\nyour `EdgeGuard` against it rather than against the copy the source has said it no longer\nholds (see \"The deriver\" above). Shown, not adopted — the order does not move for it, and a\n`retry` is still re-dispatched unchanged.\n\nThe store ships three resolutions:\n\n```ts\nstore.discardMine(id);     // drop the overlay; adopts nothing\nstore.retry(id);           // re-dispatch, unchanged, against the document as it stands\nstore.retryOnLatest(id);   // re-read the document, then re-dispatch, unchanged, against what it confirmed\n```\n\n`discardMine` and `retry` read the record and act on it with no `await` in between, so neither\ncan be overtaken. `retryOnLatest` has a read in the middle, and everything that can happen\nduring it — the read failing, the user discarding, the user pressing again — is a decision\nabout *user intent*. The store settles all three by **reserving the edit before the read**:\nthe record is `pending` from the call on, and a pending edit is one `discardMine` and a second\npress decline to touch, exactly as they do while any write is in flight. The read and the\nre-dispatch then run as **one queued operation**, so nothing can be queued between them:\n\n- the read **fails** — the record is restored exactly as it was, `upstream` and all, nothing is\n  dispatched, and `hydrationError` says why. The conflict is still there to try again;\n- the read **confirms** a document that no longer admits the edit — it is refused `invalid`\n  against that document and never dispatched;\n- otherwise the edit goes out against the confirmed document, with the guard judging it there.\n\nThat reservation is what the store used to lack, and why the composition once had to live in\nthe host — [issue #7](https://github.com/autnmy/issuegraph/issues/7) is the record of the three\ndefects a hand-rolled version drew. The adapter is the authority on the current document; the\nstore asks it rather than keeping a guess about how stale its own copy has become.\n\n`lastChange` belongs to the edit that is **current when it lands**. Two edits proposed before\nthe first settles means the first's summary would otherwise be written after the second began,\nand a host would show the previous edit's blast radius beside the current one.\n\n## Two smaller contracts worth knowing\n\n- **Issue references are opaque.** The format admits both `123` and `owner/repo#123`\n  (§4.2); normalising between them is the reader's job. This store compares references and\n  never parses one, so an adapter must emit canonical identifiers.\n- **Edge identity is derived, not assigned.** It is a pure function of kind and endpoints,\n  with the two symmetric kinds (§4.3.4, §4.3.7) sorting theirs — so `A serialize-with B` and\n  `B serialize-with A` are one edge. That is what lets the store recognise the edge your\n  adapter returns as the one it drew.\n\n---\n\n## What this package does not do\n\nRendering of any kind, including the wording of the change summary — the counts ship as\nnumbers so a host writes the sentence in its own language. Undo, multi-select batching, the\nfirst-pass review queue, audit findings and graph clustering all compose on top of it rather\nthan living in it.\n","readmeFilename":"README.md"}