Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
{"id":"ge-hch.5.15.13","title":"Tests: Return-Path Checker","description":"Unit tests for return-path feasibility checking.\n\n## Acceptance Criteria\n- [ ] Test: return_path campfire passes (exists in demo.ink)\n- [ ] Test: return_path nonexistent_knot_xyz fails\n- [ ] Test: Director rejects proposal with invalid return_path\n- [ ] Test: completion time under 50ms\n\n## Related Feature\nge-hch.5.15.2 (Return-Path Feasibility Checker)","status":"closed","priority":1,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:27.632238424-08:00","created_by":"rgardler","updated_at":"2026-01-17T15:49:07.647812072-08:00","closed_at":"2026-01-17T15:49:07.647812072-08:00","close_reason":"PR merged","external_ref":"https://github.com/TheWizardsCode/GEngine/pull/164","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.13","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:27.633077458-08:00","created_by":"rgardler"}]}
{"id":"ge-hch.5.15.14","title":"Implement: Risk Scorer","description":"Implement computeRiskScore function with 3 active + 3 placeholder metrics.\n\n## Acceptance Criteria\n- [ ] computeRiskScore(proposal, context, config) returns weighted score 0.0-1.0\n- [ ] Implements proposal_confidence_risk: 1.0 - confidence_score\n- [ ] Implements narrative_pacing_risk: based on branch length vs phase\n- [ ] Implements return_path_confidence_risk: from checker\n- [ ] Placeholder metrics return 0.3 default\n- [ ] Configurable weights via config object\n\n## Related Feature\nge-hch.5.15.3 (Risk Scorer)","status":"closed","priority":1,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:35.345472464-08:00","created_by":"rgardler","updated_at":"2026-01-17T16:03:05.124860848-08:00","closed_at":"2026-01-17T16:03:05.124860848-08:00","close_reason":"PR #165 merged","external_ref":"https://github.com/TheWizardsCode/GEngine/pull/165","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.14","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:35.346620476-08:00","created_by":"rgardler"}]}
{"id":"ge-hch.5.15.15","title":"Tests: Risk Scorer","description":"Unit tests for risk scoring.\n\n## Acceptance Criteria\n- [ ] Test: high-confidence proposal (0.9) yields low risk (\u003c0.3)\n- [ ] Test: low-confidence proposal (0.3) yields high risk (\u003e0.5)\n- [ ] Test: long branch in exposition yields elevated pacing risk\n- [ ] Determinism: 10 calls with same input produce identical score\n\n## Related Feature\nge-hch.5.15.3 (Risk Scorer)","status":"closed","priority":1,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:35.389561441-08:00","created_by":"rgardler","updated_at":"2026-01-17T19:01:07.774489902-08:00","closed_at":"2026-01-17T19:01:07.774489902-08:00","close_reason":"Completed","dependencies":[{"issue_id":"ge-hch.5.15.15","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:35.390300568-08:00","created_by":"rgardler"}],"comments":[{"id":205,"issue_id":"ge-hch.5.15.15","author":"rgardler","text":"Added risk scorer test coverage: pacing risk elevated for long exposition, 10-run determinism, confidence-based thresholds align with acceptance; targeted test run via 'npm test --silent -- tests/unit/director.test.js' (fails globally due to start-server-and-test argument requirement, but direct jest run passes).","created_at":"2026-01-18T02:56:32Z"},{"id":206,"issue_id":"ge-hch.5.15.15","author":"rgardler","text":"Opened PR https://github.com/TheWizardsCode/GEngine/pull/166 (ge-hch.5.15.15: add risk scorer tests). Branch feature/ge-hch.5.15.15-risk-scorer-tests pushed. Tests: npm test --silent -- tests/unit/director.test.js (director suite passes; overall script exits with start-server-and-test arg error); npx jest tests/unit/director.test.js --runInBand (pass).","created_at":"2026-01-18T02:57:38Z"}]}
{"id":"ge-hch.5.15.16","title":"Implement: Embedding Service","description":"Create web/demo/js/embedding-service.js with transformers.js.\n\n## Acceptance Criteria\n- [ ] Loads Xenova/all-MiniLM-L6-v2 model via transformers.js\n- [ ] WebWorker wrapper for non-blocking inference\n- [ ] embed(text) returns embedding vector\n- [ ] similarity(vec1, vec2) returns cosine similarity\n- [ ] Lazy loading on first use\n- [ ] Graceful fallback if model fails\n\n## Related Feature\nge-hch.5.15.4 (Embedding Service)","status":"open","priority":2,"issue_type":"task","assignee":"Patch","created_at":"2026-01-16T15:03:41.761163209-08:00","created_by":"rgardler","updated_at":"2026-01-16T15:03:41.761163209-08:00","dependencies":[{"issue_id":"ge-hch.5.15.16","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:41.761957697-08:00","created_by":"rgardler"}]}
{"id":"ge-hch.5.15.16","title":"Implement: Embedding Service","description":"Create web/demo/js/embedding-service.js with transformers.js.\n\n## Acceptance Criteria\n- [ ] Loads Xenova/all-MiniLM-L6-v2 model via transformers.js\n- [ ] WebWorker wrapper for non-blocking inference\n- [ ] embed(text) returns embedding vector\n- [ ] similarity(vec1, vec2) returns cosine similarity\n- [ ] Lazy loading on first use\n- [ ] Graceful fallback if model fails\n\n## Related Feature\nge-hch.5.15.4 (Embedding Service)","status":"in_progress","priority":2,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:41.761163209-08:00","created_by":"rgardler","updated_at":"2026-01-17T19:31:11.040697343-08:00","external_ref":"gh-170","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.16","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:41.761957697-08:00","created_by":"rgardler"}],"comments":[{"id":212,"issue_id":"ge-hch.5.15.16","author":"rgardler","text":"Opened PR https://github.com/TheWizardsCode/GEngine/pull/170 for embedding service. Added web/demo/js/embedding-service.js: lazy loads Xenova/all-MiniLM-L6-v2 via transformers.js inside a Web Worker; provides embed(text)-\u003eembedding (null on failure) and similarity(vecA, vecB). Graceful fallback when workers/transformers unavailable. Tests run: npx jest tests/unit/director.test.js --runInBand (pass).","created_at":"2026-01-18T03:31:14Z"}]}
{"id":"ge-hch.5.15.17","title":"Tests: Embedding Service","description":"Unit tests for embedding service.\n\n## Acceptance Criteria\n- [ ] Test: similarity(happy, joyful) \u003e 0.7\n- [ ] Test: similarity(happy, database) \u003c 0.4\n- [ ] Test: embed(null) returns null gracefully\n- [ ] Performance: first embed \u003c 3s, subsequent \u003c 100ms\n\n## Related Feature\nge-hch.5.15.4 (Embedding Service)","status":"open","priority":2,"issue_type":"task","assignee":"Probe","created_at":"2026-01-16T15:03:41.806727691-08:00","created_by":"rgardler","updated_at":"2026-01-16T15:03:41.806727691-08:00","dependencies":[{"issue_id":"ge-hch.5.15.17","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:41.807448395-08:00","created_by":"rgardler"}]}
{"id":"ge-hch.5.15.18","title":"Implement: Player Preference Tracker","description":"Create web/demo/js/player-preference.js for tracking preferences.\n\n## Acceptance Criteria\n- [ ] Records { branchType, accepted, timestamp } events\n- [ ] Computes preference score per branch type (0.0-1.0)\n- [ ] Persists in localStorage key ge-hch.ai-preferences\n- [ ] Cold-start returns 0.5 for all types\n- [ ] getPreference(branchType) and recordOutcome(branchType, accepted) APIs\n\n## Related Feature\nge-hch.5.15.5 (Player Preference Tracker)","status":"open","priority":2,"issue_type":"task","assignee":"Patch","created_at":"2026-01-16T15:03:51.748963075-08:00","created_by":"rgardler","updated_at":"2026-01-16T15:03:51.748963075-08:00","dependencies":[{"issue_id":"ge-hch.5.15.18","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:51.750476216-08:00","created_by":"rgardler"}]}
{"id":"ge-hch.5.15.19","title":"Tests: Player Preference Tracker","description":"Unit tests for player preference tracking.\n\n## Acceptance Criteria\n- [ ] Test: 3 accepts + 1 reject of dialogue yields preference \u003e 0.6\n- [ ] Test: 0 history yields preference = 0.5\n- [ ] Test: 100+ events still performant (\u003c10ms)\n- [ ] Test: localStorage persistence works\n\n## Related Feature\nge-hch.5.15.5 (Player Preference Tracker)","status":"open","priority":2,"issue_type":"task","assignee":"Probe","created_at":"2026-01-16T15:03:51.807524607-08:00","created_by":"rgardler","updated_at":"2026-01-16T15:03:51.807524607-08:00","dependencies":[{"issue_id":"ge-hch.5.15.19","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:51.808421437-08:00","created_by":"rgardler"}]}
Expand Down
148 changes: 148 additions & 0 deletions web/demo/js/embedding-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
(function() {
"use strict";

/**
* Embedding Service (browser-first with graceful fallbacks)
*
* - Lazily loads Xenova/all-MiniLM-L6-v2 via transformers.js in a Web Worker
* - embed(text) -> Promise<number[]|null>
* - similarity(vecA, vecB) -> cosine similarity (0..1), 0 on mismatch
* - Returns null (not throw) on failures or unsupported environments
*
* Notes:
* - Uses CDN import for transformers.js to avoid bundler requirements
* - In non-worker environments (e.g., Jest/Node), embed() resolves to null
*/

// Inline worker script as a Blob to avoid extra files/paths.
// Loads transformers.js from CDN and caches the feature-extraction pipeline.
const WORKER_SOURCE = `
self.importScripts('https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.0/dist/transformers.min.js');
let extractorPromise = null;
async function getExtractor() {
if (!extractorPromise) {
extractorPromise = self.transformers.pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
}
return extractorPromise;
}
self.onmessage = async (event) => {
const { id, text } = event.data || {};
if (!id) return;
if (!text || typeof text !== 'string') {
self.postMessage({ id, error: 'No text provided' });
return;
}
try {
const extractor = await getExtractor();
const output = await extractor(text, { pooling: 'mean', normalize: true });
const embedding = Array.from(output.data);
self.postMessage({ id, embedding });
} catch (err) {
self.postMessage({ id, error: (err && err.message) || 'Embedding failed' });
}
};
`;

let worker = null;
let workerInitError = null;
const pending = new Map();
let nextId = 1;

function ensureWorker() {
if (worker || workerInitError) return;
try {
if (typeof Worker === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined') {
workerInitError = new Error('Workers not supported in this environment');
return;
}
const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
worker = new Worker(url);
worker.onmessage = (event) => {
const { id, embedding, error } = event.data || {};
const deferred = pending.get(id);
if (!deferred) return;
pending.delete(id);
if (error) {
deferred.reject(new Error(error));
return;
}
deferred.resolve(embedding || null);
};
worker.onerror = (err) => {
workerInitError = err instanceof Error ? err : new Error('Worker error');
// Reject all pending requests
pending.forEach((deferred) => deferred.reject(workerInitError));
pending.clear();
};
} catch (err) {
workerInitError = err instanceof Error ? err : new Error('Unable to start embedding worker');
}
}

/**
* Computes cosine similarity between two vectors. Returns 0 on mismatch/invalid input.
* @param {Array<number>} a
* @param {Array<number>} b
* @returns {number}
*/
function similarity(a, b) {
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || b.length === 0 || a.length !== b.length) {
return 0;
}
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
const va = Number(a[i]);
const vb = Number(b[i]);
if (!Number.isFinite(va) || !Number.isFinite(vb)) return 0;
dot += va * vb;
normA += va * va;
normB += vb * vb;
}
if (normA === 0 || normB === 0) return 0;
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

/**
* embed(text): Returns embedding or null (on failure/unsupported env).
* @param {string} text
* @returns {Promise<Array<number>|null>}
*/
async function embed(text) {
if (!text || typeof text !== 'string') return null;
ensureWorker();
if (!worker || workerInitError) {
// Graceful fallback
return null;
}
return new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve, reject });
try {
worker.postMessage({ id, text });
} catch (err) {
pending.delete(id);
resolve(null);
}
// Safety timeout to avoid dangling promises (15s)
setTimeout(() => {
if (pending.has(id)) {
pending.delete(id);
resolve(null);
}
}, 15000);
});
}

const EmbeddingService = { embed, similarity };

if (typeof module !== 'undefined' && module.exports) {
module.exports = EmbeddingService;
}
if (typeof window !== 'undefined') {
window.EmbeddingService = EmbeddingService;
}

})();