diff --git a/packages/workspace-lens/__tests__/workspace-lens.test.ts b/packages/workspace-lens/__tests__/workspace-lens.test.ts index 6bf022ae..1405f235 100644 --- a/packages/workspace-lens/__tests__/workspace-lens.test.ts +++ b/packages/workspace-lens/__tests__/workspace-lens.test.ts @@ -76,6 +76,39 @@ describe('WorkspaceLens', () => { }); }); +describe('fromUrl / fromRegistry (HTTP lens loading)', () => { + // A fetch-shaped stub: 200 returns the artifact JSON; non-200 returns { ok:false, status }. + function fetchStub(status: number, body: unknown): typeof fetch { + return (async (_url: string) => ({ + ok: status >= 200 && status < 300, + status, + json: async () => body, + })) as unknown as typeof fetch; + } + + it('fromUrl fetches + constructs a lens from a served artifact', async () => { + const lens = await WorkspaceLens.fromUrl('https://cdn.example/qwen.json', { + fetchImpl: fetchStub(200, synthArtifact()), + }); + expect(lens.modelId).toBe('synth-3d'); + expect(lens.readout({ layer: 5, position: 0, h: [0, 0, 5] }).tokens[0].token).toBe('wrong'); + }); + + it('fromUrl fails loudly on a non-2xx response', async () => { + await expect( + WorkspaceLens.fromUrl('https://cdn.example/missing.json', { fetchImpl: fetchStub(404, {}) }), + ).rejects.toThrow(/HTTP 404/); + }); + + it('fromRegistry builds `${baseUrl}/${name}.json` (trailing slash tolerated) and constructs', async () => { + let seen = ''; + const capture = (async (url: string) => { seen = url; return { ok: true, status: 200, json: async () => synthArtifact() }; }) as unknown as typeof fetch; + const lens = await WorkspaceLens.fromRegistry('qwen-2.5-7b-jlens', { baseUrl: 'https://cdn.example/lenses/', fetchImpl: capture }); + expect(seen).toBe('https://cdn.example/lenses/qwen-2.5-7b-jlens.json'); + expect(lens.lensId).toBe('jlens-synth-v1'); + }); +}); + describe('safety triggers (vectorized, tokenizer-agnostic)', () => { const lens = WorkspaceLens.fromArtifact(synthArtifact()); const states: HiddenState[] = [{ layer: 5, position: 3, h: [0, 0, 5] }]; diff --git a/packages/workspace-lens/package.json b/packages/workspace-lens/package.json index 86dd6403..dca0de69 100644 --- a/packages/workspace-lens/package.json +++ b/packages/workspace-lens/package.json @@ -1,6 +1,6 @@ { "name": "@metaharness/workspace-lens", - "version": "0.1.0", + "version": "0.1.1", "description": "Jacobian-Lens interpretability primitive for open-weight LLMs (Anthropic 2026-07-06, 'Verbalizable Representations Form a Global Workspace'). Reads the model's functional workspace at inference time — lens_l(h)=unembed(J_l·h) — into workspace tokens, layer-trajectory, drift/entropy scores, vectorized safety flags, and a signable interpretability receipt. Runtime-only (fitting is external); model-agnostic; dependency-free.", "type": "module", "main": "./dist/index.js", @@ -11,7 +11,10 @@ "import": "./dist/index.js" } }, - "files": ["dist", "README.md"], + "files": [ + "dist", + "README.md" + ], "publishConfig": { "access": "public" }, diff --git a/packages/workspace-lens/src/lens.ts b/packages/workspace-lens/src/lens.ts index 1947dad7..7f2951df 100644 --- a/packages/workspace-lens/src/lens.ts +++ b/packages/workspace-lens/src/lens.ts @@ -32,6 +32,35 @@ export class WorkspaceLens { return new WorkspaceLens(JSON.parse(raw) as LensArtifact); } + /** + * Load a fitted lens over HTTP — fetch a serialized `LensArtifact` JSON from any URL (a CDN, an + * artifact store, a raw gist). The typical deploy path: fit a lens offline, publish the artifact, + * `fromUrl` it at startup. Inject `fetchImpl` for tests or non-global-fetch runtimes (defaults to the + * global `fetch`). Fails loudly on a non-2xx response so a bad URL never silently yields a broken lens. + */ + static async fromUrl(url: string, opts: { fetchImpl?: typeof fetch } = {}): Promise { + const doFetch = opts.fetchImpl ?? fetch; + const res = await doFetch(url); + if (!res.ok) throw new Error(`WorkspaceLens.fromUrl: ${url} → HTTP ${res.status}`); + return new WorkspaceLens((await res.json()) as LensArtifact); + } + + /** + * Convenience: load a lens by NAME from a registry base URL (e.g. a CDN of community pre-fit lenses). + * No registry is hardcoded — the caller supplies `baseUrl`, and the artifact is fetched from + * `${baseUrl}/${name}.json`. Keeps the package free of any assumed/embedded endpoint. + */ + static async fromRegistry( + name: string, + opts: { baseUrl: string; fetchImpl?: typeof fetch }, + ): Promise { + // Strip trailing slashes with a linear loop (NOT a `/\/+$/` regex — that backtracks on many-slash + // input, a ReDoS the CodeQL js/redos-on-library-input query flags). + let base = opts.baseUrl; + while (base.endsWith('/')) base = base.slice(0, -1); + return WorkspaceLens.fromUrl(`${base}/${encodeURIComponent(name)}.json`, { fetchImpl: opts.fetchImpl }); + } + get modelId(): string { return this.artifact.modelId; } get lensId(): string { return this.artifact.lensId; } get dModel(): number { return this.artifact.dModel; }