@almadar/workspace 0.1.3 → 0.1.5
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/__tests__/workspace-index-cosine.test.d.ts +1 -0
- package/dist/__tests__/workspace-index-fingerprint.test.d.ts +1 -0
- package/dist/__tests__/workspace-index-real-embedder.test.d.ts +14 -0
- package/dist/__tests__/workspace-index.test.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +286 -3
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +3 -0
- package/dist/types.d.ts +9 -0
- package/dist/workspace-index/cosine.d.ts +8 -0
- package/dist/workspace-index/embedder.d.ts +24 -0
- package/dist/workspace-index/fingerprint.d.ts +54 -0
- package/dist/workspace-index/index-impl.d.ts +39 -0
- package/dist/workspace-index/index.d.ts +11 -0
- package/dist/workspace-index/sidecar.d.ts +22 -0
- package/dist/workspace-index/types.d.ts +132 -0
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end retrieval spec on the REAL `@almadar/llm` EmbeddingClient
|
|
3
|
+
* (bge-base-en-v1.5, 768-d). Skipped automatically when
|
|
4
|
+
* `OPEN_ROUTER_API_KEY` is not set in the environment.
|
|
5
|
+
*
|
|
6
|
+
* Purpose: validate before consumer integration that the workspace
|
|
7
|
+
* index actually catches the R-10 (orbital name drift) and R-8
|
|
8
|
+
* (extraTraits[] duplicate-ref) failure modes the design doc targets.
|
|
9
|
+
* Mock-embedder tests prove the wiring; this spec proves the geometry.
|
|
10
|
+
*
|
|
11
|
+
* Also reports per-fixture cosine to confirm
|
|
12
|
+
* `DEFAULT_COERCION_THRESHOLD = 0.85` is in the right ballpark.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -8,3 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { openWorkspace } from './open-workspace.js';
|
|
10
10
|
export type { WorkspaceService, WorkspaceObserver, WorkspaceWriteEvent, OpenWorkspaceOptions, RestoreBackend, GitHubConfig, GitStatusInfo, FileTreeNode, } from './types.js';
|
|
11
|
+
export type { WorkspaceIndex, EmbedderPort, ResolveResult, ResolveOptions, TraitRefEmit, WorkspaceIndexStats, OrbitalIndexEntry, ExtraTraitIdentity, } from './workspace-index/types.js';
|
|
12
|
+
export { DEFAULT_COERCION_THRESHOLD, WORKSPACE_INDEX_SCHEMA_VERSION, } from './workspace-index/types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path2 from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { execFile } from 'child_process';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
4
|
+
import { randomBytes, createHash } from 'crypto';
|
|
5
|
+
import { EmbeddingClient } from '@almadar/llm';
|
|
5
6
|
|
|
6
7
|
// src/open-workspace.ts
|
|
7
8
|
var LocalBackend = class {
|
|
@@ -529,6 +530,15 @@ async function restoreWorkspace(backend, rootDir, restore) {
|
|
|
529
530
|
}
|
|
530
531
|
|
|
531
532
|
// src/internal/memory-files.ts
|
|
533
|
+
async function readJsonFile(backend, absPath) {
|
|
534
|
+
if (!backend.exists(absPath)) return null;
|
|
535
|
+
try {
|
|
536
|
+
const raw = await backend.readFile(absPath);
|
|
537
|
+
return JSON.parse(raw);
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
532
542
|
async function writeJsonFile(backend, absPath, value) {
|
|
533
543
|
await backend.writeFile(absPath, JSON.stringify(value, null, 2));
|
|
534
544
|
}
|
|
@@ -538,6 +548,254 @@ async function appendJsonLine(backend, absPath, value) {
|
|
|
538
548
|
await backend.writeFile(absPath, existing + ending + JSON.stringify(value) + "\n");
|
|
539
549
|
}
|
|
540
550
|
|
|
551
|
+
// src/workspace-index/types.ts
|
|
552
|
+
var WORKSPACE_INDEX_SCHEMA_VERSION = 1;
|
|
553
|
+
var DEFAULT_COERCION_THRESHOLD = 0.85;
|
|
554
|
+
|
|
555
|
+
// src/workspace-index/cosine.ts
|
|
556
|
+
function cosineSimilarity(a, b) {
|
|
557
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
558
|
+
let dot = 0;
|
|
559
|
+
let magA = 0;
|
|
560
|
+
let magB = 0;
|
|
561
|
+
for (let i = 0; i < a.length; i++) {
|
|
562
|
+
const av = a[i] ?? 0;
|
|
563
|
+
const bv = b[i] ?? 0;
|
|
564
|
+
dot += av * bv;
|
|
565
|
+
magA += av * av;
|
|
566
|
+
magB += bv * bv;
|
|
567
|
+
}
|
|
568
|
+
if (magA === 0 || magB === 0) return 0;
|
|
569
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/workspace-index/fingerprint.ts
|
|
573
|
+
function composeOrbitalIdentityFingerprint(spec) {
|
|
574
|
+
const segments = [];
|
|
575
|
+
pushString(segments, spec["orbitalName"]);
|
|
576
|
+
const params = asJsonObject(spec["params"]);
|
|
577
|
+
if (params) {
|
|
578
|
+
pushString(segments, params["entityName"]);
|
|
579
|
+
}
|
|
580
|
+
return segments.join(" \xB7 ");
|
|
581
|
+
}
|
|
582
|
+
function composeExtraTraitIdentityFingerprint(emit) {
|
|
583
|
+
const segments = [];
|
|
584
|
+
pushString(segments, emit.ref);
|
|
585
|
+
const alias = emit.name && emit.name.length > 0 ? emit.name : tailOfRef(emit.ref);
|
|
586
|
+
if (alias && alias !== emit.ref) {
|
|
587
|
+
pushString(segments, alias);
|
|
588
|
+
}
|
|
589
|
+
pushString(segments, emit.linkedEntity);
|
|
590
|
+
return segments.join(" \xB7 ");
|
|
591
|
+
}
|
|
592
|
+
function deriveExtraTraitAlias(emit) {
|
|
593
|
+
if (emit.name && emit.name.length > 0) return emit.name;
|
|
594
|
+
return tailOfRef(emit.ref);
|
|
595
|
+
}
|
|
596
|
+
function pushString(out, value) {
|
|
597
|
+
if (typeof value === "string" && value.length > 0) out.push(value);
|
|
598
|
+
}
|
|
599
|
+
function asJsonObject(value) {
|
|
600
|
+
if (value === void 0 || value === null) return null;
|
|
601
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
function tailOfRef(ref) {
|
|
605
|
+
const match = /\.traits\.([A-Za-z0-9_]+)$/.exec(ref);
|
|
606
|
+
return match ? match[1] : ref;
|
|
607
|
+
}
|
|
608
|
+
var SIDECAR_BASENAME = "index.json";
|
|
609
|
+
function sidecarPath(workDir, orbital) {
|
|
610
|
+
return orbitalSessionFile(workDir, orbital, SIDECAR_BASENAME);
|
|
611
|
+
}
|
|
612
|
+
async function readSidecar(backend, workDir, orbital) {
|
|
613
|
+
const raw = await readJsonFile(backend, sidecarPath(workDir, orbital));
|
|
614
|
+
if (raw === null) return null;
|
|
615
|
+
if (raw.schemaVersion !== WORKSPACE_INDEX_SCHEMA_VERSION) return null;
|
|
616
|
+
return raw;
|
|
617
|
+
}
|
|
618
|
+
async function writeSidecar(backend, workDir, orbital, entry) {
|
|
619
|
+
await ensureOrbitalSessionDir(backend, workDir, orbital);
|
|
620
|
+
await writeJsonFile(
|
|
621
|
+
backend,
|
|
622
|
+
sidecarPath(workDir, orbital),
|
|
623
|
+
entry
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
function checksumSpec(spec) {
|
|
627
|
+
return createHash("sha256").update(JSON.stringify(spec)).digest("hex");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/workspace-index/index-impl.ts
|
|
631
|
+
var WorkspaceIndexImpl = class {
|
|
632
|
+
constructor(deps) {
|
|
633
|
+
this.deps = deps;
|
|
634
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
635
|
+
/** Serialized re-bake per orbital so concurrent writeSpec events don't race. */
|
|
636
|
+
this.bakeQueue = /* @__PURE__ */ new Map();
|
|
637
|
+
/** Names whose checksum doesn't match the current spec — surfaced via stats. */
|
|
638
|
+
this.stale = /* @__PURE__ */ new Set();
|
|
639
|
+
this.deps.sinks.subscribe({
|
|
640
|
+
onWrite: async (event) => {
|
|
641
|
+
if (event.kind !== "spec") return;
|
|
642
|
+
await this.rebakeOrbital(event.orbital, event.content);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async warm() {
|
|
647
|
+
const orbitals = this.deps.listOrbitals();
|
|
648
|
+
const work = [];
|
|
649
|
+
for (const orbital of orbitals) {
|
|
650
|
+
const spec = this.deps.readSpec(orbital);
|
|
651
|
+
if (spec === null) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const checksum = checksumSpec(spec);
|
|
655
|
+
const existing = await readSidecar(this.deps.backend, this.deps.workDir, orbital);
|
|
656
|
+
if (existing !== null && existing.specChecksum === checksum) {
|
|
657
|
+
this.entries.set(orbital, existing);
|
|
658
|
+
this.stale.delete(orbital);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
work.push(this.bakeAndPersist(orbital, spec, checksum));
|
|
662
|
+
}
|
|
663
|
+
await Promise.all(work);
|
|
664
|
+
}
|
|
665
|
+
async resolveOrbitalName(name, opts) {
|
|
666
|
+
const threshold = opts?.threshold ?? DEFAULT_COERCION_THRESHOLD;
|
|
667
|
+
if (this.entries.size === 0) {
|
|
668
|
+
return { coercedTo: null, similarity: 0, method: "identity-vector" };
|
|
669
|
+
}
|
|
670
|
+
const [vector] = await this.embedOne(name);
|
|
671
|
+
let bestOrbital = null;
|
|
672
|
+
let bestSim = 0;
|
|
673
|
+
for (const [orbital, entry] of this.entries) {
|
|
674
|
+
const sim = cosineSimilarity(vector, entry.identityVector);
|
|
675
|
+
if (sim > bestSim) {
|
|
676
|
+
bestSim = sim;
|
|
677
|
+
bestOrbital = orbital;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (bestOrbital !== null && bestSim >= threshold) {
|
|
681
|
+
return { coercedTo: bestOrbital, similarity: bestSim, method: "identity-vector" };
|
|
682
|
+
}
|
|
683
|
+
return { coercedTo: null, similarity: bestSim, method: "identity-vector" };
|
|
684
|
+
}
|
|
685
|
+
async resolveTraitRef(emit, orbitalContext, opts) {
|
|
686
|
+
const threshold = opts?.threshold ?? DEFAULT_COERCION_THRESHOLD;
|
|
687
|
+
const entry = this.entries.get(orbitalContext.orbitalName);
|
|
688
|
+
if (!entry || entry.extraTraitIdentities.length === 0) {
|
|
689
|
+
return { coercedTo: null, similarity: 0, method: "identity-vector" };
|
|
690
|
+
}
|
|
691
|
+
const fingerprint = composeExtraTraitIdentityFingerprint(emit);
|
|
692
|
+
const [vector] = await this.embedOne(fingerprint);
|
|
693
|
+
let bestAlias = null;
|
|
694
|
+
let bestSim = 0;
|
|
695
|
+
for (const candidate of entry.extraTraitIdentities) {
|
|
696
|
+
const sim = cosineSimilarity(vector, candidate.vector);
|
|
697
|
+
if (sim > bestSim) {
|
|
698
|
+
bestSim = sim;
|
|
699
|
+
bestAlias = candidate.alias;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (bestAlias !== null && bestSim >= threshold) {
|
|
703
|
+
return { coercedTo: bestAlias, similarity: bestSim, method: "identity-vector" };
|
|
704
|
+
}
|
|
705
|
+
return { coercedTo: null, similarity: bestSim, method: "identity-vector" };
|
|
706
|
+
}
|
|
707
|
+
stats() {
|
|
708
|
+
let extraTraitIdentityCount = 0;
|
|
709
|
+
for (const entry of this.entries.values()) {
|
|
710
|
+
extraTraitIdentityCount += entry.extraTraitIdentities.length;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
orbitalCount: this.entries.size,
|
|
714
|
+
extraTraitIdentityCount,
|
|
715
|
+
staleOrbitals: Array.from(this.stale).sort()
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
async rebakeOrbital(orbital, spec) {
|
|
719
|
+
const checksum = checksumSpec(spec);
|
|
720
|
+
const existing = this.entries.get(orbital);
|
|
721
|
+
if (existing && existing.specChecksum === checksum) return;
|
|
722
|
+
await this.enqueueBake(orbital, () => this.bakeAndPersist(orbital, spec, checksum));
|
|
723
|
+
}
|
|
724
|
+
enqueueBake(orbital, op) {
|
|
725
|
+
const prev = this.bakeQueue.get(orbital) ?? Promise.resolve();
|
|
726
|
+
const next = prev.then(op);
|
|
727
|
+
const swallowed = next.catch(() => void 0);
|
|
728
|
+
this.bakeQueue.set(orbital, swallowed);
|
|
729
|
+
return next;
|
|
730
|
+
}
|
|
731
|
+
async bakeAndPersist(orbital, spec, checksum) {
|
|
732
|
+
this.stale.add(orbital);
|
|
733
|
+
const identityFingerprint = composeOrbitalIdentityFingerprint(spec);
|
|
734
|
+
const extraInputs = extractExtraTraitInputs(spec);
|
|
735
|
+
const allTexts = [identityFingerprint, ...extraInputs.map((e) => e.fingerprint)];
|
|
736
|
+
const { embeddings } = await this.deps.embedder.embedBatch(allTexts);
|
|
737
|
+
if (embeddings.length !== allTexts.length) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
`[workspace-index] embedder returned ${embeddings.length} vectors for ${allTexts.length} inputs`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
const [identityVector, ...extraVectors] = embeddings;
|
|
743
|
+
const extraTraitIdentities = extraInputs.map((input, idx) => ({
|
|
744
|
+
ref: input.ref,
|
|
745
|
+
alias: input.alias,
|
|
746
|
+
fingerprint: input.fingerprint,
|
|
747
|
+
vector: extraVectors[idx]
|
|
748
|
+
}));
|
|
749
|
+
const entry = {
|
|
750
|
+
schemaVersion: WORKSPACE_INDEX_SCHEMA_VERSION,
|
|
751
|
+
specChecksum: checksum,
|
|
752
|
+
identityVector,
|
|
753
|
+
identityFingerprint,
|
|
754
|
+
extraTraitIdentities,
|
|
755
|
+
bakedAt: Date.now()
|
|
756
|
+
};
|
|
757
|
+
await writeSidecar(this.deps.backend, this.deps.workDir, orbital, entry);
|
|
758
|
+
this.entries.set(orbital, entry);
|
|
759
|
+
this.stale.delete(orbital);
|
|
760
|
+
}
|
|
761
|
+
async embedOne(text) {
|
|
762
|
+
const { embeddings } = await this.deps.embedder.embedBatch([text]);
|
|
763
|
+
if (embeddings.length !== 1) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`[workspace-index] embedder returned ${embeddings.length} vectors for a single text`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
return embeddings;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
function extractExtraTraitInputs(spec) {
|
|
772
|
+
const params = asJsonObject2(spec["params"]);
|
|
773
|
+
if (!params) return [];
|
|
774
|
+
const extras = params["extraTraits"];
|
|
775
|
+
if (!Array.isArray(extras)) return [];
|
|
776
|
+
const out = [];
|
|
777
|
+
for (const entry of extras) {
|
|
778
|
+
const obj = asJsonObject2(entry);
|
|
779
|
+
if (!obj) continue;
|
|
780
|
+
const ref = typeof obj["ref"] === "string" ? obj["ref"] : null;
|
|
781
|
+
if (ref === null) continue;
|
|
782
|
+
const name = typeof obj["name"] === "string" ? obj["name"] : void 0;
|
|
783
|
+
const linkedEntity = typeof obj["linkedEntity"] === "string" ? obj["linkedEntity"] : void 0;
|
|
784
|
+
const alias = deriveExtraTraitAlias({ ref, name });
|
|
785
|
+
out.push({
|
|
786
|
+
ref,
|
|
787
|
+
alias,
|
|
788
|
+
fingerprint: composeExtraTraitIdentityFingerprint({ ref, name, linkedEntity })
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
function asJsonObject2(value) {
|
|
794
|
+
if (value === void 0 || value === null) return null;
|
|
795
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
|
|
541
799
|
// src/service.ts
|
|
542
800
|
var COORD_FILES = {
|
|
543
801
|
analysis: "analysis.json",
|
|
@@ -563,6 +821,14 @@ var WorkspaceServiceImpl = class {
|
|
|
563
821
|
this._appId = args.appId;
|
|
564
822
|
this.git = args.git;
|
|
565
823
|
this.github = args.github;
|
|
824
|
+
this.index = new WorkspaceIndexImpl({
|
|
825
|
+
workDir: this.workDir,
|
|
826
|
+
backend: this.backend,
|
|
827
|
+
sinks: this.sinks,
|
|
828
|
+
embedder: args.embedder,
|
|
829
|
+
listOrbitals: () => this.listOrbitals(),
|
|
830
|
+
readSpec: (orbital) => this.readSpec(orbital)
|
|
831
|
+
});
|
|
566
832
|
}
|
|
567
833
|
// === Identity ===
|
|
568
834
|
get appId() {
|
|
@@ -1009,6 +1275,20 @@ async function removeTree(backend, dir) {
|
|
|
1009
1275
|
}
|
|
1010
1276
|
}
|
|
1011
1277
|
}
|
|
1278
|
+
function createDefaultEmbedder() {
|
|
1279
|
+
let client = null;
|
|
1280
|
+
function get() {
|
|
1281
|
+
if (client === null) client = new EmbeddingClient({ provider: "openrouter" });
|
|
1282
|
+
return client;
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
async embedBatch(texts) {
|
|
1286
|
+
if (texts.length === 0) return { embeddings: [] };
|
|
1287
|
+
const result = await get().embedBatch(texts);
|
|
1288
|
+
return { embeddings: result.embeddings };
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1012
1292
|
|
|
1013
1293
|
// src/open-workspace.ts
|
|
1014
1294
|
var openWorkspaceInFlight = /* @__PURE__ */ new Map();
|
|
@@ -1063,14 +1343,17 @@ async function openWorkspaceInternal(opts) {
|
|
|
1063
1343
|
if (opts.github && opts.backend !== "memory") {
|
|
1064
1344
|
git = await ensureGitInit(backend, resolved.workDir);
|
|
1065
1345
|
}
|
|
1066
|
-
|
|
1346
|
+
const service = new WorkspaceServiceImpl({
|
|
1067
1347
|
workDir: resolved.workDir,
|
|
1068
1348
|
backend,
|
|
1069
1349
|
sinks,
|
|
1350
|
+
embedder: opts.embedder ?? createDefaultEmbedder(),
|
|
1070
1351
|
appId: resolved.appId,
|
|
1071
1352
|
git,
|
|
1072
1353
|
github: opts.github
|
|
1073
1354
|
});
|
|
1355
|
+
await service.index.warm();
|
|
1356
|
+
return service;
|
|
1074
1357
|
}
|
|
1075
1358
|
async function resolveLifecycle(backend, opts) {
|
|
1076
1359
|
if (opts.adopt) {
|
|
@@ -1102,6 +1385,6 @@ async function resolveLifecycle(backend, opts) {
|
|
|
1102
1385
|
return { workDir, appId: opts.appId };
|
|
1103
1386
|
}
|
|
1104
1387
|
|
|
1105
|
-
export { openWorkspace };
|
|
1388
|
+
export { DEFAULT_COERCION_THRESHOLD, WORKSPACE_INDEX_SCHEMA_VERSION, openWorkspace };
|
|
1106
1389
|
//# sourceMappingURL=index.js.map
|
|
1107
1390
|
//# sourceMappingURL=index.js.map
|