@fgv/ts-agent-memory 5.1.0-50 → 5.1.0-51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  2. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  3. package/dist/packlets/store/fileTreeMemoryStore.js +13 -24
  4. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  5. package/dist/packlets/store/memoryStore.js.map +1 -1
  6. package/dist/packlets/store/storeIdentity.js +62 -0
  7. package/dist/packlets/store/storeIdentity.js.map +1 -0
  8. package/dist/packlets/types/identityResolver.js +6 -0
  9. package/dist/packlets/types/identityResolver.js.map +1 -0
  10. package/dist/packlets/types/index.js +1 -0
  11. package/dist/packlets/types/index.js.map +1 -1
  12. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +42 -2
  13. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  14. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  15. package/dist/ts-agent-memory.d.ts +192 -9
  16. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
  17. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  18. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  19. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  20. package/lib/packlets/store/fileTreeMemoryStore.d.ts +4 -2
  21. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  22. package/lib/packlets/store/fileTreeMemoryStore.js +13 -24
  23. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  24. package/lib/packlets/store/memoryStore.d.ts +2 -2
  25. package/lib/packlets/store/memoryStore.d.ts.map +1 -1
  26. package/lib/packlets/store/memoryStore.js.map +1 -1
  27. package/lib/packlets/store/storeIdentity.d.ts +38 -0
  28. package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
  29. package/lib/packlets/store/storeIdentity.js +67 -0
  30. package/lib/packlets/store/storeIdentity.js.map +1 -0
  31. package/lib/packlets/types/identityResolver.d.ts +42 -0
  32. package/lib/packlets/types/identityResolver.d.ts.map +1 -0
  33. package/lib/packlets/types/identityResolver.js +7 -0
  34. package/lib/packlets/types/identityResolver.js.map +1 -0
  35. package/lib/packlets/types/index.d.ts +1 -0
  36. package/lib/packlets/types/index.d.ts.map +1 -1
  37. package/lib/packlets/types/index.js +1 -0
  38. package/lib/packlets/types/index.js.map +1 -1
  39. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +13 -2
  40. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  41. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +42 -2
  42. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  43. package/lib/packlets/vector/vectorIndex.d.ts +57 -6
  44. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  45. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  46. package/package.json +7 -7
@@ -1,5 +1,5 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
- import { DedupScope, EntityId, IMemoryRecord, IMemoryRecordResolver, Kind, MemoryId, MemoryScopeKey } from '../types';
2
+ import { DedupScope, EntityId, IMemoryRecord, IMemoryRecordResolver, Kind, MemoryId, MemoryScopeKey, IIdentityResolver } from '../types';
3
3
  import { IIndexedMemoryEntry } from '../index';
4
4
  import { IMemoryRecordSource, IScopedMemoryRecord } from '../vector';
5
5
  import { MemoryListSelection } from './listSelection';
@@ -15,7 +15,7 @@ import { DerivedArtifact, ReconcileReport } from './reconcile';
15
15
  * a consumer actually holds, not only for the concrete `FileTreeMemoryStore`.
16
16
  * @public
17
17
  */
18
- export interface IMemoryStore extends IMemoryRecordResolver {
18
+ export interface IMemoryStore extends IMemoryRecordResolver, IIdentityResolver {
19
19
  /**
20
20
  * Keyed read by entity id. Resolves `entityId` to a storage address via the
21
21
  * registered {@link IIdentityCodec} for `kind`. Returns `undefined` when no
@@ -1 +1 @@
1
- {"version":3,"file":"memoryStore.d.ts","sourceRoot":"","sources":["../../../src/packlets/store/memoryStore.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EACL,UAAU,EACV,QAAQ,EACR,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,QAAQ,EACR,cAAc,EACf,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAa,SAAQ,qBAAqB;IACzD;;;;;;;;OAQG;IACH,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAEzF;;OAEG;IACH,OAAO,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAElG;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAEnD;;;;;;;;;;;;;OAaG;IACH,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;IAEnE;;;;;;;OAOG;IACH,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;IAElE;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,cAAc,IAAI,mBAAmB,CAAC;IAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC;IAEtC;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;IAEhC;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC;IAEnF;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;CACnE"}
1
+ {"version":3,"file":"memoryStore.d.ts","sourceRoot":"","sources":["../../../src/packlets/store/memoryStore.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EACL,UAAU,EACV,QAAQ,EACR,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,QAAQ,EACR,cAAc,EACd,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAa,SAAQ,qBAAqB,EAAE,iBAAiB;IAC5E;;;;;;;;OAQG;IACH,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAEzF;;OAEG;IACH,OAAO,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAElG;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAEnD;;;;;;;;;;;;;OAaG;IACH,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;IAEnE;;;;;;;OAOG;IACH,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;IAElE;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,cAAc,IAAI,mBAAmB,CAAC;IAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC;IAEtC;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;IAEhC;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC;IAEnF;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;CACnE"}
@@ -1 +1 @@
1
- {"version":3,"file":"memoryStore.js","sourceRoot":"","sources":["../../../src/packlets/store/memoryStore.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result } from '@fgv/ts-utils';\nimport {\n DedupScope,\n EntityId,\n IMemoryRecord,\n IMemoryRecordResolver,\n Kind,\n MemoryId,\n MemoryScopeKey\n} from '../types';\nimport { IIndexedMemoryEntry } from '../index';\nimport { IMemoryRecordSource, IScopedMemoryRecord } from '../vector';\nimport { MemoryListSelection } from './listSelection';\nimport { IDerivedStateCoverage } from './coverage';\nimport { DerivedArtifact, ReconcileReport } from './reconcile';\n\n/**\n * The writable, FileTree-backed, content-hash-deduped memory store.\n *\n * @remarks\n * Extends {@link IMemoryRecordResolver} because the retrievers take a resolver\n * and a store is the obvious one to hand them — `{ index, resolver: store }` is\n * the documented wiring, and it has to type-check for the `IMemoryStore` handle\n * a consumer actually holds, not only for the concrete `FileTreeMemoryStore`.\n * @public\n */\nexport interface IMemoryStore extends IMemoryRecordResolver {\n /**\n * Keyed read by entity id. Resolves `entityId` to a storage address via the\n * registered {@link IIdentityCodec} for `kind`. Returns `undefined` when no\n * record exists. For a versioned (temporal) kind the current version is\n * *selected* from the derived in-memory index — the version history is walked\n * over envelopes, never bodies — and then that one version is read from\n * storage. So the selection costs no file reads and the result costs exactly\n * one, rather than one per version.\n */\n get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;\n\n /**\n * Direct read by `(scope, MemoryId)`. Returns `undefined` when not found.\n */\n getById(scope: MemoryScopeKey, id: MemoryId): Promise<Result<IMemoryRecord<unknown> | undefined>>;\n\n /**\n * List records: select over the derived index, then materialize the survivors.\n *\n * @remarks\n * **The selection is required and must narrow.** Since the index holds\n * envelopes only, every returned record is read from storage — so a call that\n * narrows nothing reads the whole vault, and that has to be a decision rather\n * than a default. Omitting the argument is a compile error; passing one with no\n * `scope` / `kind` / `tag` fails with a message naming {@link scanEveryRecord},\n * which is how a caller says it meant it.\n *\n * The requirement buys **explicitness, not a cost bound** — `{ kind }` on a\n * vault dominated by that kind still materializes most of it. What it prevents\n * is the whole-vault read nobody chose.\n *\n * If you only need to select, use {@link IMemoryStore.listEntries} instead: no\n * selection, no file reads.\n */\n list(selection: MemoryListSelection): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n\n /**\n * How much of the store's **derived state** exists, resolved by {@link Kind}.\n *\n * @remarks\n * Answers *\"is my derived state consistent with my records, and if not by how\n * much?\"* for every artifact the store derives — `rank`, record vectors, and\n * fragment vectors — in one call.\n *\n * **Cheap and total, by contract rather than by implementation.** Every input is\n * an envelope field or an index-side count: it reads **no record bodies** and\n * calls **no embedder**, and the walk over the vault's own state touches the\n * filesystem not at all. That is why it takes no selection, unlike\n * {@link IMemoryStore.list} — the guard there exists because an unnarrowed list\n * reads the vault, and putting one here would decorate a free operation and make\n * that guard mean less.\n *\n * **The index-side counts are the one exception, and it is the caller's own\n * index that spends it.** A persistent index answers `size` / `recordCount` /\n * `fragmentCount` with a query — `SqliteVecVectorIndex` runs a prepared `COUNT`\n * — so on a durable backend this call does I/O and can **fail**. It is bounded\n * (one count per wired index, never per record) and it is why the return is a\n * `Result` rather than a bare value.\n *\n * *If a future addition to the report would require reading a record body, it\n * does not belong on this report.*\n *\n * The counterpart is {@link IMemoryStore.reconcile}: coverage says **how big**\n * the gap is, cheaply; reconcile says **what it was** and closes what it can, at\n * the cost of re-running the embedder. Neither substitutes for the other.\n *\n * **A lane is reported here whenever its *index* is wired**, which is weaker\n * than what reconcile requires (index **and** embedder). That is intended: an\n * index without an embedder still holds vectors and still answers queries, so\n * its coverage is a real number worth reporting. The consequence to expect is a\n * half-wired store that reports a gap `reconcile` will refuse to close, naming\n * the missing embedder.\n */\n coverage(): Promise<Result<IDerivedStateCoverage>>;\n\n /**\n * Every entry in the vault — scope and envelope, **no bodies**. Reads no files\n * and requires no selection, because there is nothing to be careful about: it\n * returns what the index already holds.\n *\n * @remarks\n * This is the whole-vault read most callers actually want. Selection, grouping,\n * counting, \"which kinds are in here\", \"what links at this\" — all of it is\n * envelope work. Reach for {@link IMemoryStore.list} with\n * {@link scanEveryRecord} only when you genuinely need every body.\n *\n * Synchronous in spirit but `Promise`-returning for consistency with the rest\n * of the store surface.\n */\n listEntries(): Promise<Result<ReadonlyArray<IIndexedMemoryEntry>>>;\n\n /**\n * List EVERY record in the vault, each paired with its scope-qualified\n * `(scope, id)` address — the projection {@link IMemoryRecordSource} requires.\n * Unlike {@link IMemoryStore.list | list}, it takes no filter (whole-vault) and\n * returns {@link IScopedMemoryRecord}s so a re-index keys each entry on the same\n * scoped target the incremental embed-on-write path uses. Two records that share\n * a filename stem across scopes appear as distinct entries.\n */\n listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;\n\n /**\n * Adapt this store to the {@link IMemoryRecordSource} seam so it can drive\n * {@link IVectorIndex} rebuilds (e.g. `InMemoryCosineIndex.rebuild`). The\n * returned source's `list()` delegates to {@link IMemoryStore.listScoped},\n * **filtered to the kinds {@link IMemoryStore.embedsKind | embedsKind} reports** —\n * this source exists to feed the record vector index, so a kind excluded from\n * that index is excluded here too, and a reopen does not re-embed records the\n * index will never return. With no\n * {@link IFileTreeMemoryStoreCreateParams.embedKinds | embedKinds} declaration\n * every kind participates and the filter is the identity. `listScoped` itself is\n * **not** filtered and remains the whole-vault surface.\n *\n * The filter also **counts what it drops**, onto\n * {@link IMemoryRecordListing.excluded} — this is the layer where the exclusion\n * decision is made and so the only one that can. The count is always present\n * (empty when nothing was excluded).\n *\n * The store cannot implement {@link IMemoryRecordSource} directly because its\n * `list(filter?)` returns bare records (the ergonomic query surface) while the\n * seam's `list()` returns scope-qualified records.\n */\n asRecordSource(): IMemoryRecordSource;\n\n /**\n * The EFFECTIVE {@link DedupScope} for `kind` — the granularity at which a\n * write for this kind deduplicates against the existing vault.\n *\n * @remarks\n * This is a **read accessor over the store's already-injected write policies**,\n * and it is the single place any caller — the store's own write path included —\n * asks what a kind's dedup granularity is. It resolves the full chain the store\n * applies on write: the kind's registered {@link IWritePolicy}, falling back to\n * the store's default policy, then that policy's\n * {@link IWritePolicy.dedupScope | dedupScope}, falling back to\n * {@link DEFAULT_DEDUP_SCOPE}. Note the store's default policy is a\n * {@link KnowledgeLwwPolicy}, which declares `'content'` — so a kind with NO\n * registered policy resolves to `'content'`, not to `DEFAULT_DEDUP_SCOPE`.\n *\n * It exists so a caller that must agree with the store about dedup granularity\n * — notably the ingest orchestrator's stage-4 layer-1 exact match — can read the\n * declaration through this seam instead of being handed a second copy of the\n * policy map. A second declaration site is precisely the defect this accessor\n * was added to remove.\n *\n * Deliberately synchronous, total, and NOT `Result`-returning: it reads\n * constructor-injected configuration, touches no I/O, and cannot fail (every\n * link in the fallback chain has a total default). It exposes only the scope,\n * never the {@link IWritePolicy} itself, so it can never become a back door for\n * invoking admission or merge logic out of band.\n */\n dedupScopeFor(kind: Kind): DedupScope;\n\n /**\n * Whether records of `kind` participate in the **record-granular** vector index.\n *\n * @remarks\n * A read accessor over the store's injected {@link IFileTreeMemoryStoreCreateParams.embedKinds | embedKinds}\n * declaration, in the same spirit as {@link IMemoryStore.dedupScopeFor} — one\n * place to ask, so the store's write path and any caller reasoning about index\n * coverage cannot disagree. `true` for every kind when no declaration was made.\n *\n * **This is distinct from a {@link MemoryEmbedder} decline, and the difference is\n * cost.** An embedder that returns `undefined` has already been called: the\n * round trip is paid, and on a locally-hosted model that round trip is the\n * expense. A kind excluded here is never handed to the embedder at all. The\n * decline makes the intent *expressible*; this makes it *free*.\n *\n * Deliberately synchronous, total, and NOT `Result`-returning: it reads\n * constructor-injected configuration, touches no I/O, and cannot fail.\n */\n embedsKind(kind: Kind): boolean;\n\n /**\n * Write a record. Validates the body, computes a content hash, deduplicates\n * (scope-wide, before policy), applies the kind's {@link IWritePolicy}, stamps\n * transaction-time metadata (`created` / `updated` / `seq` / `contentHash`),\n * writes the file, and patches the index. Returns the written record — or the\n * existing record unchanged on a dedup no-op.\n */\n put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>>;\n\n /**\n * Repair one derived artifact for one {@link Kind} — **targeted and\n * non-destructive**, unlike `IVectorIndex.rebuild`, which resets the index and\n * re-embeds everything.\n *\n * @remarks\n * The counterpart to {@link IMemoryStore.coverage}: coverage says **how big**\n * the gap is and costs nothing; reconcile says **what it was** and closes what\n * it can, at the cost of reading bodies and re-running the embedder. Neither\n * substitutes for the other — in particular, only reconcile can distinguish a\n * *declined* record from a *failed* one, because learning that requires calling\n * the embedder again.\n *\n * **It only touches what is missing.** For the vector lanes it asks\n * `has(target)` per record and skips the ones already held, so a repair after a\n * brief outage costs a handful of embedder calls rather than a whole vault.\n * That check is also the only way to see a record whose vector the index holds\n * but whose envelope lost its `embeddingRef` — which needs a restamp and no\n * embedder call at all, and which an `embeddingRef`-only repair cannot detect.\n *\n * **`artifact` is required and names one lane.** See {@link DerivedArtifact}\n * for why an operation repairing \"everything wired\" would be the wrong shape.\n *\n * **A vector lane must be wired on BOTH halves — index *and* embedder — or this\n * fails, and it is deliberately stricter than {@link IMemoryStore.coverage}.**\n * The two ask different questions of the same wiring. Coverage asks *what does\n * the index hold*, which an index alone can answer: an index wired without an\n * embedder is a legal store (queries work; writes simply do not embed), so\n * coverage reports that lane rather than pretending it is absent. Reconcile\n * asks to *produce* vectors, which needs the embedder. So a half-wired store\n * legitimately reports a coverage gap it cannot repair, and reconcile names the\n * missing half rather than returning a cheerful success with every record in\n * `failed`.\n *\n * Runs under the store's write lock, like any other mutation.\n */\n reconcile(kind: Kind, artifact: DerivedArtifact): Promise<Result<ReconcileReport>>;\n\n /**\n * Delete a record by `(kind, entityId)`. Non-temporal kinds physically delete\n * the file and return the deleted record's {@link MemoryId}. Temporal\n * (versioned) kinds SOFT-delete: the current version is invalidated\n * (`invalid_at` set), history is retained, and the invalidated version's\n * {@link MemoryId} is returned.\n */\n delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>>;\n}\n"]}
1
+ {"version":3,"file":"memoryStore.js","sourceRoot":"","sources":["../../../src/packlets/store/memoryStore.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result } from '@fgv/ts-utils';\nimport {\n DedupScope,\n EntityId,\n IMemoryRecord,\n IMemoryRecordResolver,\n Kind,\n MemoryId,\n MemoryScopeKey,\n IIdentityResolver\n} from '../types';\nimport { IIndexedMemoryEntry } from '../index';\nimport { IMemoryRecordSource, IScopedMemoryRecord } from '../vector';\nimport { MemoryListSelection } from './listSelection';\nimport { IDerivedStateCoverage } from './coverage';\nimport { DerivedArtifact, ReconcileReport } from './reconcile';\n\n/**\n * The writable, FileTree-backed, content-hash-deduped memory store.\n *\n * @remarks\n * Extends {@link IMemoryRecordResolver} because the retrievers take a resolver\n * and a store is the obvious one to hand them — `{ index, resolver: store }` is\n * the documented wiring, and it has to type-check for the `IMemoryStore` handle\n * a consumer actually holds, not only for the concrete `FileTreeMemoryStore`.\n * @public\n */\nexport interface IMemoryStore extends IMemoryRecordResolver, IIdentityResolver {\n /**\n * Keyed read by entity id. Resolves `entityId` to a storage address via the\n * registered {@link IIdentityCodec} for `kind`. Returns `undefined` when no\n * record exists. For a versioned (temporal) kind the current version is\n * *selected* from the derived in-memory index — the version history is walked\n * over envelopes, never bodies — and then that one version is read from\n * storage. So the selection costs no file reads and the result costs exactly\n * one, rather than one per version.\n */\n get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;\n\n /**\n * Direct read by `(scope, MemoryId)`. Returns `undefined` when not found.\n */\n getById(scope: MemoryScopeKey, id: MemoryId): Promise<Result<IMemoryRecord<unknown> | undefined>>;\n\n /**\n * List records: select over the derived index, then materialize the survivors.\n *\n * @remarks\n * **The selection is required and must narrow.** Since the index holds\n * envelopes only, every returned record is read from storage — so a call that\n * narrows nothing reads the whole vault, and that has to be a decision rather\n * than a default. Omitting the argument is a compile error; passing one with no\n * `scope` / `kind` / `tag` fails with a message naming {@link scanEveryRecord},\n * which is how a caller says it meant it.\n *\n * The requirement buys **explicitness, not a cost bound** — `{ kind }` on a\n * vault dominated by that kind still materializes most of it. What it prevents\n * is the whole-vault read nobody chose.\n *\n * If you only need to select, use {@link IMemoryStore.listEntries} instead: no\n * selection, no file reads.\n */\n list(selection: MemoryListSelection): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n\n /**\n * How much of the store's **derived state** exists, resolved by {@link Kind}.\n *\n * @remarks\n * Answers *\"is my derived state consistent with my records, and if not by how\n * much?\"* for every artifact the store derives — `rank`, record vectors, and\n * fragment vectors — in one call.\n *\n * **Cheap and total, by contract rather than by implementation.** Every input is\n * an envelope field or an index-side count: it reads **no record bodies** and\n * calls **no embedder**, and the walk over the vault's own state touches the\n * filesystem not at all. That is why it takes no selection, unlike\n * {@link IMemoryStore.list} — the guard there exists because an unnarrowed list\n * reads the vault, and putting one here would decorate a free operation and make\n * that guard mean less.\n *\n * **The index-side counts are the one exception, and it is the caller's own\n * index that spends it.** A persistent index answers `size` / `recordCount` /\n * `fragmentCount` with a query — `SqliteVecVectorIndex` runs a prepared `COUNT`\n * — so on a durable backend this call does I/O and can **fail**. It is bounded\n * (one count per wired index, never per record) and it is why the return is a\n * `Result` rather than a bare value.\n *\n * *If a future addition to the report would require reading a record body, it\n * does not belong on this report.*\n *\n * The counterpart is {@link IMemoryStore.reconcile}: coverage says **how big**\n * the gap is, cheaply; reconcile says **what it was** and closes what it can, at\n * the cost of re-running the embedder. Neither substitutes for the other.\n *\n * **A lane is reported here whenever its *index* is wired**, which is weaker\n * than what reconcile requires (index **and** embedder). That is intended: an\n * index without an embedder still holds vectors and still answers queries, so\n * its coverage is a real number worth reporting. The consequence to expect is a\n * half-wired store that reports a gap `reconcile` will refuse to close, naming\n * the missing embedder.\n */\n coverage(): Promise<Result<IDerivedStateCoverage>>;\n\n /**\n * Every entry in the vault — scope and envelope, **no bodies**. Reads no files\n * and requires no selection, because there is nothing to be careful about: it\n * returns what the index already holds.\n *\n * @remarks\n * This is the whole-vault read most callers actually want. Selection, grouping,\n * counting, \"which kinds are in here\", \"what links at this\" — all of it is\n * envelope work. Reach for {@link IMemoryStore.list} with\n * {@link scanEveryRecord} only when you genuinely need every body.\n *\n * Synchronous in spirit but `Promise`-returning for consistency with the rest\n * of the store surface.\n */\n listEntries(): Promise<Result<ReadonlyArray<IIndexedMemoryEntry>>>;\n\n /**\n * List EVERY record in the vault, each paired with its scope-qualified\n * `(scope, id)` address — the projection {@link IMemoryRecordSource} requires.\n * Unlike {@link IMemoryStore.list | list}, it takes no filter (whole-vault) and\n * returns {@link IScopedMemoryRecord}s so a re-index keys each entry on the same\n * scoped target the incremental embed-on-write path uses. Two records that share\n * a filename stem across scopes appear as distinct entries.\n */\n listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;\n\n /**\n * Adapt this store to the {@link IMemoryRecordSource} seam so it can drive\n * {@link IVectorIndex} rebuilds (e.g. `InMemoryCosineIndex.rebuild`). The\n * returned source's `list()` delegates to {@link IMemoryStore.listScoped},\n * **filtered to the kinds {@link IMemoryStore.embedsKind | embedsKind} reports** —\n * this source exists to feed the record vector index, so a kind excluded from\n * that index is excluded here too, and a reopen does not re-embed records the\n * index will never return. With no\n * {@link IFileTreeMemoryStoreCreateParams.embedKinds | embedKinds} declaration\n * every kind participates and the filter is the identity. `listScoped` itself is\n * **not** filtered and remains the whole-vault surface.\n *\n * The filter also **counts what it drops**, onto\n * {@link IMemoryRecordListing.excluded} — this is the layer where the exclusion\n * decision is made and so the only one that can. The count is always present\n * (empty when nothing was excluded).\n *\n * The store cannot implement {@link IMemoryRecordSource} directly because its\n * `list(filter?)` returns bare records (the ergonomic query surface) while the\n * seam's `list()` returns scope-qualified records.\n */\n asRecordSource(): IMemoryRecordSource;\n\n /**\n * The EFFECTIVE {@link DedupScope} for `kind` — the granularity at which a\n * write for this kind deduplicates against the existing vault.\n *\n * @remarks\n * This is a **read accessor over the store's already-injected write policies**,\n * and it is the single place any caller — the store's own write path included —\n * asks what a kind's dedup granularity is. It resolves the full chain the store\n * applies on write: the kind's registered {@link IWritePolicy}, falling back to\n * the store's default policy, then that policy's\n * {@link IWritePolicy.dedupScope | dedupScope}, falling back to\n * {@link DEFAULT_DEDUP_SCOPE}. Note the store's default policy is a\n * {@link KnowledgeLwwPolicy}, which declares `'content'` — so a kind with NO\n * registered policy resolves to `'content'`, not to `DEFAULT_DEDUP_SCOPE`.\n *\n * It exists so a caller that must agree with the store about dedup granularity\n * — notably the ingest orchestrator's stage-4 layer-1 exact match — can read the\n * declaration through this seam instead of being handed a second copy of the\n * policy map. A second declaration site is precisely the defect this accessor\n * was added to remove.\n *\n * Deliberately synchronous, total, and NOT `Result`-returning: it reads\n * constructor-injected configuration, touches no I/O, and cannot fail (every\n * link in the fallback chain has a total default). It exposes only the scope,\n * never the {@link IWritePolicy} itself, so it can never become a back door for\n * invoking admission or merge logic out of band.\n */\n dedupScopeFor(kind: Kind): DedupScope;\n\n /**\n * Whether records of `kind` participate in the **record-granular** vector index.\n *\n * @remarks\n * A read accessor over the store's injected {@link IFileTreeMemoryStoreCreateParams.embedKinds | embedKinds}\n * declaration, in the same spirit as {@link IMemoryStore.dedupScopeFor} — one\n * place to ask, so the store's write path and any caller reasoning about index\n * coverage cannot disagree. `true` for every kind when no declaration was made.\n *\n * **This is distinct from a {@link MemoryEmbedder} decline, and the difference is\n * cost.** An embedder that returns `undefined` has already been called: the\n * round trip is paid, and on a locally-hosted model that round trip is the\n * expense. A kind excluded here is never handed to the embedder at all. The\n * decline makes the intent *expressible*; this makes it *free*.\n *\n * Deliberately synchronous, total, and NOT `Result`-returning: it reads\n * constructor-injected configuration, touches no I/O, and cannot fail.\n */\n embedsKind(kind: Kind): boolean;\n\n /**\n * Write a record. Validates the body, computes a content hash, deduplicates\n * (scope-wide, before policy), applies the kind's {@link IWritePolicy}, stamps\n * transaction-time metadata (`created` / `updated` / `seq` / `contentHash`),\n * writes the file, and patches the index. Returns the written record — or the\n * existing record unchanged on a dedup no-op.\n */\n put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>>;\n\n /**\n * Repair one derived artifact for one {@link Kind} — **targeted and\n * non-destructive**, unlike `IVectorIndex.rebuild`, which resets the index and\n * re-embeds everything.\n *\n * @remarks\n * The counterpart to {@link IMemoryStore.coverage}: coverage says **how big**\n * the gap is and costs nothing; reconcile says **what it was** and closes what\n * it can, at the cost of reading bodies and re-running the embedder. Neither\n * substitutes for the other — in particular, only reconcile can distinguish a\n * *declined* record from a *failed* one, because learning that requires calling\n * the embedder again.\n *\n * **It only touches what is missing.** For the vector lanes it asks\n * `has(target)` per record and skips the ones already held, so a repair after a\n * brief outage costs a handful of embedder calls rather than a whole vault.\n * That check is also the only way to see a record whose vector the index holds\n * but whose envelope lost its `embeddingRef` — which needs a restamp and no\n * embedder call at all, and which an `embeddingRef`-only repair cannot detect.\n *\n * **`artifact` is required and names one lane.** See {@link DerivedArtifact}\n * for why an operation repairing \"everything wired\" would be the wrong shape.\n *\n * **A vector lane must be wired on BOTH halves — index *and* embedder — or this\n * fails, and it is deliberately stricter than {@link IMemoryStore.coverage}.**\n * The two ask different questions of the same wiring. Coverage asks *what does\n * the index hold*, which an index alone can answer: an index wired without an\n * embedder is a legal store (queries work; writes simply do not embed), so\n * coverage reports that lane rather than pretending it is absent. Reconcile\n * asks to *produce* vectors, which needs the embedder. So a half-wired store\n * legitimately reports a coverage gap it cannot repair, and reconcile names the\n * missing half rather than returning a cheerful success with every record in\n * `failed`.\n *\n * Runs under the store's write lock, like any other mutation.\n */\n reconcile(kind: Kind, artifact: DerivedArtifact): Promise<Result<ReconcileReport>>;\n\n /**\n * Delete a record by `(kind, entityId)`. Non-temporal kinds physically delete\n * the file and return the deleted record's {@link MemoryId}. Temporal\n * (versioned) kinds SOFT-delete: the current version is invalidated\n * (`invalid_at` set), history is retained, and the invalidated version's\n * {@link MemoryId} is returned.\n */\n delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>>;\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import { FileTree } from '@fgv/ts-json-base';
2
+ import { Result } from '@fgv/ts-utils';
3
+ import { EntityId, IIdentityCodec, IIdentityCodecResult, IMemoryRecord, Kind, MemoryScopeKey } from '../types';
4
+ /**
5
+ * The identity codec registered for `kind`, or the default.
6
+ *
7
+ * @remarks
8
+ * Package-internal. Extracted from `FileTreeMemoryStore` rather than living on it
9
+ * because the file is at its `max-lines` cap — the fourth consecutive stream to pay
10
+ * that toll, which `TECH_DEBT.md` names as the trigger to promote the split to P1.
11
+ * @internal
12
+ */
13
+ export declare function codecFor(codecs: ReadonlyMap<Kind, IIdentityCodec>, defaultCodec: IIdentityCodec | undefined, kind: Kind): Result<IIdentityCodec>;
14
+ /**
15
+ * Resolve `(kind, entityId)` to the storage address the vault files it under,
16
+ * without reading the record.
17
+ *
18
+ * @remarks
19
+ * `kind` selects the codec and the codec computes the address, so this is a
20
+ * function rather than a search — which is what makes an `EntityId` that collides
21
+ * across kinds a non-issue instead of an ambiguity to disambiguate.
22
+ * @internal
23
+ */
24
+ export declare function resolveIdentity(codecs: ReadonlyMap<Kind, IIdentityCodec>, defaultCodec: IIdentityCodec | undefined, kind: Kind, entityId: EntityId): Result<IIdentityCodecResult>;
25
+ /**
26
+ * Cross-check a loaded record's declared identity against the address it was read
27
+ * from.
28
+ *
29
+ * @remarks
30
+ * The filename stem and the scope are the storage-side identity; the envelope's
31
+ * `id` / `entityId` are what downstream code trusts verbatim (merge-into
32
+ * re-addressing, for one). A tampered or corrupt file declaring a foreign
33
+ * `entityId` would otherwise load undetected, so the two are reconciled here
34
+ * through the codec's own round-trip.
35
+ * @internal
36
+ */
37
+ export declare function verifyLoadedIdentity(codec: Result<IIdentityCodec>, scope: MemoryScopeKey, file: FileTree.IFileTreeFileItem, record: IMemoryRecord<unknown>): Result<IMemoryRecord<unknown>>;
38
+ //# sourceMappingURL=storeIdentity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storeIdentity.d.ts","sourceRoot":"","sources":["../../../src/packlets/store/storeIdentity.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EACL,QAAQ,EACR,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,IAAI,EACJ,cAAc,EACf,MAAM,UAAU,CAAC;AAElB;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,cAAc,CAAC,EACzC,YAAY,EAAE,cAAc,GAAG,SAAS,EACxC,IAAI,EAAE,IAAI,GACT,MAAM,CAAC,cAAc,CAAC,CAMxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,cAAc,CAAC,EACzC,YAAY,EAAE,cAAc,GAAG,SAAS,EACxC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,GACjB,MAAM,CAAC,oBAAoB,CAAC,CAE9B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC,EAC7B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,QAAQ,CAAC,iBAAiB,EAChC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAC7B,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAiBhC"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.codecFor = codecFor;
8
+ exports.resolveIdentity = resolveIdentity;
9
+ exports.verifyLoadedIdentity = verifyLoadedIdentity;
10
+ const ts_utils_1 = require("@fgv/ts-utils");
11
+ /**
12
+ * The identity codec registered for `kind`, or the default.
13
+ *
14
+ * @remarks
15
+ * Package-internal. Extracted from `FileTreeMemoryStore` rather than living on it
16
+ * because the file is at its `max-lines` cap — the fourth consecutive stream to pay
17
+ * that toll, which `TECH_DEBT.md` names as the trigger to promote the split to P1.
18
+ * @internal
19
+ */
20
+ function codecFor(codecs, defaultCodec, kind) {
21
+ var _a;
22
+ const codec = (_a = codecs.get(kind)) !== null && _a !== void 0 ? _a : defaultCodec;
23
+ if (codec === undefined) {
24
+ return (0, ts_utils_1.fail)(`no identity codec registered for kind '${kind}'`);
25
+ }
26
+ return (0, ts_utils_1.succeed)(codec);
27
+ }
28
+ /**
29
+ * Resolve `(kind, entityId)` to the storage address the vault files it under,
30
+ * without reading the record.
31
+ *
32
+ * @remarks
33
+ * `kind` selects the codec and the codec computes the address, so this is a
34
+ * function rather than a search — which is what makes an `EntityId` that collides
35
+ * across kinds a non-issue instead of an ambiguity to disambiguate.
36
+ * @internal
37
+ */
38
+ function resolveIdentity(codecs, defaultCodec, kind, entityId) {
39
+ return codecFor(codecs, defaultCodec, kind).onSuccess((codec) => codec.encode(entityId));
40
+ }
41
+ /**
42
+ * Cross-check a loaded record's declared identity against the address it was read
43
+ * from.
44
+ *
45
+ * @remarks
46
+ * The filename stem and the scope are the storage-side identity; the envelope's
47
+ * `id` / `entityId` are what downstream code trusts verbatim (merge-into
48
+ * re-addressing, for one). A tampered or corrupt file declaring a foreign
49
+ * `entityId` would otherwise load undetected, so the two are reconciled here
50
+ * through the codec's own round-trip.
51
+ * @internal
52
+ */
53
+ function verifyLoadedIdentity(codec, scope, file, record) {
54
+ if (record.envelope.id !== file.baseName) {
55
+ return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`);
56
+ }
57
+ return codec
58
+ .onSuccess((c) => c.verifyRoundTrip(scope, file.baseName).onSuccess(() => c.decode(scope, file.baseName)))
59
+ .withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
60
+ .onSuccess((decodedEntityId) => {
61
+ if (decodedEntityId !== record.envelope.entityId) {
62
+ return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`);
63
+ }
64
+ return (0, ts_utils_1.succeed)(record);
65
+ });
66
+ }
67
+ //# sourceMappingURL=storeIdentity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storeIdentity.js","sourceRoot":"","sources":["../../../src/packlets/store/storeIdentity.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAsBH,4BAUC;AAYD,0CAOC;AAcD,oDAsBC;AApFD,4CAAsD;AAUtD;;;;;;;;GAQG;AACH,SAAgB,QAAQ,CACtB,MAAyC,EACzC,YAAwC,EACxC,IAAU;;IAEV,MAAM,KAAK,GAA+B,MAAA,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,YAAY,CAAC;IAC3E,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,IAAA,eAAI,EAAC,0CAA0C,IAAI,GAAG,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,IAAA,kBAAO,EAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,eAAe,CAC7B,MAAyC,EACzC,YAAwC,EACxC,IAAU,EACV,QAAkB;IAElB,OAAO,QAAQ,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,oBAAoB,CAClC,KAA6B,EAC7B,KAAqB,EACrB,IAAgC,EAChC,MAA8B;IAE9B,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzC,OAAO,IAAA,eAAI,EACT,gBAAgB,IAAI,CAAC,YAAY,mBAAmB,MAAM,CAAC,QAAQ,CAAC,EAAE,mCAAmC,IAAI,CAAC,QAAQ,GAAG,CAC1H,CAAC;IACJ,CAAC;IACD,OAAO,KAAK;SACT,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;SACzG,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,IAAI,CAAC,YAAY,MAAM,GAAG,EAAE,CAAC;SACtE,SAAS,CAAC,CAAC,eAAe,EAAE,EAAE;QAC7B,IAAI,eAAe,KAAK,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACjD,OAAO,IAAA,eAAI,EACT,gBAAgB,IAAI,CAAC,YAAY,yBAAyB,MAAM,CAAC,QAAQ,CAAC,QAAQ,4CAA4C,eAAe,GAAG,CACjJ,CAAC;QACJ,CAAC;QACD,OAAO,IAAA,kBAAO,EAAC,MAAM,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { FileTree } from '@fgv/ts-json-base';\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport {\n EntityId,\n IIdentityCodec,\n IIdentityCodecResult,\n IMemoryRecord,\n Kind,\n MemoryScopeKey\n} from '../types';\n\n/**\n * The identity codec registered for `kind`, or the default.\n *\n * @remarks\n * Package-internal. Extracted from `FileTreeMemoryStore` rather than living on it\n * because the file is at its `max-lines` cap — the fourth consecutive stream to pay\n * that toll, which `TECH_DEBT.md` names as the trigger to promote the split to P1.\n * @internal\n */\nexport function codecFor(\n codecs: ReadonlyMap<Kind, IIdentityCodec>,\n defaultCodec: IIdentityCodec | undefined,\n kind: Kind\n): Result<IIdentityCodec> {\n const codec: IIdentityCodec | undefined = codecs.get(kind) ?? defaultCodec;\n if (codec === undefined) {\n return fail(`no identity codec registered for kind '${kind}'`);\n }\n return succeed(codec);\n}\n\n/**\n * Resolve `(kind, entityId)` to the storage address the vault files it under,\n * without reading the record.\n *\n * @remarks\n * `kind` selects the codec and the codec computes the address, so this is a\n * function rather than a search — which is what makes an `EntityId` that collides\n * across kinds a non-issue instead of an ambiguity to disambiguate.\n * @internal\n */\nexport function resolveIdentity(\n codecs: ReadonlyMap<Kind, IIdentityCodec>,\n defaultCodec: IIdentityCodec | undefined,\n kind: Kind,\n entityId: EntityId\n): Result<IIdentityCodecResult> {\n return codecFor(codecs, defaultCodec, kind).onSuccess((codec) => codec.encode(entityId));\n}\n\n/**\n * Cross-check a loaded record's declared identity against the address it was read\n * from.\n *\n * @remarks\n * The filename stem and the scope are the storage-side identity; the envelope's\n * `id` / `entityId` are what downstream code trusts verbatim (merge-into\n * re-addressing, for one). A tampered or corrupt file declaring a foreign\n * `entityId` would otherwise load undetected, so the two are reconciled here\n * through the codec's own round-trip.\n * @internal\n */\nexport function verifyLoadedIdentity(\n codec: Result<IIdentityCodec>,\n scope: MemoryScopeKey,\n file: FileTree.IFileTreeFileItem,\n record: IMemoryRecord<unknown>\n): Result<IMemoryRecord<unknown>> {\n if (record.envelope.id !== file.baseName) {\n return fail(\n `memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`\n );\n }\n return codec\n .onSuccess((c) => c.verifyRoundTrip(scope, file.baseName).onSuccess(() => c.decode(scope, file.baseName)))\n .withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)\n .onSuccess((decodedEntityId) => {\n if (decodedEntityId !== record.envelope.entityId) {\n return fail(\n `memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`\n );\n }\n return succeed(record);\n });\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import { Result } from '@fgv/ts-utils';
2
+ import { IIdentityCodecResult } from './identityCodec';
3
+ import { EntityId, Kind } from './ids';
4
+ /**
5
+ * Resolves a consumer-facing `(kind, entityId)` address to the storage address the
6
+ * vault files it under, **without reading the record**.
7
+ *
8
+ * @remarks
9
+ * This is the resolution `IMemoryStore.get(kind, entityId)` already performs before
10
+ * it reads anything: `kind` selects the kind's `IIdentityCodec`, and the codec's
11
+ * `encode` computes `{ scope, idStem, isVersioned }`. Exposing it separately lets a
12
+ * caller that needs only the *address* — a fragment query narrowing to one record,
13
+ * say — avoid paying for a read it does not want.
14
+ *
15
+ * **The resolution is a function, not a search, and that is the load-bearing
16
+ * property.** A consumer holds an `EntityId`; a vector hit is addressed by a
17
+ * `(scope, id)` pair; and `EntityId` promises no uniqueness beyond a scope, so the
18
+ * same id may legitimately appear in several scopes (a document `acme-corp` under
19
+ * one kind and the entity `acme-corp` under another is the ordinary case, not a
20
+ * pathological one). Supplying `kind` selects one codec, and a codec cannot return
21
+ * two answers — so ambiguity is structurally impossible rather than merely unlikely,
22
+ * and no disambiguation pass is needed anywhere downstream.
23
+ *
24
+ * Narrow by design, mirroring `IMemoryRecordResolver`: a component that needs to
25
+ * turn an entity address into a storage address should depend on this rather than on
26
+ * the whole store.
27
+ * @public
28
+ */
29
+ export interface IIdentityResolver {
30
+ /**
31
+ * The storage address `(kind, entityId)` maps to, without reading the record.
32
+ *
33
+ * @param kind - Selects the identity codec. Required: it is what makes the
34
+ * resolution unambiguous.
35
+ * @param entityId - The consumer-supplied domain key.
36
+ * @returns `Success` with the codec's `{ scope, idStem, isVersioned }`, or
37
+ * `Failure` if no codec is registered for `kind` (and no default is wired) or the
38
+ * codec rejects the id. Both are caller errors and both are loud.
39
+ */
40
+ resolveIdentity(kind: Kind, entityId: EntityId): Result<IIdentityCodecResult>;
41
+ }
42
+ //# sourceMappingURL=identityResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identityResolver.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/identityResolver.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;OASG;IACH,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;CAC/E"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ //# sourceMappingURL=identityResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identityResolver.js","sourceRoot":"","sources":["../../../src/packlets/types/identityResolver.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result } from '@fgv/ts-utils';\nimport { IIdentityCodecResult } from './identityCodec';\nimport { EntityId, Kind } from './ids';\n\n/**\n * Resolves a consumer-facing `(kind, entityId)` address to the storage address the\n * vault files it under, **without reading the record**.\n *\n * @remarks\n * This is the resolution `IMemoryStore.get(kind, entityId)` already performs before\n * it reads anything: `kind` selects the kind's `IIdentityCodec`, and the codec's\n * `encode` computes `{ scope, idStem, isVersioned }`. Exposing it separately lets a\n * caller that needs only the *address* — a fragment query narrowing to one record,\n * say — avoid paying for a read it does not want.\n *\n * **The resolution is a function, not a search, and that is the load-bearing\n * property.** A consumer holds an `EntityId`; a vector hit is addressed by a\n * `(scope, id)` pair; and `EntityId` promises no uniqueness beyond a scope, so the\n * same id may legitimately appear in several scopes (a document `acme-corp` under\n * one kind and the entity `acme-corp` under another is the ordinary case, not a\n * pathological one). Supplying `kind` selects one codec, and a codec cannot return\n * two answers — so ambiguity is structurally impossible rather than merely unlikely,\n * and no disambiguation pass is needed anywhere downstream.\n *\n * Narrow by design, mirroring `IMemoryRecordResolver`: a component that needs to\n * turn an entity address into a storage address should depend on this rather than on\n * the whole store.\n * @public\n */\nexport interface IIdentityResolver {\n /**\n * The storage address `(kind, entityId)` maps to, without reading the record.\n *\n * @param kind - Selects the identity codec. Required: it is what makes the\n * resolution unambiguous.\n * @param entityId - The consumer-supplied domain key.\n * @returns `Success` with the codec's `{ scope, idStem, isVersioned }`, or\n * `Failure` if no codec is registered for `kind` (and no default is wired) or the\n * codec rejects the id. Both are caller errors and both are loud.\n */\n resolveIdentity(kind: Kind, entityId: EntityId): Result<IIdentityCodecResult>;\n}\n"]}
@@ -2,6 +2,7 @@ export * from './ids';
2
2
  export * from './envelope';
3
3
  export * from './filenameSafety';
4
4
  export * from './identityCodec';
5
+ export * from './identityResolver';
5
6
  export * from './recordResolver';
6
7
  export * from './temporal';
7
8
  export * from './writePolicy';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":"AAKA,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":"AAKA,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
@@ -22,6 +22,7 @@ __exportStar(require("./ids"), exports);
22
22
  __exportStar(require("./envelope"), exports);
23
23
  __exportStar(require("./filenameSafety"), exports);
24
24
  __exportStar(require("./identityCodec"), exports);
25
+ __exportStar(require("./identityResolver"), exports);
25
26
  __exportStar(require("./recordResolver"), exports);
26
27
  __exportStar(require("./temporal"), exports);
27
28
  __exportStar(require("./writePolicy"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;AAEH,wCAAsB;AACtB,6CAA2B;AAC3B,mDAAiC;AACjC,kDAAgC;AAChC,mDAAiC;AACjC,6CAA2B;AAC3B,gDAA8B","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './ids';\nexport * from './envelope';\nexport * from './filenameSafety';\nexport * from './identityCodec';\nexport * from './recordResolver';\nexport * from './temporal';\nexport * from './writePolicy';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;AAEH,wCAAsB;AACtB,6CAA2B;AAC3B,mDAAiC;AACjC,kDAAgC;AAChC,qDAAmC;AACnC,mDAAiC;AACjC,6CAA2B;AAC3B,gDAA8B","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './ids';\nexport * from './envelope';\nexport * from './filenameSafety';\nexport * from './identityCodec';\nexport * from './identityResolver';\nexport * from './recordResolver';\nexport * from './temporal';\nexport * from './writePolicy';\n"]}
@@ -1,6 +1,6 @@
1
1
  import { DetailedResult, Result } from '@fgv/ts-utils';
2
2
  import { IEdgeTarget } from '../types';
3
- import { FragmentEmbedder, IEmbeddedFragment, IFragmentVectorIndex, IFragmentVectorRebuildReport, IMemoryRecordSource, IVectorQueryHit, IVectorRebuildOptions } from './vectorIndex';
3
+ import { FragmentEmbedder, IEmbeddedFragment, IFragmentQueryOptions, IFragmentVectorIndex, IFragmentVectorRebuildReport, IMemoryRecordSource, IVectorQueryHit, IVectorRebuildOptions } from './vectorIndex';
4
4
  /**
5
5
  * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the
6
6
  * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many
@@ -50,7 +50,18 @@ export declare class InMemoryFragmentCosineIndex implements IFragmentVectorIndex
50
50
  /** {@inheritDoc IFragmentVectorIndex.remove} */
51
51
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
52
52
  /** {@inheritDoc IFragmentVectorIndex.query} */
53
- query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
53
+ query(vector: Float32Array, topK: number, options?: IFragmentQueryOptions): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
54
+ /**
55
+ * The records a query is allowed to score, honoring the `scope` / `id` narrowing.
56
+ *
57
+ * @remarks
58
+ * The single-record case is an O(1) map lookup rather than a scan, because the
59
+ * record map is keyed by `edgeTargetKey`. The scope-only case (a versioned kind's
60
+ * per-entity subtree) is a filtered walk — still bounded by the vault, but it
61
+ * scores only the entity's own fragments, which is what makes the caller's `topK`
62
+ * meaningful.
63
+ */
64
+ private _selectRecords;
54
65
  /**
55
66
  * Re-embed every record from `source` and rebuild the fragment index from
56
67
  * scratch. Clears the current contents (and the established dimension) first, so
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryFragmentCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,WAAW,EAAuB,MAAM,UAAU,CAAC;AAC5D,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,4BAA4B,EAE5B,mBAAmB,EAEnB,eAAe,EACf,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAqCvB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,2BAA4B,YAAW,oBAAoB;IACtE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,wFAAwF;IACxF,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,8EAA8E;IAC9E,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,uEAAuE;IACvE,IAAW,aAAa,IAAI,MAAM,CAMjC;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,2BAA2B,CAAC;IAI3D,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IA+C1B,6CAA6C;IACtC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAIzD,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAkDlD;;;;;;;;;;;;;;;;;;OAkBG;IACH,iDAAiD;IACpC,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,4BAA4B,EAAE,4BAA4B,CAAC,CAAC;IAuEtF,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
1
+ {"version":3,"file":"inMemoryFragmentCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,WAAW,EAAiD,MAAM,UAAU,CAAC;AACtF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,4BAA4B,EAE5B,mBAAmB,EAEnB,eAAe,EACf,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAqCvB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,2BAA4B,YAAW,oBAAoB;IACtE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,wFAAwF;IACxF,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,8EAA8E;IAC9E,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,uEAAuE;IACvE,IAAW,aAAa,IAAI,MAAM,CAMjC;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,2BAA2B,CAAC;IAI3D,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IA+C1B,6CAA6C;IACtC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAIzD,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAwDlD;;;;;;;;;OASG;IACH,OAAO,CAAE,cAAc;IAyBvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,iDAAiD;IACpC,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,4BAA4B,EAAE,4BAA4B,CAAC,CAAC;IAuEtF,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
@@ -110,8 +110,9 @@ class InMemoryFragmentCosineIndex {
110
110
  return Promise.resolve((0, ts_utils_1.succeed)(target));
111
111
  }
112
112
  /** {@inheritDoc IFragmentVectorIndex.query} */
113
- query(vector, topK, maxPerRecord) {
113
+ query(vector, topK, options) {
114
114
  var _a;
115
+ const maxPerRecord = options === null || options === void 0 ? void 0 : options.maxPerRecord;
115
116
  if (topK <= 0 || this._records.size === 0) {
116
117
  return Promise.resolve((0, ts_utils_1.succeed)([]));
117
118
  }
@@ -120,7 +121,12 @@ class InMemoryFragmentCosineIndex {
120
121
  }
121
122
  const queryMagnitude = InMemoryFragmentCosineIndex._magnitude(vector);
122
123
  const scored = [];
123
- for (const record of this._records.values()) {
124
+ // The narrowing is applied HERE — choosing which records are scored at all —
125
+ // rather than by filtering hits afterwards. That is the whole point: a
126
+ // post-filter would leave `topK` applied to the global set, so a scoped search
127
+ // would silently return fewer than `topK` whenever other records outscored the
128
+ // target's fragments.
129
+ for (const record of this._selectRecords(options)) {
124
130
  for (const fragment of record.fragments) {
125
131
  scored.push({
126
132
  key: (0, types_1.edgeTargetKey)(record.target),
@@ -151,6 +157,40 @@ class InMemoryFragmentCosineIndex {
151
157
  }
152
158
  return Promise.resolve((0, ts_utils_1.succeed)(hits));
153
159
  }
160
+ /**
161
+ * The records a query is allowed to score, honoring the `scope` / `id` narrowing.
162
+ *
163
+ * @remarks
164
+ * The single-record case is an O(1) map lookup rather than a scan, because the
165
+ * record map is keyed by `edgeTargetKey`. The scope-only case (a versioned kind's
166
+ * per-entity subtree) is a filtered walk — still bounded by the vault, but it
167
+ * scores only the entity's own fragments, which is what makes the caller's `topK`
168
+ * meaningful.
169
+ */
170
+ *_selectRecords(options) {
171
+ // Narrowed once, deliberately: re-deriving through `options?.` a second time
172
+ // after an early return that already implies `options !== undefined` creates an
173
+ // optional-chain arm that cannot fire, which is a dead branch rather than an
174
+ // untested one.
175
+ if (options === undefined || options.scope === undefined) {
176
+ yield* this._records.values();
177
+ return;
178
+ }
179
+ const scope = options.scope;
180
+ const id = options.id;
181
+ if (id !== undefined) {
182
+ const record = this._records.get((0, types_1.edgeTargetKey)({ scope, id }));
183
+ if (record !== undefined) {
184
+ yield record;
185
+ }
186
+ return;
187
+ }
188
+ for (const record of this._records.values()) {
189
+ if (record.target.scope === scope) {
190
+ yield record;
191
+ }
192
+ }
193
+ }
154
194
  /**
155
195
  * Re-embed every record from `source` and rebuild the fragment index from
156
196
  * scratch. Clears the current contents (and the established dimension) first, so
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryFragmentCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { DetailedResult, Result, fail, failWithDetail, succeed, succeedWithDetail } from '@fgv/ts-utils';\nimport { IEdgeTarget, Kind, edgeTargetKey } from '../types';\nimport {\n FragmentEmbedder,\n IEmbeddedFragment,\n IFragmentVectorIndex,\n IFragmentVectorRebuildReport,\n IMemoryRecordListing,\n IMemoryRecordSource,\n ISkippedVectorRecord,\n IVectorQueryHit,\n IVectorRebuildOptions\n} from './vectorIndex';\nimport { invokeHook, tally } from './rebuildHelpers';\n\n/**\n * The identity fields a fragment was added with, already in query-hit shape: a field\n * the fragment did not carry is *absent*, never present-but-`undefined`, so a hit\n * for a fragment added without a `fragmentId` is structurally identical to one\n * produced before `fragmentId` existed.\n */\ntype FragmentIdentity = Pick<IVectorQueryHit, 'locator' | 'fragmentId'>;\n\n/** Project an incoming fragment's identity fields, dropping the ones it did not carry. */\nfunction fragmentIdentity(fragment: IEmbeddedFragment): FragmentIdentity {\n return {\n ...(fragment.locator !== undefined ? { locator: fragment.locator } : {}),\n ...(fragment.fragmentId !== undefined ? { fragmentId: fragment.fragmentId } : {})\n };\n}\n\n/** One stored fragment: the identity it was added with plus its vector. */\ninterface IStoredFragment {\n readonly identity: FragmentIdentity;\n readonly vector: Float32Array;\n}\n\n/** Every stored fragment for one record, tagged with the record's scoped address. */\ninterface IStoredRecordFragments {\n readonly target: IEdgeTarget;\n readonly fragments: IStoredFragment[];\n}\n\n/** A candidate hit carried through selection: the fragment's key, hit, and score. */\ninterface IScoredFragment {\n readonly key: string;\n readonly hit: IVectorQueryHit;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the\n * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many\n * `Float32Array`s per record (one per {@link IEmbeddedFragment | fragment}) and\n * answers a query by computing cosine similarity against every stored fragment,\n * returning the top-k fragment hits by descending score. Each hit carries back\n * whichever of {@link IFragmentLocator | locator} / `fragmentId` its fragment was\n * added with; a fragment must carry at least one of the two.\n *\n * @remarks\n * Same regime and same non-goals as {@link InMemoryCosineIndex}: no external\n * dependency, no ANN structure, a linear scan over the stored fragments — the seam\n * ({@link IFragmentVectorIndex}) stays open for a consumer to swap a persistent /\n * ANN backend once N grows. `addFragments` is whole-record-replace, so re-authoring\n * a document never leaves stale fragments behind. The index has a single dimension\n * established by the first fragment added; every subsequent fragment and every\n * `query` vector must match it or fail loudly — a mismatched dimension is an\n * embedder-wiring bug, never a silent zero-similarity result.\n * {@link InMemoryFragmentCosineIndex.rebuild | rebuild} clears the index (and the\n * established dimension), so a re-embed with a different model is supported.\n *\n * The optional `maxPerRecord` cap on `query` is applied **during selection**, before\n * the `topK` cut, so one long document with many strong fragments cannot crowd every\n * other record out of the result.\n * @public\n */\nexport class InMemoryFragmentCosineIndex implements IFragmentVectorIndex {\n /**\n * Stored fragments keyed by the canonical {@link edgeTargetKey} of the record's\n * scope-qualified address, so two records that share a filename stem across\n * scopes occupy distinct entries and never overwrite each other's fragments.\n */\n private readonly _records: Map<string, IStoredRecordFragments>;\n /** The dimension of every stored fragment vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._records = new Map<string, IStoredRecordFragments>();\n this._dimension = undefined;\n }\n\n /** The number of records that currently have at least one stored fragment. */\n public get recordCount(): number {\n return this._records.size;\n }\n\n /** The total number of fragments currently held across all records. */\n public get fragmentCount(): number {\n let total: number = 0;\n for (const record of this._records.values()) {\n total += record.fragments.length;\n }\n return total;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryFragmentCosineIndex> {\n return succeed(new InMemoryFragmentCosineIndex());\n }\n\n /** {@inheritDoc IFragmentVectorIndex.addFragments} */\n public addFragments(\n target: IEdgeTarget,\n fragments: ReadonlyArray<IEmbeddedFragment>\n ): Promise<Result<number>> {\n const key: string = edgeTargetKey(target);\n // Validate every fragment before mutating any state, so a bad fragment never\n // leaves the record half-replaced OR the index dimension half-established\n // (whole-record-replace must be all-or-nothing). The effective dimension is the\n // established one, or — on a still-dimensionless index — the first fragment's\n // length; it is only committed to `this._dimension` once the whole batch passes.\n const stored: IStoredFragment[] = [];\n let dimension: number | undefined = this._dimension;\n for (const fragment of fragments) {\n if (fragment.vector.length === 0) {\n return Promise.resolve(fail(`fragment index: cannot add '${key}': empty fragment vector`));\n }\n // A fragment carrying neither identity cannot be resolved back to anything by a\n // consumer holding the hit — the same invariant `embeddedFragmentConverter`\n // enforces at the untyped boundary, re-checked here at the index seam.\n if (fragment.locator === undefined && fragment.fragmentId === undefined) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment requires at least one of 'locator' or 'fragmentId'`\n )\n );\n }\n if (dimension === undefined) {\n dimension = fragment.vector.length;\n } else if (fragment.vector.length !== dimension) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment dimension ${fragment.vector.length} does not match index dimension ${dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `addFragments`.\n stored.push({ identity: fragmentIdentity(fragment), vector: Float32Array.from(fragment.vector) });\n }\n // Whole-record replace: an empty `fragments` array drops the record entirely\n // rather than leaving an empty shell behind. Commit the (possibly newly-derived)\n // dimension only alongside a successful, non-empty store.\n if (stored.length === 0) {\n this._records.delete(key);\n } else {\n this._dimension = dimension;\n this._records.set(key, { target, fragments: stored });\n }\n return Promise.resolve(succeed(stored.length));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.has} */\n public has(target: IEdgeTarget): Promise<Result<boolean>> {\n return Promise.resolve(succeed(this._records.has(edgeTargetKey(target))));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._records.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.query} */\n public query(\n vector: Float32Array,\n topK: number,\n maxPerRecord?: number\n ): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._records.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `fragment index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryFragmentCosineIndex._magnitude(vector);\n const scored: IScoredFragment[] = [];\n for (const record of this._records.values()) {\n for (const fragment of record.fragments) {\n scored.push({\n key: edgeTargetKey(record.target),\n hit: {\n target: record.target,\n score: InMemoryFragmentCosineIndex._cosine(vector, queryMagnitude, fragment.vector),\n ...fragment.identity\n }\n });\n }\n }\n // Descending by score; the caller re-resolves each `(target, locator)` hit.\n scored.sort((a, b) => b.hit.score - a.hit.score);\n\n const hits: IVectorQueryHit[] = [];\n // Apply the per-record cap during selection (before the topK cut) so a single\n // long document cannot monopolize the result. `undefined` maxPerRecord means\n // uncapped; the counter map is always allocated (tiny) so the guard narrows\n // `maxPerRecord` directly without a non-null assertion.\n const perRecord: Map<string, number> = new Map<string, number>();\n for (const candidate of scored) {\n if (hits.length >= topK) {\n break;\n }\n if (maxPerRecord !== undefined) {\n const used: number = perRecord.get(candidate.key) ?? 0;\n if (used >= maxPerRecord) {\n continue;\n }\n perRecord.set(candidate.key, used + 1);\n }\n hits.push(candidate.hit);\n }\n return Promise.resolve(succeed(hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the fragment index from\n * scratch. Clears the current contents (and the established dimension) first, so\n * a re-embed with a different model is supported. Returns the total number of\n * fragments indexed.\n *\n * On any failure (list, embed, or add) the index is rolled back to empty rather\n * than left in a partially-rebuilt state.\n *\n * @remarks\n * **Deliberately still returns a bare count**, unlike the record-granular\n * {@link InMemoryCosineIndex.rebuild}, which reports an\n * {@link IVectorRebuildReport}. The asymmetry is scope, not oversight: the\n * fragment path is tracked separately and gains the same treatment when the\n * `IVectorIndex`/`IFragmentVectorIndex` contracts are revisited together.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The fragment embedder applied to each record.\n */\n /** {@inheritDoc IFragmentVectorIndex.rebuild} */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: FragmentEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n const listed: Result<IMemoryRecordListing> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE the reset, matching the record-granular sibling: a\n // failed list is no evidence about the fragments already held, and nothing\n // has been re-embedded yet, so there is no half-rebuilt state to guard\n // against. Discarding a healthy index over a transient read error is data\n // loss, not caution. No detail either — an all-zero report would describe\n // an index this call never touched.\n return failWithDetail(`fragment index rebuild: failed to list records: ${listed.message}`);\n }\n // From here a rebuild is genuinely starting, so clear. A mid-loop failure\n // under `'fail'` still resets, which is what keeps that contract honest.\n this._reset();\n const indexed: Map<Kind, number> = new Map<Kind, number>();\n const fragments: Map<Kind, number> = new Map<Kind, number>();\n const declined: Map<Kind, number> = new Map<Kind, number>();\n const skipped: ISkippedVectorRecord[] = [];\n // Only the source knows what it filtered, so an absent `excluded` propagates\n // as absent rather than becoming an empty map. Before this contract existed\n // the fragment path dropped this tally on the floor, having nowhere honest to\n // put it.\n const report = (): IFragmentVectorRebuildReport => ({\n indexed,\n fragments,\n declined,\n excluded: listed.value.excluded,\n skipped\n });\n for (const scoped of listed.value.records) {\n const kind: Kind = scoped.record.envelope.kind;\n // Consumer-supplied, so a throw or rejection is captured rather than\n // escaping mid-loop and leaving the index half-populated.\n const embedded: Result<ReadonlyArray<IEmbeddedFragment>> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `fragment index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // An empty array is this lane's decline. It still performs a real\n // whole-record-replace — which is what clears any stale fragments — so it\n // is written, then counted as declined rather than indexed.\n const added: Result<number> = await this.addFragments(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `fragment index rebuild: ${added.message}`;\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n if (added.value === 0) {\n tally(declined, kind);\n continue;\n }\n // Tallied per successful add rather than read back off the counts at the\n // end, so the per-kind buckets line up with their siblings.\n tally(indexed, kind);\n tally(fragments, kind, added.value);\n }\n return succeedWithDetail(report());\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._records.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored fragment vector. A zero-magnitude vector\n * on either side yields `0` rather than `NaN`.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryFragmentCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}
1
+ {"version":3,"file":"inMemoryFragmentCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { DetailedResult, Result, fail, failWithDetail, succeed, succeedWithDetail } from '@fgv/ts-utils';\nimport { IEdgeTarget, Kind, MemoryId, MemoryScopeKey, edgeTargetKey } from '../types';\nimport {\n FragmentEmbedder,\n IEmbeddedFragment,\n IFragmentQueryOptions,\n IFragmentVectorIndex,\n IFragmentVectorRebuildReport,\n IMemoryRecordListing,\n IMemoryRecordSource,\n ISkippedVectorRecord,\n IVectorQueryHit,\n IVectorRebuildOptions\n} from './vectorIndex';\nimport { invokeHook, tally } from './rebuildHelpers';\n\n/**\n * The identity fields a fragment was added with, already in query-hit shape: a field\n * the fragment did not carry is *absent*, never present-but-`undefined`, so a hit\n * for a fragment added without a `fragmentId` is structurally identical to one\n * produced before `fragmentId` existed.\n */\ntype FragmentIdentity = Pick<IVectorQueryHit, 'locator' | 'fragmentId'>;\n\n/** Project an incoming fragment's identity fields, dropping the ones it did not carry. */\nfunction fragmentIdentity(fragment: IEmbeddedFragment): FragmentIdentity {\n return {\n ...(fragment.locator !== undefined ? { locator: fragment.locator } : {}),\n ...(fragment.fragmentId !== undefined ? { fragmentId: fragment.fragmentId } : {})\n };\n}\n\n/** One stored fragment: the identity it was added with plus its vector. */\ninterface IStoredFragment {\n readonly identity: FragmentIdentity;\n readonly vector: Float32Array;\n}\n\n/** Every stored fragment for one record, tagged with the record's scoped address. */\ninterface IStoredRecordFragments {\n readonly target: IEdgeTarget;\n readonly fragments: IStoredFragment[];\n}\n\n/** A candidate hit carried through selection: the fragment's key, hit, and score. */\ninterface IScoredFragment {\n readonly key: string;\n readonly hit: IVectorQueryHit;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the\n * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many\n * `Float32Array`s per record (one per {@link IEmbeddedFragment | fragment}) and\n * answers a query by computing cosine similarity against every stored fragment,\n * returning the top-k fragment hits by descending score. Each hit carries back\n * whichever of {@link IFragmentLocator | locator} / `fragmentId` its fragment was\n * added with; a fragment must carry at least one of the two.\n *\n * @remarks\n * Same regime and same non-goals as {@link InMemoryCosineIndex}: no external\n * dependency, no ANN structure, a linear scan over the stored fragments — the seam\n * ({@link IFragmentVectorIndex}) stays open for a consumer to swap a persistent /\n * ANN backend once N grows. `addFragments` is whole-record-replace, so re-authoring\n * a document never leaves stale fragments behind. The index has a single dimension\n * established by the first fragment added; every subsequent fragment and every\n * `query` vector must match it or fail loudly — a mismatched dimension is an\n * embedder-wiring bug, never a silent zero-similarity result.\n * {@link InMemoryFragmentCosineIndex.rebuild | rebuild} clears the index (and the\n * established dimension), so a re-embed with a different model is supported.\n *\n * The optional `maxPerRecord` cap on `query` is applied **during selection**, before\n * the `topK` cut, so one long document with many strong fragments cannot crowd every\n * other record out of the result.\n * @public\n */\nexport class InMemoryFragmentCosineIndex implements IFragmentVectorIndex {\n /**\n * Stored fragments keyed by the canonical {@link edgeTargetKey} of the record's\n * scope-qualified address, so two records that share a filename stem across\n * scopes occupy distinct entries and never overwrite each other's fragments.\n */\n private readonly _records: Map<string, IStoredRecordFragments>;\n /** The dimension of every stored fragment vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._records = new Map<string, IStoredRecordFragments>();\n this._dimension = undefined;\n }\n\n /** The number of records that currently have at least one stored fragment. */\n public get recordCount(): number {\n return this._records.size;\n }\n\n /** The total number of fragments currently held across all records. */\n public get fragmentCount(): number {\n let total: number = 0;\n for (const record of this._records.values()) {\n total += record.fragments.length;\n }\n return total;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryFragmentCosineIndex> {\n return succeed(new InMemoryFragmentCosineIndex());\n }\n\n /** {@inheritDoc IFragmentVectorIndex.addFragments} */\n public addFragments(\n target: IEdgeTarget,\n fragments: ReadonlyArray<IEmbeddedFragment>\n ): Promise<Result<number>> {\n const key: string = edgeTargetKey(target);\n // Validate every fragment before mutating any state, so a bad fragment never\n // leaves the record half-replaced OR the index dimension half-established\n // (whole-record-replace must be all-or-nothing). The effective dimension is the\n // established one, or — on a still-dimensionless index — the first fragment's\n // length; it is only committed to `this._dimension` once the whole batch passes.\n const stored: IStoredFragment[] = [];\n let dimension: number | undefined = this._dimension;\n for (const fragment of fragments) {\n if (fragment.vector.length === 0) {\n return Promise.resolve(fail(`fragment index: cannot add '${key}': empty fragment vector`));\n }\n // A fragment carrying neither identity cannot be resolved back to anything by a\n // consumer holding the hit — the same invariant `embeddedFragmentConverter`\n // enforces at the untyped boundary, re-checked here at the index seam.\n if (fragment.locator === undefined && fragment.fragmentId === undefined) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment requires at least one of 'locator' or 'fragmentId'`\n )\n );\n }\n if (dimension === undefined) {\n dimension = fragment.vector.length;\n } else if (fragment.vector.length !== dimension) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment dimension ${fragment.vector.length} does not match index dimension ${dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `addFragments`.\n stored.push({ identity: fragmentIdentity(fragment), vector: Float32Array.from(fragment.vector) });\n }\n // Whole-record replace: an empty `fragments` array drops the record entirely\n // rather than leaving an empty shell behind. Commit the (possibly newly-derived)\n // dimension only alongside a successful, non-empty store.\n if (stored.length === 0) {\n this._records.delete(key);\n } else {\n this._dimension = dimension;\n this._records.set(key, { target, fragments: stored });\n }\n return Promise.resolve(succeed(stored.length));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.has} */\n public has(target: IEdgeTarget): Promise<Result<boolean>> {\n return Promise.resolve(succeed(this._records.has(edgeTargetKey(target))));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._records.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.query} */\n public query(\n vector: Float32Array,\n topK: number,\n options?: IFragmentQueryOptions\n ): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n const maxPerRecord: number | undefined = options?.maxPerRecord;\n if (topK <= 0 || this._records.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `fragment index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryFragmentCosineIndex._magnitude(vector);\n const scored: IScoredFragment[] = [];\n // The narrowing is applied HERE — choosing which records are scored at all —\n // rather than by filtering hits afterwards. That is the whole point: a\n // post-filter would leave `topK` applied to the global set, so a scoped search\n // would silently return fewer than `topK` whenever other records outscored the\n // target's fragments.\n for (const record of this._selectRecords(options)) {\n for (const fragment of record.fragments) {\n scored.push({\n key: edgeTargetKey(record.target),\n hit: {\n target: record.target,\n score: InMemoryFragmentCosineIndex._cosine(vector, queryMagnitude, fragment.vector),\n ...fragment.identity\n }\n });\n }\n }\n // Descending by score; the caller re-resolves each `(target, locator)` hit.\n scored.sort((a, b) => b.hit.score - a.hit.score);\n\n const hits: IVectorQueryHit[] = [];\n // Apply the per-record cap during selection (before the topK cut) so a single\n // long document cannot monopolize the result. `undefined` maxPerRecord means\n // uncapped; the counter map is always allocated (tiny) so the guard narrows\n // `maxPerRecord` directly without a non-null assertion.\n const perRecord: Map<string, number> = new Map<string, number>();\n for (const candidate of scored) {\n if (hits.length >= topK) {\n break;\n }\n if (maxPerRecord !== undefined) {\n const used: number = perRecord.get(candidate.key) ?? 0;\n if (used >= maxPerRecord) {\n continue;\n }\n perRecord.set(candidate.key, used + 1);\n }\n hits.push(candidate.hit);\n }\n return Promise.resolve(succeed(hits));\n }\n\n /**\n * The records a query is allowed to score, honoring the `scope` / `id` narrowing.\n *\n * @remarks\n * The single-record case is an O(1) map lookup rather than a scan, because the\n * record map is keyed by `edgeTargetKey`. The scope-only case (a versioned kind's\n * per-entity subtree) is a filtered walk — still bounded by the vault, but it\n * scores only the entity's own fragments, which is what makes the caller's `topK`\n * meaningful.\n */\n private *_selectRecords(options: IFragmentQueryOptions | undefined): Generator<IStoredRecordFragments> {\n // Narrowed once, deliberately: re-deriving through `options?.` a second time\n // after an early return that already implies `options !== undefined` creates an\n // optional-chain arm that cannot fire, which is a dead branch rather than an\n // untested one.\n if (options === undefined || options.scope === undefined) {\n yield* this._records.values();\n return;\n }\n const scope: MemoryScopeKey = options.scope;\n const id: MemoryId | undefined = options.id;\n if (id !== undefined) {\n const record: IStoredRecordFragments | undefined = this._records.get(edgeTargetKey({ scope, id }));\n if (record !== undefined) {\n yield record;\n }\n return;\n }\n for (const record of this._records.values()) {\n if (record.target.scope === scope) {\n yield record;\n }\n }\n }\n\n /**\n * Re-embed every record from `source` and rebuild the fragment index from\n * scratch. Clears the current contents (and the established dimension) first, so\n * a re-embed with a different model is supported. Returns the total number of\n * fragments indexed.\n *\n * On any failure (list, embed, or add) the index is rolled back to empty rather\n * than left in a partially-rebuilt state.\n *\n * @remarks\n * **Deliberately still returns a bare count**, unlike the record-granular\n * {@link InMemoryCosineIndex.rebuild}, which reports an\n * {@link IVectorRebuildReport}. The asymmetry is scope, not oversight: the\n * fragment path is tracked separately and gains the same treatment when the\n * `IVectorIndex`/`IFragmentVectorIndex` contracts are revisited together.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The fragment embedder applied to each record.\n */\n /** {@inheritDoc IFragmentVectorIndex.rebuild} */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: FragmentEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n const listed: Result<IMemoryRecordListing> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE the reset, matching the record-granular sibling: a\n // failed list is no evidence about the fragments already held, and nothing\n // has been re-embedded yet, so there is no half-rebuilt state to guard\n // against. Discarding a healthy index over a transient read error is data\n // loss, not caution. No detail either — an all-zero report would describe\n // an index this call never touched.\n return failWithDetail(`fragment index rebuild: failed to list records: ${listed.message}`);\n }\n // From here a rebuild is genuinely starting, so clear. A mid-loop failure\n // under `'fail'` still resets, which is what keeps that contract honest.\n this._reset();\n const indexed: Map<Kind, number> = new Map<Kind, number>();\n const fragments: Map<Kind, number> = new Map<Kind, number>();\n const declined: Map<Kind, number> = new Map<Kind, number>();\n const skipped: ISkippedVectorRecord[] = [];\n // Only the source knows what it filtered, so an absent `excluded` propagates\n // as absent rather than becoming an empty map. Before this contract existed\n // the fragment path dropped this tally on the floor, having nowhere honest to\n // put it.\n const report = (): IFragmentVectorRebuildReport => ({\n indexed,\n fragments,\n declined,\n excluded: listed.value.excluded,\n skipped\n });\n for (const scoped of listed.value.records) {\n const kind: Kind = scoped.record.envelope.kind;\n // Consumer-supplied, so a throw or rejection is captured rather than\n // escaping mid-loop and leaving the index half-populated.\n const embedded: Result<ReadonlyArray<IEmbeddedFragment>> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `fragment index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // An empty array is this lane's decline. It still performs a real\n // whole-record-replace — which is what clears any stale fragments — so it\n // is written, then counted as declined rather than indexed.\n const added: Result<number> = await this.addFragments(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `fragment index rebuild: ${added.message}`;\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n if (added.value === 0) {\n tally(declined, kind);\n continue;\n }\n // Tallied per successful add rather than read back off the counts at the\n // end, so the per-kind buckets line up with their siblings.\n tally(indexed, kind);\n tally(fragments, kind, added.value);\n }\n return succeedWithDetail(report());\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._records.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored fragment vector. A zero-magnitude vector\n * on either side yields `0` rather than `NaN`.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryFragmentCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}