@wovin/core 0.3.0 → 0.3.3

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 (53) hide show
  1. package/dist/applog/applog-utils.d.ts +25 -1
  2. package/dist/applog/applog-utils.d.ts.map +1 -1
  3. package/dist/applog.js +5 -3
  4. package/dist/{chunk-2OXLPZQI.js → chunk-4MKPGQIM.js} +8 -16
  5. package/dist/chunk-4MKPGQIM.js.map +1 -0
  6. package/dist/{chunk-VGIACGWX.js → chunk-6CSJTSQP.js} +3 -3
  7. package/dist/{chunk-WVW4YXB5.js → chunk-BIYQEX3N.js} +2 -2
  8. package/dist/{chunk-Q4EMPWA3.js → chunk-H3JNNTVP.js} +9 -3
  9. package/dist/chunk-H3JNNTVP.js.map +1 -0
  10. package/dist/{chunk-EHO2BFFY.js → chunk-H4YVJKB7.js} +2 -2
  11. package/dist/chunk-N5QPZNKD.js +288 -0
  12. package/dist/chunk-N5QPZNKD.js.map +1 -0
  13. package/dist/{chunk-2PJFLZRC.js → chunk-N7SEGHU4.js} +2 -2
  14. package/dist/index.js +20 -10
  15. package/dist/ipfs/car.d.ts.map +1 -1
  16. package/dist/ipfs.js +4 -4
  17. package/dist/ipns/ipns-record.d.ts +19 -1
  18. package/dist/ipns/ipns-record.d.ts.map +1 -1
  19. package/dist/ipns/ipns-w3name.d.ts +31 -1
  20. package/dist/ipns/ipns-w3name.d.ts.map +1 -1
  21. package/dist/ipns/ipns-watcher.d.ts.map +1 -1
  22. package/dist/ipns.js +118 -21
  23. package/dist/ipns.js.map +1 -1
  24. package/dist/pubsub.js +4 -4
  25. package/dist/query/epoch-snapshot.d.ts +205 -0
  26. package/dist/query/epoch-snapshot.d.ts.map +1 -0
  27. package/dist/query.d.ts +1 -0
  28. package/dist/query.d.ts.map +1 -1
  29. package/dist/query.js +12 -4
  30. package/dist/retrieve.js +4 -4
  31. package/dist/thread.js +1 -1
  32. package/dist/viewmodel/index.js +4 -4
  33. package/dist/viewmodel/index.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/applog/applog-utils.test.ts +43 -1
  36. package/src/applog/applog-utils.ts +38 -21
  37. package/src/applog/object-values.test.ts +9 -9
  38. package/src/ipfs/car.ts +6 -0
  39. package/src/ipns/ipns-record.ts +95 -17
  40. package/src/ipns/ipns-w3name.ts +109 -5
  41. package/src/ipns/ipns-watcher.ts +4 -3
  42. package/src/query/epoch-snapshot.test.ts +594 -0
  43. package/src/query/epoch-snapshot.ts +392 -0
  44. package/src/query.ts +1 -0
  45. package/src/viewmodel/factory.ts +2 -2
  46. package/dist/chunk-2OXLPZQI.js.map +0 -1
  47. package/dist/chunk-OKXRRWNS.js +0 -138
  48. package/dist/chunk-OKXRRWNS.js.map +0 -1
  49. package/dist/chunk-Q4EMPWA3.js.map +0 -1
  50. /package/dist/{chunk-VGIACGWX.js.map → chunk-6CSJTSQP.js.map} +0 -0
  51. /package/dist/{chunk-WVW4YXB5.js.map → chunk-BIYQEX3N.js.map} +0 -0
  52. /package/dist/{chunk-EHO2BFFY.js.map → chunk-H4YVJKB7.js.map} +0 -0
  53. /package/dist/{chunk-2PJFLZRC.js.map → chunk-N7SEGHU4.js.map} +0 -0
@@ -0,0 +1,205 @@
1
+ import type { Applog, Attribute, Timestamp } from '../applog/datom-types.ts';
2
+ import type { Thread } from '../thread/basic.ts';
3
+ import type { WriteableThread } from '../thread/writeable.ts';
4
+ /**
5
+ * An EpochSnapshot maps every {@link Attribute} present in a time window to the
6
+ * array of {@link Applog}s (from any entity) that carry that attribute.
7
+ *
8
+ * This is **not** a flat array — logs are grouped by attribute first so that
9
+ * consumers can answer questions like "which entities had their `movie/title`
10
+ * updated in this window?" without re-filtering.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const snap = createEpochSnapshot(thread, {
15
+ * start: '2024-01-01T00:00:00.000Z',
16
+ * end: '2024-06-01T00:00:00.000Z',
17
+ * })
18
+ * for (const [attr, logs] of snap) {
19
+ * console.log(`${attr}: ${logs.length} logs`)
20
+ * }
21
+ * ```
22
+ */
23
+ export type EpochSnapshot = ReadonlyMap<Attribute, readonly Applog[]>;
24
+ /**
25
+ * Options for {@link createEpochSnapshot}.
26
+ */
27
+ export interface EpochSnapshotOptions {
28
+ /**
29
+ * Start of the time window (inclusive). ISO 8601 string matching the
30
+ * {@link Timestamp} format used on every {@link Applog}.
31
+ */
32
+ readonly start: Timestamp;
33
+ /**
34
+ * End of the time window (exclusive). ISO 8601 string.
35
+ * Logs whose `ts` is **equal to** `end` are excluded.
36
+ */
37
+ readonly end: Timestamp;
38
+ }
39
+ /**
40
+ * Creates an {@link EpochSnapshot} — a point-in-time grouping of all app logs
41
+ * within `[start, end)` grouped by their attribute name.
42
+ *
43
+ * The function is a **one-off (snapshot) computation**: it reads the current
44
+ * state of the thread and returns a frozen map. If you need reactivity, wrap
45
+ * the result in a reactive derivation or subscribe to thread changes and
46
+ * re-compute.
47
+ *
48
+ * @param thread - The thread to snapshot.
49
+ * @param options - Time range bounds.
50
+ * @returns A `ReadonlyMap` keyed by attribute name. Each value is the
51
+ * (chronologically sorted) array of applogs whose `at` matches that
52
+ * key and whose `ts` falls inside `[start, end)`.
53
+ *
54
+ * @throws {Error} If `start` is lexically `>=` `end` (empty range).
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const snap = createEpochSnapshot(thread, {
59
+ * start: '2024-01-01T00:00:00.000Z',
60
+ * end: '2025-01-01T00:00:00.000Z',
61
+ * })
62
+ * // Iterate attribute groups:
63
+ * for (const [attr, logs] of snap) {
64
+ * console.log(attr, logs.length)
65
+ * }
66
+ * ```
67
+ */
68
+ export declare function createEpochSnapshot(thread: Thread, options: EpochSnapshotOptions): EpochSnapshot;
69
+ /**
70
+ * A flexible time specification used by {@link purgeHistory}.
71
+ *
72
+ * - `-1` (number) — the earliest log's timestamp in the thread ("beginning")
73
+ * - `0` (number) — current time ("now")
74
+ * - Positive number — treated as a Unix millisecond timestamp
75
+ * - `'-1d'`, `'-2h'`, `'-30m'`, `'-5s'` — relative offset **before** now
76
+ * - `'+1d'`, `'2h'` — relative offset **after** now (no sign = forward)
77
+ * - ISO 8601 string — absolute timestamp, used as-is
78
+ * - `'now'` — current time
79
+ */
80
+ export type TimeSpec = number | string;
81
+ /**
82
+ * Resolve a {@link TimeSpec} to an absolute ISO {@link Timestamp}.
83
+ *
84
+ * @param spec - The time specification to resolve.
85
+ * @param thread - The thread (used to find earliest log when spec is `-1`).
86
+ * @param now - The reference "now" for relative specs.
87
+ * @returns An ISO 8601 timestamp string.
88
+ */
89
+ export declare function resolveTimeSpec(spec: TimeSpec, thread: Thread, now: Date): Timestamp;
90
+ /**
91
+ * A JSON-serializable representation of an {@link EpochSnapshot}, safe for
92
+ * writing to disk, sending over the wire, or storing in IPFS.
93
+ */
94
+ export interface SerializedEpochSnapshot {
95
+ /** Format marker for versioning */
96
+ readonly format: 'wovin-epoch-snapshot-v1';
97
+ /** The attribute whose history was captured / purged */
98
+ readonly attribute: string;
99
+ /** The time range this snapshot covers */
100
+ readonly timeRange: {
101
+ readonly start: Timestamp;
102
+ readonly end: Timestamp;
103
+ };
104
+ /** When this snapshot was created */
105
+ readonly createdAt: Timestamp;
106
+ /** Total number of applogs across all groups */
107
+ readonly logCount: number;
108
+ /**
109
+ * The grouped applogs. Each key is an attribute name; the value is the
110
+ * array of applogs that carried that attribute within the time window.
111
+ */
112
+ readonly groups: Record<string, readonly Applog[]>;
113
+ }
114
+ /**
115
+ * Converts an {@link EpochSnapshot} into a {@link SerializedEpochSnapshot}
116
+ * that can be persisted as JSON.
117
+ *
118
+ * @param snapshot - The live snapshot to serialize.
119
+ * @param metadata - Identifying metadata to attach.
120
+ * @returns A plain, JSON-safe object.
121
+ */
122
+ export declare function serializeEpochSnapshot(snapshot: EpochSnapshot, metadata: {
123
+ /** The attribute this purge targets */
124
+ readonly attribute: string;
125
+ /** The time range of the snapshot */
126
+ readonly timeRange: {
127
+ readonly start: Timestamp;
128
+ readonly end: Timestamp;
129
+ };
130
+ }): SerializedEpochSnapshot;
131
+ /**
132
+ * Options for {@link purgeHistory}.
133
+ */
134
+ export interface PurgeHistoryOptions {
135
+ /**
136
+ * Callback invoked with the serialized snapshot **before** any logs are
137
+ * purged. Use this to persist the snapshot to a file, database, IPFS, or
138
+ * any other storage you choose.
139
+ *
140
+ * If omitted, the snapshot is created and returned in the result but not
141
+ * automatically persisted anywhere.
142
+ */
143
+ persistSnapshot?: (serialized: SerializedEpochSnapshot) => Promise<void> | void;
144
+ /**
145
+ * Override "now" for deterministic tests. Defaults to `new Date()`.
146
+ */
147
+ now?: Date;
148
+ }
149
+ /**
150
+ * The result of a successful {@link purgeHistory} call.
151
+ */
152
+ export interface PurgeHistoryResult {
153
+ /**
154
+ * The {@link EpochSnapshot} that was taken **before** the purge.
155
+ * Contains all logs (across all attributes) within the time window, even
156
+ * though only one attribute was purged.
157
+ */
158
+ readonly snapshot: EpochSnapshot;
159
+ /** Number of applogs that were removed from the thread. */
160
+ readonly purgedCount: number;
161
+ /** Number of applogs kept as LWW winners for the purged attribute. */
162
+ readonly keptCount: number;
163
+ /** The resolved time range that was processed. */
164
+ readonly timeRange: {
165
+ readonly start: Timestamp;
166
+ readonly end: Timestamp;
167
+ };
168
+ }
169
+ /**
170
+ * For a given **attribute** and **time range**:
171
+ *
172
+ * 1. Creates an {@link EpochSnapshot} capturing all app logs within
173
+ * `[start, end)` across **all** attributes.
174
+ * 2. Calls `options.persistSnapshot` with a serialized copy of the snapshot
175
+ * (so you can store the pre-purge state before any data is removed).
176
+ * 3. For the target attribute, groups the captured logs by entity and keeps
177
+ * only the **latest** (last-write-wins) applog per `(en, at)` pair.
178
+ * All older logs for that pair are purged from the thread.
179
+ *
180
+ * Logs for **other** attributes are never touched.
181
+ *
182
+ * @param thread - A writable thread to purge from.
183
+ * @param attribute - Only logs carrying this attribute are candidates for
184
+ * removal. Logs with other attributes are left untouched.
185
+ * @param start - Start of the time window (see {@link TimeSpec}).
186
+ * @param end - End of the time window, exclusive (see {@link TimeSpec}).
187
+ * @param options - Optional persistence callback and time override.
188
+ * @returns A {@link PurgeHistoryResult} with the snapshot and counts.
189
+ *
190
+ * @throws {RangeError} If the resolved `start` is not before `end`.
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * // Purge all but the latest block/content per entity from the beginning
195
+ * // up to yesterday, saving a snapshot first.
196
+ * const result = await purgeHistory(thread, 'block/content', -1, '-1d', {
197
+ * persistSnapshot: async (snap) => {
198
+ * await writeFile('purge-archive.json', JSON.stringify(snap, null, 2))
199
+ * },
200
+ * })
201
+ * console.log(`Purged ${result.purgedCount}, kept ${result.keptCount}`)
202
+ * ```
203
+ */
204
+ export declare function purgeHistory(thread: WriteableThread, attribute: Attribute, start: TimeSpec, end: TimeSpec, options?: PurgeHistoryOptions): Promise<PurgeHistoryResult>;
205
+ //# sourceMappingURL=epoch-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"epoch-snapshot.d.ts","sourceRoot":"","sources":["../../src/query/epoch-snapshot.ts"],"names":[],"mappings":"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"}
package/dist/query.d.ts CHANGED
@@ -7,4 +7,5 @@ export * from './query/subscribable.ts';
7
7
  export * from './query/types.ts';
8
8
  export * from './query/attr-helpers.ts';
9
9
  export * from './query/entity-collection.ts';
10
+ export * from './query/epoch-snapshot.ts';
10
11
  //# sourceMappingURL=query.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,8BAA8B,CAAA"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA"}
package/dist/query.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import {
2
+ createEpochSnapshot,
2
3
  liveEntityCollection,
3
- queryDivergencesByPrev
4
- } from "./chunk-OKXRRWNS.js";
4
+ purgeHistory,
5
+ queryDivergencesByPrev,
6
+ resolveTimeSpec,
7
+ serializeEpochSnapshot
8
+ } from "./chunk-N5QPZNKD.js";
5
9
  import {
6
10
  LiveQueryResult,
7
11
  QueryNode,
@@ -44,7 +48,7 @@ import {
44
48
  throwOnTimeout,
45
49
  withTimeout,
46
50
  withoutDeleted
47
- } from "./chunk-2PJFLZRC.js";
51
+ } from "./chunk-N7SEGHU4.js";
48
52
  import {
49
53
  SubscribableArrayImpl,
50
54
  SubscribableImpl,
@@ -57,7 +61,7 @@ import {
57
61
  memoizedFn,
58
62
  prettifyThreadName,
59
63
  refCountedMemoizedFn
60
- } from "./chunk-2OXLPZQI.js";
64
+ } from "./chunk-4MKPGQIM.js";
61
65
  import "./chunk-ZAADLBSB.js";
62
66
  export {
63
67
  LiveQueryResult,
@@ -69,6 +73,7 @@ export {
69
73
  anyOf,
70
74
  createDebugName,
71
75
  createDebugNameObj,
76
+ createEpochSnapshot,
72
77
  createObjMapper,
73
78
  entityOverlap,
74
79
  entityOverlapCount,
@@ -98,6 +103,7 @@ export {
98
103
  prefixAt,
99
104
  prefixAttrs,
100
105
  prettifyThreadName,
106
+ purgeHistory,
101
107
  query,
102
108
  queryAndMap,
103
109
  queryDivergencesByPrev,
@@ -108,6 +114,8 @@ export {
108
114
  queryStepOnce,
109
115
  refCountedMemoizedFn,
110
116
  resolveKeyMapper,
117
+ resolveTimeSpec,
118
+ serializeEpochSnapshot,
111
119
  startsWith,
112
120
  stripAtPrefix,
113
121
  threadFromMaybeArray,
package/dist/retrieve.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  updateThreadFromSnapshot,
3
3
  withBlockCache
4
- } from "./chunk-VGIACGWX.js";
5
- import "./chunk-Q4EMPWA3.js";
4
+ } from "./chunk-6CSJTSQP.js";
5
+ import "./chunk-H3JNNTVP.js";
6
6
  import "./chunk-YDAKBU6Q.js";
7
- import "./chunk-2PJFLZRC.js";
8
- import "./chunk-2OXLPZQI.js";
7
+ import "./chunk-N7SEGHU4.js";
8
+ import "./chunk-4MKPGQIM.js";
9
9
  import "./chunk-ZAADLBSB.js";
10
10
  export {
11
11
  updateThreadFromSnapshot,
package/dist/thread.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  rollingFilter,
27
27
  rollingMapper,
28
28
  simpleApplogMapper
29
- } from "./chunk-2OXLPZQI.js";
29
+ } from "./chunk-4MKPGQIM.js";
30
30
  import "./chunk-ZAADLBSB.js";
31
31
  export {
32
32
  MappedThread,
@@ -13,16 +13,16 @@ import {
13
13
  import {
14
14
  lastWriteWins,
15
15
  liveEntityAt
16
- } from "../chunk-2PJFLZRC.js";
16
+ } from "../chunk-N7SEGHU4.js";
17
17
  import {
18
18
  EntityID_LENGTH,
19
19
  SubscribableImpl,
20
20
  dateNowIso,
21
21
  ensureTsPvAndFinalizeApplog,
22
- getHashID,
23
22
  isInitEvent,
23
+ mintNewEntity,
24
24
  rollingFilter
25
- } from "../chunk-2OXLPZQI.js";
25
+ } from "../chunk-4MKPGQIM.js";
26
26
  import "../chunk-ZAADLBSB.js";
27
27
 
28
28
  // src/viewmodel/builder.ts
@@ -122,7 +122,7 @@ function createViewModelFactory(options) {
122
122
  } = options;
123
123
  const attrDefs = adapter.getAttributeDefs();
124
124
  const defaults = adapter.getDefaults();
125
- const genId = generateEntityId ?? ((data) => getHashID({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength));
125
+ const genId = generateEntityId ?? ((data) => mintNewEntity({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength));
126
126
  const persistFn = (thread, applogs) => {
127
127
  defaultPersistApplogs(thread, applogs);
128
128
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/viewmodel/builder.ts","../../src/viewmodel/types.ts","../../src/viewmodel/factory.ts","../../src/viewmodel/schema-adapter.ts"],"sourcesContent":["import type { ApplogForInsertOptionalAgent, ApplogValue, EntityID } from '../applog/datom-types.ts'\n\n/**\n * Generic ObjectBuilder for type-safe entity creation and updates.\n *\n * Provides a fluent API to build a set of applogs for an entity.\n * Modeled after the note3 builder pattern.\n *\n * Usage:\n * ```ts\n * ObjectBuilder.create({ name: 'foo' })\n * .update({ type: 'bar' })\n * .build(thread)\n * ```\n */\nexport class ObjectBuilder<\n\tTARGET extends Record<string, ApplogValue> = Record<string, ApplogValue>,\n> {\n\tconstructor(\n\t\tprivate _data: Partial<TARGET>,\n\t\tpublic en: EntityID | null = null,\n\t\tprivate _atPrefix: string | null = null,\n\t\tprivate _atOverrides: Partial<Record<keyof TARGET, string>> = {},\n\t) {}\n\n\t/**\n\t * Create a new builder with initial data.\n\t */\n\tstatic create<TARGET extends Record<string, ApplogValue>>(\n\t\tinit: Partial<TARGET> = {},\n\t\ten?: EntityID,\n\t\tatPrefix?: string,\n\t\tatOverrides?: Partial<Record<keyof TARGET, string>>,\n\t): ObjectBuilder<TARGET> {\n\t\treturn new ObjectBuilder<TARGET>(init, en, atPrefix, atOverrides)\n\t}\n\n\t/**\n\t * Update the data being built.\n\t */\n\tupdate(updateObj: Partial<TARGET>): this {\n\t\tObject.assign(this._data, updateObj)\n\t\treturn this\n\t}\n\n\t/**\n\t * Build the applogs for this entity, optionally resolving against a thread.\n\t */\n\tbuild(thread?: {\n\t\tinsert(applogs: ApplogForInsertOptionalAgent[]): ApplogForInsertOptionalAgent[]\n\t}): ApplogForInsertOptionalAgent[] {\n\t\tconst atPrefix = this._atPrefix\n\t\tconst applogs: ApplogForInsertOptionalAgent[] = []\n\n\t\tfor (const [key, value] of Object.entries(this._data) as [keyof TARGET, ApplogValue][]) {\n\t\t\tif (value === undefined) continue\n\t\t\tconst atOverride = this._atOverrides[key]\n\t\t\tconst at = atOverride ?? (atPrefix ? `${atPrefix}/${String(key)}` : String(key))\n\t\t\tapplogs.push({ en: this.en!, at, vl: value })\n\t\t}\n\n\t\treturn applogs\n\t}\n\n\t/**\n\t * Get the raw data being built.\n\t */\n\tget data(): Partial<TARGET> {\n\t\treturn { ...this._data }\n\t}\n}\n","import type { ApplogValue, EntityID } from '../applog/datom-types.ts'\nimport type { Thread } from '../thread/basic.ts'\nimport type { ObjectBuilder } from './builder.ts'\n\n// ═══════════════════════════════════════════════════════════════\n// Attribute Definition\n// ═══════════════════════════════════════════════════════════════\n\nexport interface VMAttributeDef {\n\t/** Attribute name (short: e.g. 'name', 'content') */\n\tname: string\n\t/** The wovin at-path used in applogs (e.g. 'block/content', 'speaker/name') */\n\tatPath: string\n\t/** Whether the attribute is optional */\n\toptional: boolean\n\t/** Default value if not present in applogs */\n\tdefaultValue?: ApplogValue\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Schema Adapter Interface\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * A schema adapter converts a type-system schema (TypeBox, Zod, ArkType, Typia)\n * into a uniform representation that the VM factory can consume.\n */\nexport interface ISchemaAdapter<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\t/** Get the attribute definitions for this schema */\n\tgetAttributeDefs(): VMAttributeDef[]\n\n\t/** Get default values for all attributes */\n\tgetDefaults(): Partial<T>\n\n\t/** Optional: runtime validator */\n\tcreateValidator?(): (value: unknown) => value is T\n\n\t/** The entity prefix used for wovin at-paths (e.g. 'block', 'speaker') */\n\tgetEntityPrefix(): string\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Factory Options\n// ═══════════════════════════════════════════════════════════════\n\nexport interface ViewModelFactoryOptions<T extends Record<string, ApplogValue>> {\n\t/** The schema adapter for this entity type */\n\tadapter: ISchemaAdapter<T>\n\n\t/** How to get the default thread context */\n\tgetDefaultThread?: () => Thread\n\n\t/** EntityID generation length (default: 7) */\n\tentityIdLength?: number\n\n\t/** Function to generate entity IDs */\n\tgenerateEntityId?: (data: Partial<T> & { ts?: string }) => EntityID\n\n\t/** Override the entity prefix for at-paths */\n\tentityPrefix?: string\n\n\t/** Optional function to make thread current-state (lastWriteWins) */\n\tmakeCurrentThread?: (thread: Thread) => Thread\n\n\t/** Name for the VM type (used in logging and debugging) */\n\tvmName?: string\n\n\t/**\n\t * Signal adapter for framework-specific reactivity.\n\t * Default: non-reactive (direct Subscribable reads).\n\t * For SolidJS, pass createSolidSignalAdapter() from @wovin/ui-solid/viewmodel.\n\t */\n\tsignalAdapter?: ISignalAdapter\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Instance Interface\n// ═══════════════════════════════════════════════════════════════\n\nexport interface IVMInstance<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\treadonly en: EntityID\n\treadonly thread: Thread\n\n\t/** Get a builder for updating this entity */\n\tbuildUpdate(init?: Partial<T>): ObjectBuilder<T>\n\n\t/** Soft-delete this entity */\n\tsetDeleted(thread?: Thread): void\n\n\t/** Whether this entity is deleted */\n\treadonly isDeleted: boolean\n\n\t/** The entity thread filtered to this entity */\n\treadonly entityThread: Thread\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Class Interface (what the factory returns)\n// ═══════════════════════════════════════════════════════════════\n\nexport interface IVMClass<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\tnew(en: EntityID, thread: Thread, ...args: any[]): IVMInstance<T>\n\n\t/** Get or create a VM instance for the given entity ID and thread */\n\tget(en: EntityID, thread: Thread): IVMInstance<T>\n\n\t/** Create a new entity builder */\n\tbuildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Signal Adapter Interface (framework-specific reactivity)\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * A signal adapter abstracts how the VM creates reactive signals for each attribute.\n * The core factory uses a default (non-reactive) implementation.\n * Framework-specific extensions (Solid, Vue, etc.) override this.\n */\nexport interface ISignalAdapter {\n\t/**\n\t * Create a reactive getter for a subscribable value.\n\t * Returns a function that returns the current value.\n\t * In the default implementation, this is just () => subscribable.value.\n\t */\n\tcreateGetter<T>(subscribable: { value: T; subscribe(cb: () => void): () => void }): () => T\n\n\t/**\n\t * Create a writable signal for an attribute.\n\t * Returns [get, set] pair.\n\t * Default implementation stores and returns value directly.\n\t */\n\tcreateWritable<T>(initial: T): [() => T, (v: T) => void]\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Shared state maps\n// ═══════════════════════════════════════════════════════════════\n\nexport type VMInstanceMap<T> = Map<EntityID, T>\n\n/** WeakMap from Thread → Map<VMName, Map<EntityID, VMInstance>> */\nexport const GlobalVMInstances = new WeakMap<Thread, Map<string, Map<EntityID, unknown>>>()\n\nexport function getInstancesForThread(thread: Thread): Map<string, Map<EntityID, unknown>> {\n\tlet threadMap = GlobalVMInstances.get(thread)\n\tif (!threadMap) {\n\t\tthreadMap = new Map()\n\t\tGlobalVMInstances.set(thread, threadMap)\n\t}\n\treturn threadMap\n}\n","import type { ApplogForInsertOptionalAgent, ApplogValue, EntityID } from '../applog/datom-types.ts'\nimport { EntityID_LENGTH } from '../applog/datom-types.ts'\nimport { dateNowIso, getHashID } from '../applog/applog-utils.ts'\nimport { ensureTsPvAndFinalizeApplog } from '../applog/applog-helpers.ts'\nimport { lastWriteWins, liveEntityAt } from '../query/basic.ts'\nimport { SubscribableImpl } from '../query/subscribable.ts'\nimport type { Thread } from '../thread/basic.ts'\nimport { isInitEvent } from '../thread/basic.ts'\nimport { rollingFilter } from '../thread/filters.ts'\nimport { ObjectBuilder } from './builder.ts'\nimport {\n\ttype ISignalAdapter,\n\ttype IVMInstance,\n\ttype ViewModelFactoryOptions,\n\ttype VMInstanceMap,\n\tgetInstancesForThread,\n} from './types.ts'\n\n// ═══════════════════════════════════════════════════════════════\n// Default (non-reactive) Signal Adapter\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Default signal adapter — reads Subscribable values directly (snapshot).\n * Subscribe-once to keep .value current. No framework reactivity.\n */\nexport const DefaultSignalAdapter: ISignalAdapter = {\n\tcreateGetter<T>(subscribable: { value: T; subscribe(cb: () => void): () => void }): () => T {\n\t\t// Subscribe once to activate upstream (lazy activation)\n\t\tsubscribable.subscribe(() => {})\n\t\treturn () => subscribable.value\n\t},\n\tcreateWritable<T>(initial: T): [() => T, (v: T) => void] {\n\t\tlet current = initial\n\t\treturn [() => current, (v: T) => { current = v }]\n\t},\n}\n\n// ═══════════════════════════════════════════════════════════════\n// makeCurrentThread - convenience wrapper\n// ═══════════════════════════════════════════════════════════════\n\nfunction makeCurrent(thread: Thread, fn?: ViewModelFactoryOptions<any>['makeCurrentThread']): Thread {\n\tif (fn) return fn(thread)\n\treturn lastWriteWins(thread, { tolerateAlreadyFiltered: true })\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Default persist function\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Default applog persistence: finalize applogs and push into thread.\n * Works with WriteableThread (which has insertRaw).\n */\nfunction defaultPersistApplogs(thread: Thread, applogs: ApplogForInsertOptionalAgent[]): void {\n\tconst finalized = applogs.map(log => ensureTsPvAndFinalizeApplog(log as any, thread))\n\tconst writable = thread as any\n\tif (typeof writable.insertRaw === 'function') {\n\t\twritable.insertRaw(finalized)\n\t} else if (typeof writable.insert === 'function') {\n\t\twritable.insert(applogs)\n\t} else {\n\t\t// Read-only thread — just finalize but don't persist\n\t\t// This is fine for optimistic UI or read-only scenarios\n\t}\n}\n\n// ═══════════════════════════════════════════════════════════════\n// createViewModelFactory - THE MAIN FACTORY\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Create a ViewModel factory for a given schema.\n *\n * Returns a base class that can be extended or used directly.\n * The class provides:\n * - Static `get(en, thread)` for singleton access\n * - Static `buildNew(init, en)` for creating entities\n * - Instance `buildUpdate(init)` for updating entities\n * - Lazy reactive property accessors per attribute\n * - `setDeleted()` for soft deletion\n *\n * Framework-specific reactivity (Solid, Vue, etc.) can be added by\n * providing a custom `ISignalAdapter` in the options.\n *\n * @param options - Factory configuration\n * @returns A VM class constructor with static methods\n */\nexport function createViewModelFactory<T extends Record<string, ApplogValue>>(\n\toptions: ViewModelFactoryOptions<T>,\n) {\n\tconst {\n\t\tadapter,\n\t\tentityPrefix = adapter.getEntityPrefix(),\n\t\tentityIdLength = EntityID_LENGTH,\n\t\tgenerateEntityId,\n\t\tvmName = entityPrefix,\n\t\tsignalAdapter = DefaultSignalAdapter,\n\t} = options\n\n\tconst attrDefs = adapter.getAttributeDefs()\n\tconst defaults = adapter.getDefaults()\n\n\t// Entity ID generation\n\tconst genId = generateEntityId ?? ((data: Partial<T> & { ts?: string }) =>\n\t\tgetHashID({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength) as EntityID)\n\n\t// Persist function (can be overridden by extending class)\n\tconst persistFn = (thread: Thread, applogs: ApplogForInsertOptionalAgent[]) => {\n\t\tdefaultPersistApplogs(thread, applogs)\n\t}\n\n\t// ═══════════════════════════════════════════════════════════════\n\t// The ViewModel Class\n\t// ═══════════════════════════════════════════════════════════════\n\tclass ViewModel {\n\t\t/** Per-instance signal storage */\n\t\t_signals = new Map<string, () => ApplogValue>()\n\t\t_signalAdapter: ISignalAdapter = signalAdapter\n\n\t\t/** Reactive map for applog tracking: vl === this.en */\n\t\t_targetMap: SubscribableImpl<Map<EntityID, Set<string>>>\n\n\t\t/** Applogs in the current thread where vl === this.en */\n\t\t_targetApplogs: Thread\n\n\t\tconstructor(\n\t\t\tpublic en: EntityID,\n\t\t\tpublic thread: Thread,\n\t\t\tskipInit?: boolean,\n\t\t) {\n\t\t\tif (skipInit) return\n\n\t\t\tconst currentThread = makeCurrent(thread, options.makeCurrentThread)\n\n\t\t\t// Define lazy getters/setters for each attribute\n\t\t\tfor (const attr of attrDefs) {\n\t\t\t\tif (attr.name === 'en') continue\n\n\t\t\t\tconst attrName = attr.name\n\t\t\t\tconst atPath = attr.atPath\n\t\t\t\tconst defaultValue = attr.defaultValue ?? defaults?.[attrName as keyof T]\n\n\t\t\t\tObject.defineProperty(this, attrName, {\n\t\t\t\t\tget(this: ViewModel) {\n\t\t\t\t\t\tlet signal = this._signals.get(attrName)\n\t\t\t\t\t\tif (!signal) {\n\t\t\t\t\t\t\tconst subscribable = liveEntityAt(currentThread, this.en, atPath)\n\t\t\t\t\t\t\tsignal = this._signalAdapter.createGetter(subscribable)\n\t\t\t\t\t\t\tthis._signals.set(attrName, signal)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst value = signal()\n\t\t\t\t\t\treturn value ?? (defaultValue as ApplogValue | undefined) ?? value\n\t\t\t\t\t},\n\t\t\t\t\tset(this: ViewModel, v: ApplogValue) {\n\t\t\t\t\t\tconst applog: ApplogForInsertOptionalAgent = { en: this.en, at: atPath, vl: v }\n\t\t\t\t\t\tpersistFn(this.thread, [applog])\n\t\t\t\t\t},\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// ── Applog tracking: applogs where vl === this.en ────────────\n\t\t\tconst targets = new Map<EntityID, Set<string>>()\n\n\t\t\tthis._targetApplogs = rollingFilter(currentThread, { vl: this.en })\n\t\t\tthis._targetMap = new SubscribableImpl(\n\t\t\t\ttargets,\n\t\t\t\t() => this._targetApplogs.subscribe((event) => {\n\t\t\t\t\tif (isInitEvent(event)) {\n\t\t\t\t\t\ttargets.clear()\n\t\t\t\t\t\tfor (const log of event.init) {\n\t\t\t\t\t\t\tlet ats = targets.get(log.en)\n\t\t\t\t\t\t\tif (!ats) {\n\t\t\t\t\t\t\t\tats = new Set()\n\t\t\t\t\t\t\t\ttargets.set(log.en, ats)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tats.add(log.at)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfor (const log of event.added) {\n\t\t\t\t\t\t\tlet ats = targets.get(log.en)\n\t\t\t\t\t\t\tif (!ats) {\n\t\t\t\t\t\t\t\tats = new Set()\n\t\t\t\t\t\t\t\ttargets.set(log.en, ats)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tats.add(log.at)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.removed) {\n\t\t\t\t\t\t\tfor (const log of event.removed) {\n\t\t\t\t\t\t\t\tconst ats = targets.get(log.en)\n\t\t\t\t\t\t\t\tif (ats) {\n\t\t\t\t\t\t\t\t\tats.delete(log.at)\n\t\t\t\t\t\t\t\t\tif (ats.size === 0) targets.delete(log.en)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis._targetMap._set(targets)\n\t\t\t\t}, 'derived'),\n\t\t\t)\n\t\t}\n\n\t\t/** Thread scoped to this entity */\n\t\tget entityThread(): Thread {\n\t\t\treturn rollingFilter(this.thread, { en: this.en })\n\t\t}\n\n\t\t/** Applogs in the current thread where vl === this.en */\n\t\tget targetApplogs(): Thread {\n\t\t\treturn this._targetApplogs\n\t\t}\n\n\t\t/** Set of entities that have an applog with this.en as the vl */\n\t\tget targettedBy(): Set<EntityID> {\n\t\t\treturn new Set(this._targetMap.value.keys())\n\t\t}\n\n\t\t/** Set of at strings from applogs where vl === this.en */\n\t\tget targettedVia(): Set<string> {\n\t\t\tconst via = new Set<string>()\n\t\t\tfor (const ats of this._targetMap.value.values()) {\n\t\t\t\tfor (const at of ats) via.add(at)\n\t\t\t}\n\t\t\treturn via\n\t\t}\n\n\t\t/** Whether this entity is soft-deleted */\n\t\tget isDeleted(): boolean {\n\t\t\tlet signal = this._signals.get('__isDeleted')\n\t\t\tif (!signal) {\n\t\t\t\tconst currentThread = makeCurrent(this.thread, options.makeCurrentThread)\n\t\t\t\tconst subscribable = liveEntityAt(currentThread, this.en, 'isDeleted')\n\t\t\t\tsignal = this._signalAdapter.createGetter(subscribable)\n\t\t\t\tthis._signals.set('__isDeleted', signal)\n\t\t\t}\n\t\t\treturn !!signal()\n\t\t}\n\n\t\t/** Soft-delete this entity */\n\t\tsetDeleted(thread = this.thread): void {\n\t\t\tconst applog: ApplogForInsertOptionalAgent = { en: this.en, at: 'isDeleted', vl: true }\n\t\t\tpersistFn(thread, [applog])\n\t\t}\n\n\t\t/** Get a builder for updating this entity */\n\t\tbuildUpdate(init: Partial<T> = {}): ObjectBuilder<T> {\n\t\t\treturn new ObjectBuilder<T>(init, this.en, entityPrefix)\n\t\t}\n\n\t\t/** Description for debugging */\n\t\tget description(): string {\n\t\t\treturn `${vmName}VM(en=${this.en})`\n\t\t}\n\n\t\t/** Get the full entity state as a plain object */\n\t\ttoJSON(): Partial<T> {\n\t\t\tconst result: Record<string, ApplogValue> = {}\n\t\t\tfor (const attr of attrDefs) {\n\t\t\t\tconst value = (this as any)[attr.name]\n\t\t\t\tif (value !== undefined && value !== null) {\n\t\t\t\t\tresult[attr.name] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result as Partial<T>\n\t\t}\n\t}\n\n\t// ── Static Methods ────────────────────────────────────────────\n\n\t/**\n\t * Get or create a VM instance for the given entity and thread.\n\t * Implements the singleton pattern — returns cached instance if one exists.\n\t */\n\tfunction getVMClass(en: EntityID, thread: Thread): InstanceType<typeof ViewModel> {\n\t\tif (!en || typeof en !== 'string') throw new Error(`[${vmName}VM.get] invalid en: ${en}`)\n\t\tif (!thread) throw new Error(`[${vmName}VM.get] no thread provided`)\n\n\t\tconst threadMap = getInstancesForThread(thread)\n\t\tlet entityMap = threadMap.get(vmName) as VMInstanceMap<InstanceType<typeof ViewModel>> | undefined\n\t\tif (!entityMap) {\n\t\t\tentityMap = new Map()\n\t\t\tthreadMap.set(vmName, entityMap)\n\t\t}\n\n\t\tconst existing = entityMap.get(en)\n\t\tif (existing) return existing\n\n\t\tconst vm = new ViewModel(en, thread, false) as InstanceType<typeof ViewModel>\n\t\tentityMap.set(en, vm)\n\t\treturn vm\n\t}\n\n\t/**\n\t * Create a builder for constructing a new entity.\n\t */\n\tfunction buildNewEntity(init: Partial<T> = {}, en?: EntityID): ObjectBuilder<T> {\n\t\tconst entityId = en ?? genId(init as any)\n\t\treturn ObjectBuilder.create<T>(init, entityId, entityPrefix)\n\t}\n\n\t// Attach static methods to the class\n\tconst VMClass: any = ViewModel\n\tVMClass.get = getVMClass\n\tVMClass.buildNew = buildNewEntity\n\tVMClass.vmName = vmName\n\tVMClass.entityPrefix = entityPrefix\n\n\treturn VMClass as unknown as {\n\t\tnew(\n\t\t\ten: EntityID,\n\t\t\tthread: Thread,\n\t\t\tskipInit?: boolean,\n\t\t): IVMInstance<T> & T\n\n\t\t/** Get or create a VM instance for the given entity ID */\n\t\tget(en: EntityID, thread: Thread): IVMInstance<T> & T\n\n\t\t/** Create a builder for a new entity */\n\t\tbuildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>\n\n\t\t/** The VM name (for debugging) */\n\t\treadonly vmName: string\n\n\t\t/** The entity prefix used for at-paths */\n\t\treadonly entityPrefix: string\n\t}\n}\n","import type { ApplogValue } from '../applog/datom-types.ts'\nimport type { ISchemaAdapter, VMAttributeDef } from './types.ts'\n\n/**\n * Helper to create a simple schema adapter from a plain attribute list.\n * Useful for quick VM definitions or when you don't have a formal schema library.\n */\nexport function createAdapterFromAttributes<T extends Record<string, ApplogValue>>(\n\tconfig: {\n\t\tattributes: VMAttributeDef[]\n\t\tentityPrefix: string\n\t\tdefaults?: Partial<T>\n\t},\n): ISchemaAdapter<T> {\n\treturn {\n\t\tgetAttributeDefs: () => config.attributes,\n\t\tgetDefaults: () => (config.defaults ?? {}) as Partial<T>,\n\t\tgetEntityPrefix: () => config.entityPrefix,\n\t}\n}\n\n/**\n * Helper to build at-paths from attribute names and an entity prefix.\n */\nexport function buildAtPath(entityPrefix: string, attrName: string): string {\n\treturn `${entityPrefix}/${attrName}`\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/viewmodel/builder.ts","../../src/viewmodel/types.ts","../../src/viewmodel/factory.ts","../../src/viewmodel/schema-adapter.ts"],"sourcesContent":["import type { ApplogForInsertOptionalAgent, ApplogValue, EntityID } from '../applog/datom-types.ts'\n\n/**\n * Generic ObjectBuilder for type-safe entity creation and updates.\n *\n * Provides a fluent API to build a set of applogs for an entity.\n * Modeled after the note3 builder pattern.\n *\n * Usage:\n * ```ts\n * ObjectBuilder.create({ name: 'foo' })\n * .update({ type: 'bar' })\n * .build(thread)\n * ```\n */\nexport class ObjectBuilder<\n\tTARGET extends Record<string, ApplogValue> = Record<string, ApplogValue>,\n> {\n\tconstructor(\n\t\tprivate _data: Partial<TARGET>,\n\t\tpublic en: EntityID | null = null,\n\t\tprivate _atPrefix: string | null = null,\n\t\tprivate _atOverrides: Partial<Record<keyof TARGET, string>> = {},\n\t) {}\n\n\t/**\n\t * Create a new builder with initial data.\n\t */\n\tstatic create<TARGET extends Record<string, ApplogValue>>(\n\t\tinit: Partial<TARGET> = {},\n\t\ten?: EntityID,\n\t\tatPrefix?: string,\n\t\tatOverrides?: Partial<Record<keyof TARGET, string>>,\n\t): ObjectBuilder<TARGET> {\n\t\treturn new ObjectBuilder<TARGET>(init, en, atPrefix, atOverrides)\n\t}\n\n\t/**\n\t * Update the data being built.\n\t */\n\tupdate(updateObj: Partial<TARGET>): this {\n\t\tObject.assign(this._data, updateObj)\n\t\treturn this\n\t}\n\n\t/**\n\t * Build the applogs for this entity, optionally resolving against a thread.\n\t */\n\tbuild(thread?: {\n\t\tinsert(applogs: ApplogForInsertOptionalAgent[]): ApplogForInsertOptionalAgent[]\n\t}): ApplogForInsertOptionalAgent[] {\n\t\tconst atPrefix = this._atPrefix\n\t\tconst applogs: ApplogForInsertOptionalAgent[] = []\n\n\t\tfor (const [key, value] of Object.entries(this._data) as [keyof TARGET, ApplogValue][]) {\n\t\t\tif (value === undefined) continue\n\t\t\tconst atOverride = this._atOverrides[key]\n\t\t\tconst at = atOverride ?? (atPrefix ? `${atPrefix}/${String(key)}` : String(key))\n\t\t\tapplogs.push({ en: this.en!, at, vl: value })\n\t\t}\n\n\t\treturn applogs\n\t}\n\n\t/**\n\t * Get the raw data being built.\n\t */\n\tget data(): Partial<TARGET> {\n\t\treturn { ...this._data }\n\t}\n}\n","import type { ApplogValue, EntityID } from '../applog/datom-types.ts'\nimport type { Thread } from '../thread/basic.ts'\nimport type { ObjectBuilder } from './builder.ts'\n\n// ═══════════════════════════════════════════════════════════════\n// Attribute Definition\n// ═══════════════════════════════════════════════════════════════\n\nexport interface VMAttributeDef {\n\t/** Attribute name (short: e.g. 'name', 'content') */\n\tname: string\n\t/** The wovin at-path used in applogs (e.g. 'block/content', 'speaker/name') */\n\tatPath: string\n\t/** Whether the attribute is optional */\n\toptional: boolean\n\t/** Default value if not present in applogs */\n\tdefaultValue?: ApplogValue\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Schema Adapter Interface\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * A schema adapter converts a type-system schema (TypeBox, Zod, ArkType, Typia)\n * into a uniform representation that the VM factory can consume.\n */\nexport interface ISchemaAdapter<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\t/** Get the attribute definitions for this schema */\n\tgetAttributeDefs(): VMAttributeDef[]\n\n\t/** Get default values for all attributes */\n\tgetDefaults(): Partial<T>\n\n\t/** Optional: runtime validator */\n\tcreateValidator?(): (value: unknown) => value is T\n\n\t/** The entity prefix used for wovin at-paths (e.g. 'block', 'speaker') */\n\tgetEntityPrefix(): string\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Factory Options\n// ═══════════════════════════════════════════════════════════════\n\nexport interface ViewModelFactoryOptions<T extends Record<string, ApplogValue>> {\n\t/** The schema adapter for this entity type */\n\tadapter: ISchemaAdapter<T>\n\n\t/** How to get the default thread context */\n\tgetDefaultThread?: () => Thread\n\n\t/** EntityID generation length (default: 7) */\n\tentityIdLength?: number\n\n\t/** Function to generate entity IDs */\n\tgenerateEntityId?: (data: Partial<T> & { ts?: string }) => EntityID\n\n\t/** Override the entity prefix for at-paths */\n\tentityPrefix?: string\n\n\t/** Optional function to make thread current-state (lastWriteWins) */\n\tmakeCurrentThread?: (thread: Thread) => Thread\n\n\t/** Name for the VM type (used in logging and debugging) */\n\tvmName?: string\n\n\t/**\n\t * Signal adapter for framework-specific reactivity.\n\t * Default: non-reactive (direct Subscribable reads).\n\t * For SolidJS, pass createSolidSignalAdapter() from @wovin/ui-solid/viewmodel.\n\t */\n\tsignalAdapter?: ISignalAdapter\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Instance Interface\n// ═══════════════════════════════════════════════════════════════\n\nexport interface IVMInstance<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\treadonly en: EntityID\n\treadonly thread: Thread\n\n\t/** Get a builder for updating this entity */\n\tbuildUpdate(init?: Partial<T>): ObjectBuilder<T>\n\n\t/** Soft-delete this entity */\n\tsetDeleted(thread?: Thread): void\n\n\t/** Whether this entity is deleted */\n\treadonly isDeleted: boolean\n\n\t/** The entity thread filtered to this entity */\n\treadonly entityThread: Thread\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Class Interface (what the factory returns)\n// ═══════════════════════════════════════════════════════════════\n\nexport interface IVMClass<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\tnew(en: EntityID, thread: Thread, ...args: any[]): IVMInstance<T>\n\n\t/** Get or create a VM instance for the given entity ID and thread */\n\tget(en: EntityID, thread: Thread): IVMInstance<T>\n\n\t/** Create a new entity builder */\n\tbuildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Signal Adapter Interface (framework-specific reactivity)\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * A signal adapter abstracts how the VM creates reactive signals for each attribute.\n * The core factory uses a default (non-reactive) implementation.\n * Framework-specific extensions (Solid, Vue, etc.) override this.\n */\nexport interface ISignalAdapter {\n\t/**\n\t * Create a reactive getter for a subscribable value.\n\t * Returns a function that returns the current value.\n\t * In the default implementation, this is just () => subscribable.value.\n\t */\n\tcreateGetter<T>(subscribable: { value: T; subscribe(cb: () => void): () => void }): () => T\n\n\t/**\n\t * Create a writable signal for an attribute.\n\t * Returns [get, set] pair.\n\t * Default implementation stores and returns value directly.\n\t */\n\tcreateWritable<T>(initial: T): [() => T, (v: T) => void]\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Shared state maps\n// ═══════════════════════════════════════════════════════════════\n\nexport type VMInstanceMap<T> = Map<EntityID, T>\n\n/** WeakMap from Thread → Map<VMName, Map<EntityID, VMInstance>> */\nexport const GlobalVMInstances = new WeakMap<Thread, Map<string, Map<EntityID, unknown>>>()\n\nexport function getInstancesForThread(thread: Thread): Map<string, Map<EntityID, unknown>> {\n\tlet threadMap = GlobalVMInstances.get(thread)\n\tif (!threadMap) {\n\t\tthreadMap = new Map()\n\t\tGlobalVMInstances.set(thread, threadMap)\n\t}\n\treturn threadMap\n}\n","import type { ApplogForInsertOptionalAgent, ApplogValue, EntityID } from '../applog/datom-types.ts'\nimport { EntityID_LENGTH } from '../applog/datom-types.ts'\nimport { dateNowIso, mintNewEntity } from '../applog/applog-utils.ts'\nimport { ensureTsPvAndFinalizeApplog } from '../applog/applog-helpers.ts'\nimport { lastWriteWins, liveEntityAt } from '../query/basic.ts'\nimport { SubscribableImpl } from '../query/subscribable.ts'\nimport type { Thread } from '../thread/basic.ts'\nimport { isInitEvent } from '../thread/basic.ts'\nimport { rollingFilter } from '../thread/filters.ts'\nimport { ObjectBuilder } from './builder.ts'\nimport {\n\ttype ISignalAdapter,\n\ttype IVMInstance,\n\ttype ViewModelFactoryOptions,\n\ttype VMInstanceMap,\n\tgetInstancesForThread,\n} from './types.ts'\n\n// ═══════════════════════════════════════════════════════════════\n// Default (non-reactive) Signal Adapter\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Default signal adapter — reads Subscribable values directly (snapshot).\n * Subscribe-once to keep .value current. No framework reactivity.\n */\nexport const DefaultSignalAdapter: ISignalAdapter = {\n\tcreateGetter<T>(subscribable: { value: T; subscribe(cb: () => void): () => void }): () => T {\n\t\t// Subscribe once to activate upstream (lazy activation)\n\t\tsubscribable.subscribe(() => {})\n\t\treturn () => subscribable.value\n\t},\n\tcreateWritable<T>(initial: T): [() => T, (v: T) => void] {\n\t\tlet current = initial\n\t\treturn [() => current, (v: T) => { current = v }]\n\t},\n}\n\n// ═══════════════════════════════════════════════════════════════\n// makeCurrentThread - convenience wrapper\n// ═══════════════════════════════════════════════════════════════\n\nfunction makeCurrent(thread: Thread, fn?: ViewModelFactoryOptions<any>['makeCurrentThread']): Thread {\n\tif (fn) return fn(thread)\n\treturn lastWriteWins(thread, { tolerateAlreadyFiltered: true })\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Default persist function\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Default applog persistence: finalize applogs and push into thread.\n * Works with WriteableThread (which has insertRaw).\n */\nfunction defaultPersistApplogs(thread: Thread, applogs: ApplogForInsertOptionalAgent[]): void {\n\tconst finalized = applogs.map(log => ensureTsPvAndFinalizeApplog(log as any, thread))\n\tconst writable = thread as any\n\tif (typeof writable.insertRaw === 'function') {\n\t\twritable.insertRaw(finalized)\n\t} else if (typeof writable.insert === 'function') {\n\t\twritable.insert(applogs)\n\t} else {\n\t\t// Read-only thread — just finalize but don't persist\n\t\t// This is fine for optimistic UI or read-only scenarios\n\t}\n}\n\n// ═══════════════════════════════════════════════════════════════\n// createViewModelFactory - THE MAIN FACTORY\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Create a ViewModel factory for a given schema.\n *\n * Returns a base class that can be extended or used directly.\n * The class provides:\n * - Static `get(en, thread)` for singleton access\n * - Static `buildNew(init, en)` for creating entities\n * - Instance `buildUpdate(init)` for updating entities\n * - Lazy reactive property accessors per attribute\n * - `setDeleted()` for soft deletion\n *\n * Framework-specific reactivity (Solid, Vue, etc.) can be added by\n * providing a custom `ISignalAdapter` in the options.\n *\n * @param options - Factory configuration\n * @returns A VM class constructor with static methods\n */\nexport function createViewModelFactory<T extends Record<string, ApplogValue>>(\n\toptions: ViewModelFactoryOptions<T>,\n) {\n\tconst {\n\t\tadapter,\n\t\tentityPrefix = adapter.getEntityPrefix(),\n\t\tentityIdLength = EntityID_LENGTH,\n\t\tgenerateEntityId,\n\t\tvmName = entityPrefix,\n\t\tsignalAdapter = DefaultSignalAdapter,\n\t} = options\n\n\tconst attrDefs = adapter.getAttributeDefs()\n\tconst defaults = adapter.getDefaults()\n\n\t// Entity ID generation\n\tconst genId = generateEntityId ?? ((data: Partial<T> & { ts?: string }) =>\n\t\tmintNewEntity({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength) as EntityID)\n\n\t// Persist function (can be overridden by extending class)\n\tconst persistFn = (thread: Thread, applogs: ApplogForInsertOptionalAgent[]) => {\n\t\tdefaultPersistApplogs(thread, applogs)\n\t}\n\n\t// ═══════════════════════════════════════════════════════════════\n\t// The ViewModel Class\n\t// ═══════════════════════════════════════════════════════════════\n\tclass ViewModel {\n\t\t/** Per-instance signal storage */\n\t\t_signals = new Map<string, () => ApplogValue>()\n\t\t_signalAdapter: ISignalAdapter = signalAdapter\n\n\t\t/** Reactive map for applog tracking: vl === this.en */\n\t\t_targetMap: SubscribableImpl<Map<EntityID, Set<string>>>\n\n\t\t/** Applogs in the current thread where vl === this.en */\n\t\t_targetApplogs: Thread\n\n\t\tconstructor(\n\t\t\tpublic en: EntityID,\n\t\t\tpublic thread: Thread,\n\t\t\tskipInit?: boolean,\n\t\t) {\n\t\t\tif (skipInit) return\n\n\t\t\tconst currentThread = makeCurrent(thread, options.makeCurrentThread)\n\n\t\t\t// Define lazy getters/setters for each attribute\n\t\t\tfor (const attr of attrDefs) {\n\t\t\t\tif (attr.name === 'en') continue\n\n\t\t\t\tconst attrName = attr.name\n\t\t\t\tconst atPath = attr.atPath\n\t\t\t\tconst defaultValue = attr.defaultValue ?? defaults?.[attrName as keyof T]\n\n\t\t\t\tObject.defineProperty(this, attrName, {\n\t\t\t\t\tget(this: ViewModel) {\n\t\t\t\t\t\tlet signal = this._signals.get(attrName)\n\t\t\t\t\t\tif (!signal) {\n\t\t\t\t\t\t\tconst subscribable = liveEntityAt(currentThread, this.en, atPath)\n\t\t\t\t\t\t\tsignal = this._signalAdapter.createGetter(subscribable)\n\t\t\t\t\t\t\tthis._signals.set(attrName, signal)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst value = signal()\n\t\t\t\t\t\treturn value ?? (defaultValue as ApplogValue | undefined) ?? value\n\t\t\t\t\t},\n\t\t\t\t\tset(this: ViewModel, v: ApplogValue) {\n\t\t\t\t\t\tconst applog: ApplogForInsertOptionalAgent = { en: this.en, at: atPath, vl: v }\n\t\t\t\t\t\tpersistFn(this.thread, [applog])\n\t\t\t\t\t},\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// ── Applog tracking: applogs where vl === this.en ────────────\n\t\t\tconst targets = new Map<EntityID, Set<string>>()\n\n\t\t\tthis._targetApplogs = rollingFilter(currentThread, { vl: this.en })\n\t\t\tthis._targetMap = new SubscribableImpl(\n\t\t\t\ttargets,\n\t\t\t\t() => this._targetApplogs.subscribe((event) => {\n\t\t\t\t\tif (isInitEvent(event)) {\n\t\t\t\t\t\ttargets.clear()\n\t\t\t\t\t\tfor (const log of event.init) {\n\t\t\t\t\t\t\tlet ats = targets.get(log.en)\n\t\t\t\t\t\t\tif (!ats) {\n\t\t\t\t\t\t\t\tats = new Set()\n\t\t\t\t\t\t\t\ttargets.set(log.en, ats)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tats.add(log.at)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfor (const log of event.added) {\n\t\t\t\t\t\t\tlet ats = targets.get(log.en)\n\t\t\t\t\t\t\tif (!ats) {\n\t\t\t\t\t\t\t\tats = new Set()\n\t\t\t\t\t\t\t\ttargets.set(log.en, ats)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tats.add(log.at)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.removed) {\n\t\t\t\t\t\t\tfor (const log of event.removed) {\n\t\t\t\t\t\t\t\tconst ats = targets.get(log.en)\n\t\t\t\t\t\t\t\tif (ats) {\n\t\t\t\t\t\t\t\t\tats.delete(log.at)\n\t\t\t\t\t\t\t\t\tif (ats.size === 0) targets.delete(log.en)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis._targetMap._set(targets)\n\t\t\t\t}, 'derived'),\n\t\t\t)\n\t\t}\n\n\t\t/** Thread scoped to this entity */\n\t\tget entityThread(): Thread {\n\t\t\treturn rollingFilter(this.thread, { en: this.en })\n\t\t}\n\n\t\t/** Applogs in the current thread where vl === this.en */\n\t\tget targetApplogs(): Thread {\n\t\t\treturn this._targetApplogs\n\t\t}\n\n\t\t/** Set of entities that have an applog with this.en as the vl */\n\t\tget targettedBy(): Set<EntityID> {\n\t\t\treturn new Set(this._targetMap.value.keys())\n\t\t}\n\n\t\t/** Set of at strings from applogs where vl === this.en */\n\t\tget targettedVia(): Set<string> {\n\t\t\tconst via = new Set<string>()\n\t\t\tfor (const ats of this._targetMap.value.values()) {\n\t\t\t\tfor (const at of ats) via.add(at)\n\t\t\t}\n\t\t\treturn via\n\t\t}\n\n\t\t/** Whether this entity is soft-deleted */\n\t\tget isDeleted(): boolean {\n\t\t\tlet signal = this._signals.get('__isDeleted')\n\t\t\tif (!signal) {\n\t\t\t\tconst currentThread = makeCurrent(this.thread, options.makeCurrentThread)\n\t\t\t\tconst subscribable = liveEntityAt(currentThread, this.en, 'isDeleted')\n\t\t\t\tsignal = this._signalAdapter.createGetter(subscribable)\n\t\t\t\tthis._signals.set('__isDeleted', signal)\n\t\t\t}\n\t\t\treturn !!signal()\n\t\t}\n\n\t\t/** Soft-delete this entity */\n\t\tsetDeleted(thread = this.thread): void {\n\t\t\tconst applog: ApplogForInsertOptionalAgent = { en: this.en, at: 'isDeleted', vl: true }\n\t\t\tpersistFn(thread, [applog])\n\t\t}\n\n\t\t/** Get a builder for updating this entity */\n\t\tbuildUpdate(init: Partial<T> = {}): ObjectBuilder<T> {\n\t\t\treturn new ObjectBuilder<T>(init, this.en, entityPrefix)\n\t\t}\n\n\t\t/** Description for debugging */\n\t\tget description(): string {\n\t\t\treturn `${vmName}VM(en=${this.en})`\n\t\t}\n\n\t\t/** Get the full entity state as a plain object */\n\t\ttoJSON(): Partial<T> {\n\t\t\tconst result: Record<string, ApplogValue> = {}\n\t\t\tfor (const attr of attrDefs) {\n\t\t\t\tconst value = (this as any)[attr.name]\n\t\t\t\tif (value !== undefined && value !== null) {\n\t\t\t\t\tresult[attr.name] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result as Partial<T>\n\t\t}\n\t}\n\n\t// ── Static Methods ────────────────────────────────────────────\n\n\t/**\n\t * Get or create a VM instance for the given entity and thread.\n\t * Implements the singleton pattern — returns cached instance if one exists.\n\t */\n\tfunction getVMClass(en: EntityID, thread: Thread): InstanceType<typeof ViewModel> {\n\t\tif (!en || typeof en !== 'string') throw new Error(`[${vmName}VM.get] invalid en: ${en}`)\n\t\tif (!thread) throw new Error(`[${vmName}VM.get] no thread provided`)\n\n\t\tconst threadMap = getInstancesForThread(thread)\n\t\tlet entityMap = threadMap.get(vmName) as VMInstanceMap<InstanceType<typeof ViewModel>> | undefined\n\t\tif (!entityMap) {\n\t\t\tentityMap = new Map()\n\t\t\tthreadMap.set(vmName, entityMap)\n\t\t}\n\n\t\tconst existing = entityMap.get(en)\n\t\tif (existing) return existing\n\n\t\tconst vm = new ViewModel(en, thread, false) as InstanceType<typeof ViewModel>\n\t\tentityMap.set(en, vm)\n\t\treturn vm\n\t}\n\n\t/**\n\t * Create a builder for constructing a new entity.\n\t */\n\tfunction buildNewEntity(init: Partial<T> = {}, en?: EntityID): ObjectBuilder<T> {\n\t\tconst entityId = en ?? genId(init as any)\n\t\treturn ObjectBuilder.create<T>(init, entityId, entityPrefix)\n\t}\n\n\t// Attach static methods to the class\n\tconst VMClass: any = ViewModel\n\tVMClass.get = getVMClass\n\tVMClass.buildNew = buildNewEntity\n\tVMClass.vmName = vmName\n\tVMClass.entityPrefix = entityPrefix\n\n\treturn VMClass as unknown as {\n\t\tnew(\n\t\t\ten: EntityID,\n\t\t\tthread: Thread,\n\t\t\tskipInit?: boolean,\n\t\t): IVMInstance<T> & T\n\n\t\t/** Get or create a VM instance for the given entity ID */\n\t\tget(en: EntityID, thread: Thread): IVMInstance<T> & T\n\n\t\t/** Create a builder for a new entity */\n\t\tbuildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>\n\n\t\t/** The VM name (for debugging) */\n\t\treadonly vmName: string\n\n\t\t/** The entity prefix used for at-paths */\n\t\treadonly entityPrefix: string\n\t}\n}\n","import type { ApplogValue } from '../applog/datom-types.ts'\nimport type { ISchemaAdapter, VMAttributeDef } from './types.ts'\n\n/**\n * Helper to create a simple schema adapter from a plain attribute list.\n * Useful for quick VM definitions or when you don't have a formal schema library.\n */\nexport function createAdapterFromAttributes<T extends Record<string, ApplogValue>>(\n\tconfig: {\n\t\tattributes: VMAttributeDef[]\n\t\tentityPrefix: string\n\t\tdefaults?: Partial<T>\n\t},\n): ISchemaAdapter<T> {\n\treturn {\n\t\tgetAttributeDefs: () => config.attributes,\n\t\tgetDefaults: () => (config.defaults ?? {}) as Partial<T>,\n\t\tgetEntityPrefix: () => config.entityPrefix,\n\t}\n}\n\n/**\n * Helper to build at-paths from attribute names and an entity prefix.\n */\nexport function buildAtPath(entityPrefix: string, attrName: string): string {\n\treturn `${entityPrefix}/${attrName}`\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wovin/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import type { Applog } from './datom-types.ts'
3
- import { compareApplogsByTs, isLaterByTsAndPv, sortApplogsByTs } from './applog-utils.ts'
3
+ import { compareApplogsByTs, deriveDeterministicEntityID, isLaterByTsAndPv, mintNewEntity, sortApplogsByTs } from './applog-utils.ts'
4
4
 
5
5
  const mkLog = (overrides: Partial<Applog>): Applog => ({
6
6
  cid: 'cid-default' as any,
@@ -106,3 +106,45 @@ describe('isLaterByTsAndPv', () => {
106
106
  expect(isLaterByTsAndPv(b, a)).toBe('other'.localeCompare('shared') > 0)
107
107
  })
108
108
  })
109
+
110
+ describe('deriveDeterministicEntityID — natural-key identity', () => {
111
+ it('is deterministic: same key ⇒ same id (write & read converge on one soul)', () => {
112
+ const a = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 })
113
+ const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 })
114
+ expect(a).toBe(b)
115
+ })
116
+
117
+ it('is stable across key property order (safe-stable-stringify)', () => {
118
+ const a = deriveDeterministicEntityID({ mediaId: 'm1', type: 'segment' })
119
+ const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1' })
120
+ expect(a).toBe(b)
121
+ })
122
+
123
+ it('different keys ⇒ different ids', () => {
124
+ expect(deriveDeterministicEntityID('#rust')).not.toBe(deriveDeterministicEntityID('#typescript'))
125
+ })
126
+
127
+ it('honors the requested length', () => {
128
+ expect(deriveDeterministicEntityID('#rust', 7)).toHaveLength(7)
129
+ expect(deriveDeterministicEntityID('#rust', 10)).toHaveLength(10)
130
+ })
131
+ })
132
+
133
+ describe('mintNewEntity — occurrence identity', () => {
134
+ it('re-minting the same logical seed with a fresh ts yields a DIFFERENT id (the re-mint trap)', () => {
135
+ // Documents why an existing entity must be resolved & its `en` reused rather than
136
+ // re-minted: a timestamp in the seed forks a new soul on every call.
137
+ const first = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:00.000Z' })
138
+ const editedLater = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:01.000Z' })
139
+ expect(first).not.toBe(editedLater)
140
+ })
141
+
142
+ it('is deterministic for an identical seed (same ts ⇒ same id)', () => {
143
+ const seed = { msgId: 42, ts: '2026-06-16T00:00:00.000Z' }
144
+ expect(mintNewEntity(seed)).toBe(mintNewEntity({ ...seed }))
145
+ })
146
+
147
+ it('honors the requested length', () => {
148
+ expect(mintNewEntity({ a: 1 }, 6)).toHaveLength(6)
149
+ })
150
+ })
@@ -255,7 +255,35 @@ export const removeDuplicateAppLogs = (appLogArray: Applog[], mode?: RemoveDupli
255
255
  // return Array.from(logMap.values())
256
256
  // }
257
257
 
258
- export const getHashID = (stringifiable: any, lngth = 8) => cyrb53hash(stringify(stringifiable), 31, lngth) as string
258
+ /** Low-level: stable-stringify + cyrb53 hex entity id of `lngth` chars. Prefer the named wrappers below. */
259
+ const hashToEntityId = (stringifiable: any, lngth = 8) => cyrb53hash(stringify(stringifiable), 31, lngth) as string
260
+
261
+ /**
262
+ * Mint a brand-new entity id — coin a fresh "soul" for a genuinely new occurrence.
263
+ *
264
+ * The hashed `seed` MUST include something that makes this occurrence unique (a timestamp
265
+ * and/or random nonce), otherwise two distinct creations collide on one id.
266
+ *
267
+ * ⚠️ NEVER call this to re-reference an entity that already exists: doing so mints a *new*
268
+ * soul and forks a duplicate (breaking pv / lastWriteWins chains). To reference an existing
269
+ * entity, look it up and reuse its `en`. For identity that is *defined by* a stable key, use
270
+ * {@link deriveDeterministicEntityID}. See docs/architecture.md#entity-identity-soul.
271
+ */
272
+ export const mintNewEntity = (seed: any, lngth = 8) => hashToEntityId(seed, lngth)
273
+
274
+ /**
275
+ * Derive a deterministic entity id from a stable, identity-defining key.
276
+ *
277
+ * Same key ⇒ same `en`, so all references converge on one soul — used for both writing and
278
+ * reading the same entity (singleton / natural-key entities, e.g. a tag, a per-agent setting,
279
+ * a media segment).
280
+ *
281
+ * ⚠️ Use with care: two users/agents using the same name/key for something does NOT necessarily
282
+ * mean they mean the same thing/entity/soul. Only use when the key TRULY defines identity. The
283
+ * key must be canonicalized — diverging formats fork the soul and silently break lookups.
284
+ * See docs/architecture.md#entity-identity-soul.
285
+ */
286
+ export const deriveDeterministicEntityID = (stableKey: any, lngth = 8) => hashToEntityId(stableKey, lngth)
259
287
 
260
288
  export function isVariable(x: any): x is string {
261
289
  return typeof x === 'string' && x.startsWith('?')
@@ -324,21 +352,16 @@ export function matchPartStatic(field: keyof Applog, patternPart: ValueOrMatcher
324
352
  result = patternPart === atomPart // shortcut for most common use-case
325
353
  } else if (typ === 'function') {
326
354
  result = (patternPart as Function)(atomPart)
327
- } else if (Array.isArray(patternPart)) {
328
- // A bare array is ambiguous now that `vl` may itself be an array value: it could
329
- // mean set-membership or a literal-array match. Fail loudly rather than match wrong.
330
- throw ERROR(
331
- `[matchPartStatic] a bare array is not a valid matcher for field '${field}'.`
332
- + ` Use anyOf(...) for set-membership, or a predicate (v) => isEqual(v, [...]) to match a literal array value.`,
333
- patternPart,
334
- )
335
- } else if (typeof (patternPart as any).has === 'function') {
336
- result = (patternPart as Set<any>).has(atomPart)
355
+ } else if (patternPart instanceof Set) {
356
+ // A Set means set-membership (use anyOf(...) to build one). Bare arrays/objects are
357
+ // NOT membership they are literal values compared by deep equality (see else).
358
+ result = patternPart.has(atomPart)
337
359
  } // if (field === 'at' && typ === 'string' && patternPart.endsWith('*')) {
338
360
  // return typeof atomPart === 'string' && atomPart.startsWith(patternPart.slice(0, -1))
339
361
  // }
340
362
  else {
341
- // object/array values compare by deep equality; primitives via isEqual's === fast-path
363
+ // object/array values (and primitives) compare by deep equality;
364
+ // isEqual short-circuits on === so primitives stay on the cheap path
342
365
  result = valueEq(patternPart as ApplogValue, atomPart)
343
366
  }
344
367
  } else {
@@ -368,15 +391,9 @@ export function matchPart(patternPart: ValueOrMatcher<ApplogValue>, atomPart: Ap
368
391
  if (typeof patternPart === 'function') {
369
392
  return patternPart(atomPart) ? context : null
370
393
  }
371
- if (Array.isArray(patternPart)) {
372
- throw ERROR(
373
- `[matchPart] a bare array is not a valid matcher.`
374
- + ` Use anyOf(...) for set-membership, or a predicate to match a literal array value.`,
375
- patternPart,
376
- )
377
- }
378
- if (patternPart && typeof (patternPart as any).has === 'function') {
379
- return (patternPart as Set<any>).has(atomPart) ? context : null
394
+ if (patternPart instanceof Set) {
395
+ // set-membership (use anyOf(...)); bare arrays/objects are literal deep-equality matches below
396
+ return patternPart.has(atomPart) ? context : null
380
397
  }
381
398
  return valueEq(patternPart as ApplogValue, atomPart) ? context : null
382
399
  }