@almadar/workspace 0.1.6 → 0.2.1

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.
@@ -44,6 +44,8 @@ export declare function compiledDir(workDir: string): string;
44
44
  export declare function compiledFile(workDir: string, relPath: string): string;
45
45
  /** App marker — used internally by the resolver/factory. */
46
46
  export declare function appMarkerFile(workDir: string): string;
47
+ /** Workspace-level index manifest path. */
48
+ export declare function workspaceIndexManifestFile(workDir: string): string;
47
49
  /**
48
50
  * Resolve a user-supplied relative path within the sandbox. Rejects
49
51
  * absolute paths, `..` traversal, and any path that resolves outside
package/dist/types.d.ts CHANGED
@@ -93,6 +93,9 @@ export type WorkspaceWriteEvent = {
93
93
  kind: 'workspace-index';
94
94
  orbital: string;
95
95
  content: JsonObject;
96
+ } | {
97
+ kind: 'workspace-index-manifest';
98
+ content: JsonObject;
96
99
  };
97
100
  /**
98
101
  * Caller-supplied backend used when the local cache misses but the
@@ -0,0 +1,21 @@
1
+ /**
2
+ * BM25 sparse retrieval — Phase B of the workspace-index.
3
+ *
4
+ * Tokenizes per-orbital spec.json into the term list the table indexes,
5
+ * builds a workspace-level inverted index, and scores free-form queries
6
+ * against it. Pure functions; no I/O.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import type { JsonObject } from '@almadar/core';
11
+ import type { BM25Table, BM25Options } from './types.js';
12
+ /** Tokenize one spec.json into the set of terms BM25 indexes. */
13
+ export declare function tokenizeSpec(spec: JsonObject): readonly string[];
14
+ /** Build the workspace BM25 table from per-orbital token lists. */
15
+ export declare function buildBM25Table(tokensPerOrbital: Readonly<Record<string, readonly string[]>>): BM25Table;
16
+ /** Score every orbital in `table` against a free-form query string. */
17
+ export declare function queryBM25(table: BM25Table, query: string, opts?: BM25Options): readonly {
18
+ orbital: string;
19
+ score: number;
20
+ matchedTokens: readonly string[];
21
+ }[];
@@ -52,3 +52,28 @@ export declare function deriveExtraTraitAlias(emit: {
52
52
  ref: string;
53
53
  name?: string;
54
54
  }): string;
55
+ /**
56
+ * Compose the orbital CONTENT fingerprint from a spec — the richer
57
+ * text the retrieval system embeds for prompt narrowing.
58
+ *
59
+ * Starts from the identity fingerprint then folds in every
60
+ * user-meaningful signal the spec carries:
61
+ * - traitOverrides keys (the trait names being customized)
62
+ * - traitOverrides.*.config.* values (the actual customizations
63
+ * — discount percentages, labels, button text, chart types, etc.)
64
+ * - entityFields[].name (the user's domain vocabulary)
65
+ * - extraTraits[].name + tail-of-ref (added capabilities)
66
+ * - ruleOverlay.rules[].capability values (compliance/audit signals)
67
+ *
68
+ * Order matters for token-position effects: identity first (so the
69
+ * embedder weights it heaviest), then the config values (the "what
70
+ * does it DO" signal), then entity + extra + rule tags.
71
+ *
72
+ * Per the design doc this is intentionally not catalog-filtered by
73
+ * `@tier === 'domain'` — that would require an @almadar/std dep from
74
+ * inside the workspace package. If retrieval-quality eval shows
75
+ * noise from internal-tier knobs polluting the fingerprint, the
76
+ * fix lands in a follow-up that brings the catalog metadata into
77
+ * reach (see plan B-Wave 0 notes).
78
+ */
79
+ export declare function composeOrbitalContentFingerprint(spec: JsonObject): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Graph builders — Phase C of the workspace-index.
3
+ *
4
+ * Pure walkers over typed inputs (spec.json batches, composed
5
+ * schema.orb text, params-history rows) that produce the hash-lookup
6
+ * maps the public Phase C surface serves.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import type { JsonObject } from '@almadar/core';
11
+ import type { ComposedMaps, IntentMaps, RecencyEntry } from './types.js';
12
+ /** Per-orbital spec.json walker — derives the four intent maps. */
13
+ export declare function buildIntentMaps(specs: Readonly<Record<string, JsonObject>>): IntentMaps;
14
+ /** Composed schema.orb parser — derives event_name + composed entity_name maps. */
15
+ export declare function buildComposedMaps(schemaOrbContent: string | null): ComposedMaps;
16
+ /** Per-orbital params-history.jsonl tail walker — derives the recency map. */
17
+ export declare function buildRecencyMap(history: Readonly<Record<string, readonly JsonObject[]>>): Readonly<Record<string, RecencyEntry>>;
@@ -9,7 +9,7 @@
9
9
  import type { JsonObject } from '@almadar/core';
10
10
  import type { WorkspaceBackend } from '../internal/types.js';
11
11
  import type { SinkManager } from '../internal/sink-manager.js';
12
- import type { EmbedderPort, ResolveOptions, ResolveResult, TraitRefEmit, WorkspaceIndex, WorkspaceIndexStats } from './types.js';
12
+ import type { EmbedderPort, EntityBinding, EventEdge, RecentlyEditedOptions, ResolveOptions, ResolveResult, RetrievalOptions, RetrievalResult, RuleBinding, TraitRefEmit, WorkspaceIndex, WorkspaceIndexStats } from './types.js';
13
13
  export interface WorkspaceIndexDeps {
14
14
  workDir: string;
15
15
  backend: WorkspaceBackend;
@@ -17,6 +17,10 @@ export interface WorkspaceIndexDeps {
17
17
  embedder: EmbedderPort;
18
18
  listOrbitals: () => string[];
19
19
  readSpec: (orbital: string) => JsonObject | null;
20
+ /** Composed schema.orb content; null when missing or unparseable. */
21
+ readSchema: () => string | null;
22
+ /** Tail of params-history.jsonl rows for an orbital; empty array when none. */
23
+ readHistory: (orbital: string) => readonly JsonObject[];
20
24
  }
21
25
  export declare class WorkspaceIndexImpl implements WorkspaceIndex {
22
26
  private readonly deps;
@@ -25,13 +29,39 @@ export declare class WorkspaceIndexImpl implements WorkspaceIndex {
25
29
  private readonly bakeQueue;
26
30
  /** Names whose checksum doesn't match the current spec — surfaced via stats. */
27
31
  private readonly stale;
32
+ /** Cached spec.json per orbital so manifest rebuilds don't re-read disk. */
33
+ private readonly specsByOrbital;
34
+ /** Tokenized BM25 rows per orbital — rebuilt incrementally. */
35
+ private readonly tokensByOrbital;
36
+ /** Last-known recency timeline per orbital. */
37
+ private readonly recencyByOrbital;
38
+ /** Latest manifest payload (BM25 + intent + composed + recency). */
39
+ private manifest;
40
+ /** Serialized manifest writes so concurrent observer fires don't race. */
41
+ private manifestQueue;
28
42
  constructor(deps: WorkspaceIndexDeps);
43
+ private onWorkspaceWrite;
29
44
  warm(): Promise<void>;
30
45
  resolveOrbitalName(name: string, opts?: ResolveOptions): Promise<ResolveResult>;
31
46
  resolveTraitRef(emit: TraitRefEmit, orbitalContext: {
32
47
  orbitalName: string;
33
48
  }, opts?: ResolveOptions): Promise<ResolveResult>;
34
49
  stats(): WorkspaceIndexStats;
50
+ retrieveOrbitalsForPrompt(prompt: string, opts?: RetrievalOptions): Promise<readonly RetrievalResult[]>;
51
+ findByToken(query: string): readonly string[];
52
+ orbitalsListeningTo(event: string): readonly EventEdge[];
53
+ orbitalsEmitting(event: string): readonly EventEdge[];
54
+ orbitalsLinkedToEntity(entity: string): readonly EntityBinding[];
55
+ extraTraitImporters(refPath: string): readonly string[];
56
+ composedEvents(): readonly string[];
57
+ composedEntities(): readonly string[];
58
+ composedTraitRefPaths(): readonly string[];
59
+ rulesAppliedTo(entity: string): readonly RuleBinding[];
60
+ recentlyEdited(opts?: RecentlyEditedOptions): readonly string[];
61
+ private scheduleManifestRebuild;
62
+ private rebuildManifest;
63
+ private hydrateRecencyFromManifest;
64
+ private explainKnobMatches;
35
65
  private rebakeOrbital;
36
66
  private enqueueBake;
37
67
  private bakeAndPersist;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Workspace-level manifest I/O for the BM25 token table + graph maps +
3
+ * recency. Mirrors `sidecar.ts` — same helpers (`readJsonFile` +
4
+ * `writeJsonFile`), same path-layout discipline. Parent dir `.almadar/`
5
+ * is created by `ensureSkeleton` at workspace open, so no mkdir here.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import type { WorkspaceBackend } from '../internal/types.js';
10
+ import type { WorkspaceIndexManifest } from './types.js';
11
+ export declare function manifestPath(workDir: string): string;
12
+ export declare function readManifest(backend: WorkspaceBackend, workDir: string): Promise<WorkspaceIndexManifest | null>;
13
+ export declare function writeManifest(backend: WorkspaceBackend, workDir: string, manifest: WorkspaceIndexManifest): Promise<void>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Reciprocal Rank Fusion — combines arbitrary rankings into a single
3
+ * stable order. Key-agnostic; the workspace index uses orbital names.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ export interface RRFRankInput<TKey extends string> {
8
+ rankings: ReadonlyArray<readonly TKey[]>;
9
+ }
10
+ export interface RRFResult<TKey extends string> {
11
+ key: TKey;
12
+ score: number;
13
+ /** 1-based rank per input ranking; null when the key was absent from that ranking. */
14
+ ranks: readonly (number | null)[];
15
+ }
16
+ /** Reciprocal Rank Fusion. `k` defaults to `RRF_K` (60). */
17
+ export declare function rrfFuse<TKey extends string>(rankings: ReadonlyArray<readonly TKey[]>, k?: number): readonly RRFResult<TKey>[];
@@ -8,10 +8,18 @@
8
8
  * @packageDocumentation
9
9
  */
10
10
  import type { JsonObject } from '@almadar/core';
11
- /** Schema version baked into every sidecar; mismatch triggers re-bake. */
12
- export declare const WORKSPACE_INDEX_SCHEMA_VERSION: 1;
11
+ /** Schema version baked into every sidecar; mismatch triggers re-bake.
12
+ * v1 identity vector + extraTrait identity vectors only (Phase A).
13
+ * v2 — adds contentVector + contentFingerprint for prompt-narrowing retrieval (Phase B).
14
+ * Existing v1 sidecars get re-baked transparently on next openWorkspace.
15
+ */
16
+ export declare const WORKSPACE_INDEX_SCHEMA_VERSION: 2;
13
17
  /** Default coercion threshold per the doc's locked decision. */
14
18
  export declare const DEFAULT_COERCION_THRESHOLD: 0.85;
19
+ /** RRF fusion constant (Elastic / OpenSearch convention). */
20
+ export declare const RRF_K: 60;
21
+ /** Default top-K for retrieveOrbitalsForPrompt. */
22
+ export declare const DEFAULT_RETRIEVAL_TOP_K: 3;
15
23
  /**
16
24
  * Per-extraTraits[] entry identity vector. Baked alongside the orbital
17
25
  * sidecar whenever the orbital's spec.json is written.
@@ -29,15 +37,32 @@ export interface ExtraTraitIdentity {
29
37
  /**
30
38
  * Per-orbital sidecar contents, persisted at
31
39
  * `.almadar/sessions/<Orbital>/index.json` after every spec write.
40
+ *
41
+ * Two dense vectors per orbital — DIFFERENT compositions, DIFFERENT
42
+ * invalidation cadence, DIFFERENT use:
43
+ *
44
+ * - `identityVector` (Phase A) — small fingerprint over name + entity.
45
+ * Stable across knob edits. Used by `resolveOrbitalName` for R-10
46
+ * coercion.
47
+ * - `contentVector` (Phase B) — richer fingerprint that ALSO folds
48
+ * in every `traitOverrides.*.config.*` value, entityFields[].name,
49
+ * extraTraits[].name + .ref tail, ruleOverlay rule capabilities.
50
+ * Invalidates on every spec edit. Used by
51
+ * `retrieveOrbitalsForPrompt` to rank orbitals against the user's
52
+ * edit-prompt language.
32
53
  */
33
54
  export interface OrbitalIndexEntry {
34
55
  schemaVersion: typeof WORKSPACE_INDEX_SCHEMA_VERSION;
35
56
  /** sha256 of the spec.json that produced this sidecar. */
36
57
  specChecksum: string;
37
- /** Orbital-level identity vector. */
58
+ /** Orbital-level identity vector (Phase A — R-10 coercion). */
38
59
  identityVector: readonly number[];
39
60
  /** Fingerprint text for the orbital identity vector — kept for debugging. */
40
61
  identityFingerprint: string;
62
+ /** Orbital-level content vector (Phase B — prompt narrowing). */
63
+ contentVector: readonly number[];
64
+ /** Fingerprint text for the content vector — kept for debugging. */
65
+ contentFingerprint: string;
41
66
  /** One per current `extraTraits[]` entry on this orbital. */
42
67
  extraTraitIdentities: readonly ExtraTraitIdentity[];
43
68
  /** Epoch ms at bake time — debugging only. */
@@ -111,6 +136,51 @@ export interface WorkspaceIndex {
111
136
  resolveTraitRef(emit: TraitRefEmit, orbitalContext: {
112
137
  orbitalName: string;
113
138
  }, opts?: ResolveOptions): Promise<ResolveResult>;
139
+ /**
140
+ * Phase B — RRF-hybrid prompt retrieval. Embed the prompt, run sparse
141
+ * BM25 against the workspace token table, fuse via Reciprocal Rank
142
+ * Fusion (k=60), return top-K orbitals with explainability fields.
143
+ */
144
+ retrieveOrbitalsForPrompt(prompt: string, opts?: RetrievalOptions): Promise<readonly RetrievalResult[]>;
145
+ /** Phase B — sparse-only escape hatch. Tokenizes `query` and returns orbitals containing any term. */
146
+ findByToken(query: string): readonly string[];
147
+ /** Orbitals + traits whose state machine listens for `event`. */
148
+ orbitalsListeningTo(event: string): readonly EventEdge[];
149
+ /** Orbitals + traits whose state machine emits `event`. */
150
+ orbitalsEmitting(event: string): readonly EventEdge[];
151
+ /**
152
+ * Orbitals linked to `entity` — both composed (from the resolved
153
+ * schema.orb) and override-intent (from spec.json `linkedEntity`).
154
+ */
155
+ orbitalsLinkedToEntity(entity: string): readonly EntityBinding[];
156
+ /** Orbitals importing the trait at `refPath` via extraTraits[]. */
157
+ extraTraitImporters(refPath: string): readonly string[];
158
+ /**
159
+ * Every event name currently appearing in the composed schema as an
160
+ * emit or listen edge. Lets consumers feed each name back into
161
+ * `orbitalsEmitting` / `orbitalsListeningTo` to derive cross-orbital
162
+ * reactivity hints without holding a copy of the composed map.
163
+ */
164
+ composedEvents(): readonly string[];
165
+ /**
166
+ * Every entity name currently bound by the composed schema or by spec
167
+ * overrides. Lets consumers feed each name back into
168
+ * `orbitalsLinkedToEntity` without enumerating the composed map.
169
+ */
170
+ composedEntities(): readonly string[];
171
+ /**
172
+ * Every trait `refPath` currently imported via `extraTraits[]` across
173
+ * the workspace's per-orbital spec.json files. Lets consumers feed
174
+ * each back into `extraTraitImporters` for importer-set hints.
175
+ */
176
+ composedTraitRefPaths(): readonly string[];
177
+ /** Rules applied to `entity` via spec.json ruleOverlay.rules[]. */
178
+ rulesAppliedTo(entity: string): readonly RuleBinding[];
179
+ /**
180
+ * Orbitals edited within the last N turns (per
181
+ * params-history.jsonl). Ordered newest-first.
182
+ */
183
+ recentlyEdited(opts?: RecentlyEditedOptions): readonly string[];
114
184
  /** Diagnostics surface. */
115
185
  stats(): WorkspaceIndexStats;
116
186
  }
@@ -130,3 +200,136 @@ export interface EmbedderPort {
130
200
  * workspace's writeJsonFile helper requires.
131
201
  */
132
202
  export type OrbitalIndexSidecar = OrbitalIndexEntry & JsonObject;
203
+ /**
204
+ * One orbital's tokenization row inside the BM25 table. `docLen` is the
205
+ * full token count (with repeats) — BM25's length normalization needs
206
+ * it. `termFreq` is the per-term count for cosine/IDF math.
207
+ */
208
+ export interface BM25Document {
209
+ orbital: string;
210
+ termFreq: Readonly<Record<string, number>>;
211
+ docLen: number;
212
+ }
213
+ /**
214
+ * Workspace-level inverted-index payload. Persisted inside the index
215
+ * manifest at `.almadar/index.json`. Rebuilt incrementally per
216
+ * orbital — `documents` is the row store keyed by orbitalName.
217
+ */
218
+ export interface BM25Table {
219
+ /** Per-orbital tokenization row. */
220
+ documents: Readonly<Record<string, BM25Document>>;
221
+ /**
222
+ * Document frequency — count of orbitals containing each term.
223
+ * Recomputed whenever `documents` changes (cheap; tens of orbitals).
224
+ */
225
+ docFreq: Readonly<Record<string, number>>;
226
+ /** Number of orbitals indexed (`Object.keys(documents).length`). */
227
+ docCount: number;
228
+ /** Sum(docLen) / docCount — BM25 needs this for length normalization. */
229
+ avgDocLen: number;
230
+ }
231
+ /** BM25 query options (Elastic defaults). */
232
+ export interface BM25Options {
233
+ k1?: number;
234
+ b?: number;
235
+ }
236
+ export interface RetrievalOptions {
237
+ topK?: number;
238
+ rrfK?: number;
239
+ bm25?: BM25Options;
240
+ }
241
+ export interface RetrievalResult {
242
+ orbitalName: string;
243
+ /** RRF-fused score, monotonically decreasing across the returned array. */
244
+ score: number;
245
+ /**
246
+ * Tokens from the prompt that matched the orbital's BM25 row.
247
+ * Empty when this orbital surfaced via the dense leg only.
248
+ */
249
+ matchedTokens: readonly string[];
250
+ /**
251
+ * Domain knob keys (and their values) from the orbital's spec that the
252
+ * dense leg surfaced. Heuristic: knobs whose RENDERED text appears in
253
+ * the orbital's content fingerprint AND in the prompt verbatim.
254
+ * Empty when this orbital surfaced via the sparse leg only.
255
+ */
256
+ matchedKnobs: readonly string[];
257
+ /** 1-based rank within the dense ranking; `null` if not in the top-N dense list. */
258
+ rankByDense: number | null;
259
+ /** 1-based rank within the sparse ranking; `null` if not in the top-N sparse list. */
260
+ rankBySparse: number | null;
261
+ }
262
+ /** One emit/listen edge surfaced by the composed schema.orb. */
263
+ export interface EventEdge {
264
+ orbital: string;
265
+ trait: string;
266
+ state?: string;
267
+ role: 'emit' | 'listen';
268
+ scope?: 'internal' | 'external';
269
+ }
270
+ /** One orbital→entity binding surfaced by either the composed schema or the user/agent's override intent. */
271
+ export interface EntityBinding {
272
+ orbital: string;
273
+ trait: string;
274
+ source: 'composed' | 'override';
275
+ }
276
+ /** Rule applied to an entity, derived from spec.json ruleOverlay. */
277
+ export interface RuleBinding {
278
+ orbital: string;
279
+ entityName: string;
280
+ capability: string;
281
+ }
282
+ /**
283
+ * Per-orbital recency entry. `turn` is monotonic across the workspace
284
+ * (rabit increments it on every coordinator turn). `lastChange` is
285
+ * epoch ms of the most recent params-history append.
286
+ */
287
+ export interface RecencyEntry {
288
+ recencyTurn: number;
289
+ lastChange: number;
290
+ }
291
+ /**
292
+ * Maps derived ONLY from per-orbital spec.json. Rebuilt on each
293
+ * `kind: 'spec'` write for the affected orbital.
294
+ */
295
+ export interface IntentMaps {
296
+ /** `entity_name → orbitals overriding linkedEntity`. */
297
+ entityNameOverrides: Readonly<Record<string, readonly EntityBinding[]>>;
298
+ /** `trait_ref_path → orbitals importing the trait`. */
299
+ traitRefImporters: Readonly<Record<string, readonly string[]>>;
300
+ /** `rule_capability → orbital-rule bindings`. */
301
+ ruleCapabilities: Readonly<Record<string, readonly RuleBinding[]>>;
302
+ /** `organism → orbitals`. */
303
+ organism: Readonly<Record<string, readonly string[]>>;
304
+ }
305
+ /**
306
+ * Maps derived ONLY from the composed schema.orb. Rebuilt on each
307
+ * `kind: 'schema'` write.
308
+ */
309
+ export interface ComposedMaps {
310
+ /** `event_name → emit/listen edges`. */
311
+ events: Readonly<Record<string, readonly EventEdge[]>>;
312
+ /** `entity_name → composed entity bindings`. */
313
+ entityNameComposed: Readonly<Record<string, readonly EntityBinding[]>>;
314
+ }
315
+ /**
316
+ * Workspace-level manifest — combines BM25 table + intent maps + composed
317
+ * maps + recency. Persisted at `.almadar/index.json` and emitted via
318
+ * `WorkspaceWriteEvent.kind === 'workspace-index-manifest'` so consumer
319
+ * mirrors (apps/builder's Firestore observer) pick it up.
320
+ */
321
+ export interface WorkspaceIndexManifest {
322
+ schemaVersion: typeof WORKSPACE_INDEX_SCHEMA_VERSION;
323
+ bm25: BM25Table;
324
+ intent: IntentMaps;
325
+ composed: ComposedMaps;
326
+ recency: Readonly<Record<string, RecencyEntry>>;
327
+ /** Epoch ms at last manifest write — debugging only. */
328
+ bakedAt: number;
329
+ }
330
+ /** JSON-persisted shape (intersection with JsonObject upper bound). */
331
+ export type WorkspaceIndexManifestSidecar = WorkspaceIndexManifest & JsonObject;
332
+ export interface RecentlyEditedOptions {
333
+ /** Return orbitals edited within the last N turns. Default: 5. */
334
+ withinTurns?: number;
335
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/workspace",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "Storage-agnostic workspace primitives shared by Almadar consumers. One service, six exports, hidden paths, single observer. See docs/Almadar_Workspace.md.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",