@fgv/ts-agent-memory 5.1.0-50 → 5.1.0-52
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.
- package/dist/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
- package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/dist/packlets/store/fileTreeMemoryStore.js +36 -123
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/store/memoryStore.js.map +1 -1
- package/dist/packlets/store/storeFileAccess.js +99 -0
- package/dist/packlets/store/storeFileAccess.js.map +1 -0
- package/dist/packlets/store/storeIdentity.js +88 -0
- package/dist/packlets/store/storeIdentity.js.map +1 -0
- package/dist/packlets/types/identityResolver.js +6 -0
- package/dist/packlets/types/identityResolver.js.map +1 -0
- package/dist/packlets/types/index.js +1 -0
- package/dist/packlets/types/index.js.map +1 -1
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +38 -17
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +188 -41
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +4 -19
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +36 -123
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/store/memoryStore.d.ts +2 -2
- package/lib/packlets/store/memoryStore.d.ts.map +1 -1
- package/lib/packlets/store/memoryStore.js.map +1 -1
- package/lib/packlets/store/storeFileAccess.d.ts +40 -0
- package/lib/packlets/store/storeFileAccess.d.ts.map +1 -0
- package/lib/packlets/store/storeFileAccess.js +105 -0
- package/lib/packlets/store/storeFileAccess.js.map +1 -0
- package/lib/packlets/store/storeIdentity.d.ts +63 -0
- package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
- package/lib/packlets/store/storeIdentity.js +94 -0
- package/lib/packlets/store/storeIdentity.js.map +1 -0
- package/lib/packlets/types/identityResolver.d.ts +42 -0
- package/lib/packlets/types/identityResolver.d.ts.map +1 -0
- package/lib/packlets/types/identityResolver.js +7 -0
- package/lib/packlets/types/identityResolver.js.map +1 -0
- package/lib/packlets/types/index.d.ts +1 -0
- package/lib/packlets/types/index.d.ts.map +1 -1
- package/lib/packlets/types/index.js +1 -0
- package/lib/packlets/types/index.js.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +9 -17
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +38 -17
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/lib/packlets/vector/vectorIndex.d.ts +57 -6
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- 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,
|
|
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,40 @@
|
|
|
1
|
+
import { FileTree } from '@fgv/ts-json-base';
|
|
2
|
+
import { Result } from '@fgv/ts-utils';
|
|
3
|
+
import { MemoryScopeKey } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Maps a {@link MemoryScopeKey} to the relative directory path it is stored under.
|
|
6
|
+
* Injected into the store, so this layer takes it rather than re-deriving it.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export type ScopeEncoder = (scope: MemoryScopeKey) => Result<string>;
|
|
10
|
+
/**
|
|
11
|
+
* The FileTree access layer beneath `FileTreeMemoryStore`: resolving and creating
|
|
12
|
+
* scope directories, and writing and deleting record files.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Package-internal. Separated from the store because it is the one part of it that
|
|
16
|
+
* knows *where bytes live* rather than *what a record means* — it depends on nothing
|
|
17
|
+
* but the root directory and the scope encoder, and on no record semantics at all
|
|
18
|
+
* (no registry, no codec, no policy, no index). Extracted as free functions taking
|
|
19
|
+
* their two dependencies explicitly, matching `storeIdentity.ts`, so the seam is
|
|
20
|
+
* visible in the signatures rather than implied by `this`.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the directory for a scope, returning `undefined` when it does not
|
|
25
|
+
* exist. Navigation only — does not create. Folds the path segments through
|
|
26
|
+
* `getChildren` so an absent segment short-circuits to `undefined`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveScopeDir(root: FileTree.IMutableFileTreeDirectoryItem, scopeEncoding: ScopeEncoder, scope: MemoryScopeKey): Result<FileTree.IFileTreeDirectoryItem | undefined>;
|
|
29
|
+
/** Ensure the scope directory exists, creating segments as needed. */
|
|
30
|
+
export declare function ensureScopeDir(root: FileTree.IMutableFileTreeDirectoryItem, scopeEncoding: ScopeEncoder, scope: MemoryScopeKey): Result<FileTree.IMutableFileTreeDirectoryItem>;
|
|
31
|
+
/** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
|
|
32
|
+
export declare function writeRecordFile(root: FileTree.IMutableFileTreeDirectoryItem, scopeEncoding: ScopeEncoder, scope: MemoryScopeKey, idStem: string, raw: string): Result<true>;
|
|
33
|
+
/**
|
|
34
|
+
* Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
|
|
35
|
+
* guards are unreachable through the callers (`delete` / `_evict` both read the
|
|
36
|
+
* record first, so the directory and file exist) but are kept so a future
|
|
37
|
+
* direct caller degrades loudly rather than silently.
|
|
38
|
+
*/
|
|
39
|
+
export declare function deleteRecordFile(root: FileTree.IMutableFileTreeDirectoryItem, scopeEncoding: ScopeEncoder, scope: MemoryScopeKey, idStem: string): Result<true>;
|
|
40
|
+
//# sourceMappingURL=storeFileAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storeFileAccess.d.ts","sourceRoot":"","sources":["../../../src/packlets/store/storeFileAccess.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAK1C;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC;AAErE;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,CAAC,6BAA6B,EAC5C,aAAa,EAAE,YAAY,EAC3B,KAAK,EAAE,cAAc,GACpB,MAAM,CAAC,QAAQ,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAsBrD;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAC5B,IAAI,EAAE,QAAQ,CAAC,6BAA6B,EAC5C,aAAa,EAAE,YAAY,EAC3B,KAAK,EAAE,cAAc,GACpB,MAAM,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAuBhD;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,CAAC,6BAA6B,EAC5C,aAAa,EAAE,YAAY,EAC3B,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,MAAM,CAAC,IAAI,CAAC,CAiBd;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,QAAQ,CAAC,6BAA6B,EAC5C,aAAa,EAAE,YAAY,EAC3B,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,IAAI,CAAC,CAsBd"}
|
|
@@ -0,0 +1,105 @@
|
|
|
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.resolveScopeDir = resolveScopeDir;
|
|
8
|
+
exports.ensureScopeDir = ensureScopeDir;
|
|
9
|
+
exports.writeRecordFile = writeRecordFile;
|
|
10
|
+
exports.deleteRecordFile = deleteRecordFile;
|
|
11
|
+
const ts_json_base_1 = require("@fgv/ts-json-base");
|
|
12
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
13
|
+
/** The record filename extension; the store's files are markdown + YAML frontmatter. */
|
|
14
|
+
const MEMORY_FILE_EXTENSION = '.md';
|
|
15
|
+
/**
|
|
16
|
+
* The FileTree access layer beneath `FileTreeMemoryStore`: resolving and creating
|
|
17
|
+
* scope directories, and writing and deleting record files.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Package-internal. Separated from the store because it is the one part of it that
|
|
21
|
+
* knows *where bytes live* rather than *what a record means* — it depends on nothing
|
|
22
|
+
* but the root directory and the scope encoder, and on no record semantics at all
|
|
23
|
+
* (no registry, no codec, no policy, no index). Extracted as free functions taking
|
|
24
|
+
* their two dependencies explicitly, matching `storeIdentity.ts`, so the seam is
|
|
25
|
+
* visible in the signatures rather than implied by `this`.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the directory for a scope, returning `undefined` when it does not
|
|
30
|
+
* exist. Navigation only — does not create. Folds the path segments through
|
|
31
|
+
* `getChildren` so an absent segment short-circuits to `undefined`.
|
|
32
|
+
*/
|
|
33
|
+
function resolveScopeDir(root, scopeEncoding, scope) {
|
|
34
|
+
return scopeEncoding(scope).onSuccess((encoded) => {
|
|
35
|
+
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
36
|
+
return segments.reduce((acc, segment) => acc.onSuccess((current) => {
|
|
37
|
+
if (current === undefined) {
|
|
38
|
+
return (0, ts_utils_1.succeed)(undefined);
|
|
39
|
+
}
|
|
40
|
+
return current
|
|
41
|
+
.getChildren()
|
|
42
|
+
.onSuccess((children) => (0, ts_utils_1.succeed)(children.find((c) => c.type === 'directory' && c.name === segment)));
|
|
43
|
+
}), (0, ts_utils_1.succeed)(root));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Ensure the scope directory exists, creating segments as needed. */
|
|
47
|
+
function ensureScopeDir(root, scopeEncoding, scope) {
|
|
48
|
+
return scopeEncoding(scope).onSuccess((encoded) => {
|
|
49
|
+
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
50
|
+
return segments.reduce((acc, segment) => acc.onSuccess((current) => current.getChildren().onSuccess((children) => {
|
|
51
|
+
const existing = children.find((c) => c.type === 'directory' && c.name === segment);
|
|
52
|
+
if (existing === undefined) {
|
|
53
|
+
return current.createChildDirectory(segment);
|
|
54
|
+
}
|
|
55
|
+
/* c8 ignore next 3 -- defensive: a child of a mutable in-memory/fs tree is itself mutable; the guard protects against a read-only adapter handed in as root */
|
|
56
|
+
if (!ts_json_base_1.FileTree.isMutableDirectoryItem(existing)) {
|
|
57
|
+
return (0, ts_utils_1.fail)(`${existing.absolutePath}: directory is not mutable`);
|
|
58
|
+
}
|
|
59
|
+
return (0, ts_utils_1.succeed)(existing);
|
|
60
|
+
})), (0, ts_utils_1.succeed)(root));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
|
|
64
|
+
function writeRecordFile(root, scopeEncoding, scope, idStem, raw) {
|
|
65
|
+
return ensureScopeDir(root, scopeEncoding, scope).onSuccess((scopeDir) => scopeDir.getChildren().onSuccess((children) => {
|
|
66
|
+
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
67
|
+
const existing = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
68
|
+
if (existing === undefined) {
|
|
69
|
+
return scopeDir.createChildFile(fileName, raw).onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
70
|
+
}
|
|
71
|
+
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
72
|
+
if (!ts_json_base_1.FileTree.isMutableFileItem(existing)) {
|
|
73
|
+
return (0, ts_utils_1.fail)(`${existing.absolutePath}: file is not mutable`);
|
|
74
|
+
}
|
|
75
|
+
return existing.setRawContents(raw).onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
|
|
80
|
+
* guards are unreachable through the callers (`delete` / `_evict` both read the
|
|
81
|
+
* record first, so the directory and file exist) but are kept so a future
|
|
82
|
+
* direct caller degrades loudly rather than silently.
|
|
83
|
+
*/
|
|
84
|
+
function deleteRecordFile(root, scopeEncoding, scope, idStem) {
|
|
85
|
+
return resolveScopeDir(root, scopeEncoding, scope).onSuccess((scopeDir) => {
|
|
86
|
+
/* c8 ignore next 3 -- unreachable: callers read the record (hence the scope dir) first */
|
|
87
|
+
if (scopeDir === undefined) {
|
|
88
|
+
return (0, ts_utils_1.fail)(`memory delete: scope '${scope}' not found`);
|
|
89
|
+
}
|
|
90
|
+
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
91
|
+
return scopeDir.getChildren().onSuccess((children) => {
|
|
92
|
+
const file = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
93
|
+
/* c8 ignore next 3 -- unreachable: callers read the record (hence the file) first */
|
|
94
|
+
if (file === undefined) {
|
|
95
|
+
return (0, ts_utils_1.fail)(`memory delete: file '${fileName}' not found in scope '${scope}'`);
|
|
96
|
+
}
|
|
97
|
+
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
98
|
+
if (!ts_json_base_1.FileTree.isMutableFileItem(file)) {
|
|
99
|
+
return (0, ts_utils_1.fail)(`${file.absolutePath}: file is not mutable`);
|
|
100
|
+
}
|
|
101
|
+
return file.delete().onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=storeFileAccess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storeFileAccess.js","sourceRoot":"","sources":["../../../src/packlets/store/storeFileAccess.ts"],"names":[],"mappings":"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","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 { MemoryScopeKey } from '../types';\n\n/** The record filename extension; the store's files are markdown + YAML frontmatter. */\nconst MEMORY_FILE_EXTENSION: string = '.md';\n\n/**\n * Maps a {@link MemoryScopeKey} to the relative directory path it is stored under.\n * Injected into the store, so this layer takes it rather than re-deriving it.\n * @internal\n */\nexport type ScopeEncoder = (scope: MemoryScopeKey) => Result<string>;\n\n/**\n * The FileTree access layer beneath `FileTreeMemoryStore`: resolving and creating\n * scope directories, and writing and deleting record files.\n *\n * @remarks\n * Package-internal. Separated from the store because it is the one part of it that\n * knows *where bytes live* rather than *what a record means* — it depends on nothing\n * but the root directory and the scope encoder, and on no record semantics at all\n * (no registry, no codec, no policy, no index). Extracted as free functions taking\n * their two dependencies explicitly, matching `storeIdentity.ts`, so the seam is\n * visible in the signatures rather than implied by `this`.\n * @internal\n */\n\n/**\n * Resolve the directory for a scope, returning `undefined` when it does not\n * exist. Navigation only — does not create. Folds the path segments through\n * `getChildren` so an absent segment short-circuits to `undefined`.\n */\nexport function resolveScopeDir(\n root: FileTree.IMutableFileTreeDirectoryItem,\n scopeEncoding: ScopeEncoder,\n scope: MemoryScopeKey\n): Result<FileTree.IFileTreeDirectoryItem | undefined> {\n return scopeEncoding(scope).onSuccess((encoded) => {\n const segments: string[] = encoded.split('/').filter((s) => s.length > 0);\n return segments.reduce<Result<FileTree.IFileTreeDirectoryItem | undefined>>(\n (acc, segment) =>\n acc.onSuccess((current) => {\n if (current === undefined) {\n return succeed(undefined);\n }\n return current\n .getChildren()\n .onSuccess((children) =>\n succeed(\n children.find(\n (c): c is FileTree.IFileTreeDirectoryItem => c.type === 'directory' && c.name === segment\n )\n )\n );\n }),\n succeed(root)\n );\n });\n}\n\n/** Ensure the scope directory exists, creating segments as needed. */\nexport function ensureScopeDir(\n root: FileTree.IMutableFileTreeDirectoryItem,\n scopeEncoding: ScopeEncoder,\n scope: MemoryScopeKey\n): Result<FileTree.IMutableFileTreeDirectoryItem> {\n return scopeEncoding(scope).onSuccess((encoded) => {\n const segments: string[] = encoded.split('/').filter((s) => s.length > 0);\n return segments.reduce<Result<FileTree.IMutableFileTreeDirectoryItem>>(\n (acc, segment) =>\n acc.onSuccess((current) =>\n current.getChildren().onSuccess((children) => {\n const existing: FileTree.FileTreeItem | undefined = children.find(\n (c) => c.type === 'directory' && c.name === segment\n );\n if (existing === undefined) {\n return current.createChildDirectory(segment);\n }\n /* c8 ignore next 3 -- defensive: a child of a mutable in-memory/fs tree is itself mutable; the guard protects against a read-only adapter handed in as root */\n if (!FileTree.isMutableDirectoryItem(existing)) {\n return fail(`${existing.absolutePath}: directory is not mutable`);\n }\n return succeed(existing);\n })\n ),\n succeed(root)\n );\n });\n}\n\n/** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */\nexport function writeRecordFile(\n root: FileTree.IMutableFileTreeDirectoryItem,\n scopeEncoding: ScopeEncoder,\n scope: MemoryScopeKey,\n idStem: string,\n raw: string\n): Result<true> {\n return ensureScopeDir(root, scopeEncoding, scope).onSuccess((scopeDir) =>\n scopeDir.getChildren().onSuccess((children) => {\n const fileName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;\n const existing: FileTree.FileTreeItem | undefined = children.find(\n (c) => c.type === 'file' && c.name === fileName\n );\n if (existing === undefined) {\n return scopeDir.createChildFile(fileName, raw).onSuccess(() => succeed(true));\n }\n /* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */\n if (!FileTree.isMutableFileItem(existing)) {\n return fail(`${existing.absolutePath}: file is not mutable`);\n }\n return existing.setRawContents(raw).onSuccess(() => succeed(true));\n })\n );\n}\n\n/**\n * Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing\n * guards are unreachable through the callers (`delete` / `_evict` both read the\n * record first, so the directory and file exist) but are kept so a future\n * direct caller degrades loudly rather than silently.\n */\nexport function deleteRecordFile(\n root: FileTree.IMutableFileTreeDirectoryItem,\n scopeEncoding: ScopeEncoder,\n scope: MemoryScopeKey,\n idStem: string\n): Result<true> {\n return resolveScopeDir(root, scopeEncoding, scope).onSuccess((scopeDir) => {\n /* c8 ignore next 3 -- unreachable: callers read the record (hence the scope dir) first */\n if (scopeDir === undefined) {\n return fail(`memory delete: scope '${scope}' not found`);\n }\n const fileName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;\n return scopeDir.getChildren().onSuccess((children) => {\n const file: FileTree.FileTreeItem | undefined = children.find(\n (c) => c.type === 'file' && c.name === fileName\n );\n /* c8 ignore next 3 -- unreachable: callers read the record (hence the file) first */\n if (file === undefined) {\n return fail(`memory delete: file '${fileName}' not found in scope '${scope}'`);\n }\n /* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */\n if (!FileTree.isMutableFileItem(file)) {\n return fail(`${file.absolutePath}: file is not mutable`);\n }\n return file.delete().onSuccess(() => succeed(true));\n });\n });\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
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 that a record read from a **kind-derived address** is of that kind.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* A record's address is `(scope, idStem)` and carries no kind component, so two
|
|
30
|
+
* kinds whose codecs can mint the same address name one file. Without this check
|
|
31
|
+
* the second write is not even an overwrite: the store reads the occupant, the
|
|
32
|
+
* policy merge rebuilds it as `{ ...existing.envelope, body: patched }`, and the
|
|
33
|
+
* victim keeps its own `kind` while taking the intruder's body — silently, because
|
|
34
|
+
* `kind` is immutable to every policy and so the write cannot look wrong. The
|
|
35
|
+
* intruder's own `list` then returns nothing.
|
|
36
|
+
*
|
|
37
|
+
* A kind's `IWritePolicy` cannot close this: its admission cohort is same-scope
|
|
38
|
+
* same-kind by contract, so a policy is never shown a foreign occupant. This is
|
|
39
|
+
* the only layer that sees both the incoming kind and the existing record.
|
|
40
|
+
*
|
|
41
|
+
* Pass `expected` only where a kind actually produced the address. An
|
|
42
|
+
* address-first read (`getById`) has no kind in play and must not be guarded.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyOccupantKind<T>(expected: Kind | undefined, scope: MemoryScopeKey, idStem: string, record: T & {
|
|
46
|
+
readonly envelope: {
|
|
47
|
+
readonly kind: Kind;
|
|
48
|
+
};
|
|
49
|
+
}): Result<T>;
|
|
50
|
+
/**
|
|
51
|
+
* Cross-check a loaded record's declared identity against the address it was read
|
|
52
|
+
* from.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* The filename stem and the scope are the storage-side identity; the envelope's
|
|
56
|
+
* `id` / `entityId` are what downstream code trusts verbatim (merge-into
|
|
57
|
+
* re-addressing, for one). A tampered or corrupt file declaring a foreign
|
|
58
|
+
* `entityId` would otherwise load undetected, so the two are reconciled here
|
|
59
|
+
* through the codec's own round-trip.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export declare function verifyLoadedIdentity(codec: Result<IIdentityCodec>, scope: MemoryScopeKey, file: FileTree.IFileTreeFileItem, record: IMemoryRecord<unknown>): Result<IMemoryRecord<unknown>>;
|
|
63
|
+
//# 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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,QAAQ,EAAE,IAAI,GAAG,SAAS,EAC1B,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,CAAC,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAA;CAAE,GACzD,MAAM,CAAC,CAAC,CAAC,CAOX;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,94 @@
|
|
|
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.verifyOccupantKind = verifyOccupantKind;
|
|
10
|
+
exports.verifyLoadedIdentity = verifyLoadedIdentity;
|
|
11
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
12
|
+
/**
|
|
13
|
+
* The identity codec registered for `kind`, or the default.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Package-internal. Extracted from `FileTreeMemoryStore` rather than living on it
|
|
17
|
+
* because the file is at its `max-lines` cap — the fourth consecutive stream to pay
|
|
18
|
+
* that toll, which `TECH_DEBT.md` names as the trigger to promote the split to P1.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
function codecFor(codecs, defaultCodec, kind) {
|
|
22
|
+
var _a;
|
|
23
|
+
const codec = (_a = codecs.get(kind)) !== null && _a !== void 0 ? _a : defaultCodec;
|
|
24
|
+
if (codec === undefined) {
|
|
25
|
+
return (0, ts_utils_1.fail)(`no identity codec registered for kind '${kind}'`);
|
|
26
|
+
}
|
|
27
|
+
return (0, ts_utils_1.succeed)(codec);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `(kind, entityId)` to the storage address the vault files it under,
|
|
31
|
+
* without reading the record.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* `kind` selects the codec and the codec computes the address, so this is a
|
|
35
|
+
* function rather than a search — which is what makes an `EntityId` that collides
|
|
36
|
+
* across kinds a non-issue instead of an ambiguity to disambiguate.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
function resolveIdentity(codecs, defaultCodec, kind, entityId) {
|
|
40
|
+
return codecFor(codecs, defaultCodec, kind).onSuccess((codec) => codec.encode(entityId));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Cross-check that a record read from a **kind-derived address** is of that kind.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* A record's address is `(scope, idStem)` and carries no kind component, so two
|
|
47
|
+
* kinds whose codecs can mint the same address name one file. Without this check
|
|
48
|
+
* the second write is not even an overwrite: the store reads the occupant, the
|
|
49
|
+
* policy merge rebuilds it as `{ ...existing.envelope, body: patched }`, and the
|
|
50
|
+
* victim keeps its own `kind` while taking the intruder's body — silently, because
|
|
51
|
+
* `kind` is immutable to every policy and so the write cannot look wrong. The
|
|
52
|
+
* intruder's own `list` then returns nothing.
|
|
53
|
+
*
|
|
54
|
+
* A kind's `IWritePolicy` cannot close this: its admission cohort is same-scope
|
|
55
|
+
* same-kind by contract, so a policy is never shown a foreign occupant. This is
|
|
56
|
+
* the only layer that sees both the incoming kind and the existing record.
|
|
57
|
+
*
|
|
58
|
+
* Pass `expected` only where a kind actually produced the address. An
|
|
59
|
+
* address-first read (`getById`) has no kind in play and must not be guarded.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
function verifyOccupantKind(expected, scope, idStem, record) {
|
|
63
|
+
if (expected !== undefined && record.envelope.kind !== expected) {
|
|
64
|
+
return (0, ts_utils_1.fail)(`memory address '${scope}/${idStem}' is occupied by a record of kind '${record.envelope.kind}', not '${expected}': two identity codecs mint the same address`);
|
|
65
|
+
}
|
|
66
|
+
return (0, ts_utils_1.succeed)(record);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cross-check a loaded record's declared identity against the address it was read
|
|
70
|
+
* from.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* The filename stem and the scope are the storage-side identity; the envelope's
|
|
74
|
+
* `id` / `entityId` are what downstream code trusts verbatim (merge-into
|
|
75
|
+
* re-addressing, for one). A tampered or corrupt file declaring a foreign
|
|
76
|
+
* `entityId` would otherwise load undetected, so the two are reconciled here
|
|
77
|
+
* through the codec's own round-trip.
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
function verifyLoadedIdentity(codec, scope, file, record) {
|
|
81
|
+
if (record.envelope.id !== file.baseName) {
|
|
82
|
+
return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`);
|
|
83
|
+
}
|
|
84
|
+
return codec
|
|
85
|
+
.onSuccess((c) => c.verifyRoundTrip(scope, file.baseName).onSuccess(() => c.decode(scope, file.baseName)))
|
|
86
|
+
.withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
|
|
87
|
+
.onSuccess((decodedEntityId) => {
|
|
88
|
+
if (decodedEntityId !== record.envelope.entityId) {
|
|
89
|
+
return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`);
|
|
90
|
+
}
|
|
91
|
+
return (0, ts_utils_1.succeed)(record);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//# 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;AAsBD,gDAYC;AAcD,oDAsBC;AAtHD,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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,kBAAkB,CAChC,QAA0B,EAC1B,KAAqB,EACrB,MAAc,EACd,MAA0D;IAE1D,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAChE,OAAO,IAAA,eAAI,EACT,mBAAmB,KAAK,IAAI,MAAM,sCAAsC,MAAM,CAAC,QAAQ,CAAC,IAAI,WAAW,QAAQ,8CAA8C,CAC9J,CAAC;IACJ,CAAC;IACD,OAAO,IAAA,kBAAO,EAAC,MAAM,CAAC,CAAC;AACzB,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 that a record read from a **kind-derived address** is of that kind.\n *\n * @remarks\n * A record's address is `(scope, idStem)` and carries no kind component, so two\n * kinds whose codecs can mint the same address name one file. Without this check\n * the second write is not even an overwrite: the store reads the occupant, the\n * policy merge rebuilds it as `{ ...existing.envelope, body: patched }`, and the\n * victim keeps its own `kind` while taking the intruder's body — silently, because\n * `kind` is immutable to every policy and so the write cannot look wrong. The\n * intruder's own `list` then returns nothing.\n *\n * A kind's `IWritePolicy` cannot close this: its admission cohort is same-scope\n * same-kind by contract, so a policy is never shown a foreign occupant. This is\n * the only layer that sees both the incoming kind and the existing record.\n *\n * Pass `expected` only where a kind actually produced the address. An\n * address-first read (`getById`) has no kind in play and must not be guarded.\n * @internal\n */\nexport function verifyOccupantKind<T>(\n expected: Kind | undefined,\n scope: MemoryScopeKey,\n idStem: string,\n record: T & { readonly envelope: { readonly kind: Kind } }\n): Result<T> {\n if (expected !== undefined && record.envelope.kind !== expected) {\n return fail(\n `memory address '${scope}/${idStem}' is occupied by a record of kind '${record.envelope.kind}', not '${expected}': two identity codecs mint the same address`\n );\n }\n return succeed(record);\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 @@
|
|
|
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"]}
|
|
@@ -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,26 +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,
|
|
53
|
+
query(vector: Float32Array, topK: number, options?: IFragmentQueryOptions): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
* scratch. Clears the current contents (and the established dimension) first, so
|
|
57
|
-
* a re-embed with a different model is supported. Returns the total number of
|
|
58
|
-
* fragments indexed.
|
|
59
|
-
*
|
|
60
|
-
* On any failure (list, embed, or add) the index is rolled back to empty rather
|
|
61
|
-
* than left in a partially-rebuilt state.
|
|
55
|
+
* The records a query is allowed to score, honoring the `scope` / `id` narrowing.
|
|
62
56
|
*
|
|
63
57
|
* @remarks
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* @param source - The scope-qualified record source to re-embed.
|
|
71
|
-
* @param embed - The fragment embedder applied to each record.
|
|
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.
|
|
72
63
|
*/
|
|
64
|
+
private _selectRecords;
|
|
73
65
|
/** {@inheritDoc IFragmentVectorIndex.rebuild} */
|
|
74
66
|
rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>>;
|
|
75
67
|
/** Empty the index and forget the established dimension. */
|
|
@@ -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,
|
|
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,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"}
|