groove-dev 0.27.110 → 0.27.112
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/EMBEDDING_SERVICE_BUILD_PLAN.md +200 -0
- package/TRAINING_DATA_v2.md +9 -0
- package/moe-training/client/consent.js +47 -55
- package/moe-training/client/domain-tagger.js +3 -1
- package/moe-training/client/trajectory-capture.js +3 -2
- package/moe-training/shared/constants.js +1 -0
- package/moe-training/test/client/consent.test.js +23 -20
- package/moe-training/test/client/domain-tagger.test.js +6 -4
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +24 -42
- package/node_modules/@groove-dev/daemon/src/index.js +8 -10
- package/node_modules/@groove-dev/gui/dist/assets/{index-B8JomvGM.js → index-CHu5w3i3.js} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +1 -1
- package/node_modules/moe-training/client/consent.js +47 -55
- package/node_modules/moe-training/client/domain-tagger.js +3 -1
- package/node_modules/moe-training/client/trajectory-capture.js +3 -2
- package/node_modules/moe-training/shared/constants.js +1 -0
- package/node_modules/moe-training/test/client/consent.test.js +23 -20
- package/node_modules/moe-training/test/client/domain-tagger.test.js +6 -4
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +24 -42
- package/packages/daemon/src/index.js +8 -10
- package/packages/gui/dist/assets/{index-B8JomvGM.js → index-CHu5w3i3.js} +1 -1
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/stores/groove.js +1 -1
- package/TRAINING_DATA.md +0 -12
- package/codex/browser-racing-game/README.md +0 -45
- package/codex/browser-racing-game/dist/assets/index-D-sGTraQ.js +0 -47
- package/codex/browser-racing-game/dist/assets/index-S75nJv69.css +0 -1
- package/codex/browser-racing-game/dist/index.html +0 -14
- package/codex/browser-racing-game/index.html +0 -13
- package/codex/browser-racing-game/package-lock.json +0 -841
- package/codex/browser-racing-game/package.json +0 -15
- package/codex/browser-racing-game/src/app.css +0 -359
- package/codex/browser-racing-game/src/main.ts +0 -913
- package/codex/browser-racing-game/tsconfig.json +0 -20
- package/codex/browser-racing-game/vite.config.ts +0 -12
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Build Plan: Embedding Service Endpoint for Central Command
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
Groove clients need an embedding endpoint to compute 384-dimensional vectors from session text. The client (`DomainTagger`) already has the code to call it — it just needs the URL. This unlocks semantic domain tagging and `session_embedding` in training envelopes (currently `null` for all sessions).
|
|
6
|
+
|
|
7
|
+
## Model
|
|
8
|
+
|
|
9
|
+
**`sentence-transformers/all-MiniLM-L6-v2`** — ONNX format
|
|
10
|
+
- 22M parameters, ~80MB on disk
|
|
11
|
+
- 384-dimensional output vectors
|
|
12
|
+
- Download from Hugging Face: `Xenova/all-MiniLM-L6-v2` (ONNX-optimized)
|
|
13
|
+
- Runtime: `onnxruntime-node` (npm package)
|
|
14
|
+
|
|
15
|
+
## Dependencies
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install onnxruntime-node @xenova/transformers
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`@xenova/transformers` handles tokenization + ONNX inference in one package. If you prefer manual control, use `onnxruntime-node` directly with the tokenizer JSON files from the model repo.
|
|
22
|
+
|
|
23
|
+
## Endpoint
|
|
24
|
+
|
|
25
|
+
**`POST /v1/embed`**
|
|
26
|
+
|
|
27
|
+
### Request body:
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"input": "some text to embed",
|
|
31
|
+
"model": "sentence-transformers/all-MiniLM-L6-v2"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `input` — string, required, max 512 chars (client already truncates to 512)
|
|
36
|
+
- `model` — string, optional (only one model supported, ignore or validate)
|
|
37
|
+
|
|
38
|
+
### Response body (must match this exactly — client parses `data[0].embedding`):
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"data": [
|
|
42
|
+
{
|
|
43
|
+
"embedding": [0.0123, -0.0456, 0.0789, "...384 floats total"],
|
|
44
|
+
"index": 0
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"model": "sentence-transformers/all-MiniLM-L6-v2"
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This follows the OpenAI embedding response format. The client reads it at:
|
|
52
|
+
```javascript
|
|
53
|
+
const embedding = data?.data?.[0]?.embedding; // must be Array<number>, length 384
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Error responses:
|
|
57
|
+
- `400` — missing `input` field
|
|
58
|
+
- `503` — model not loaded yet (startup)
|
|
59
|
+
|
|
60
|
+
## Health check behavior
|
|
61
|
+
|
|
62
|
+
On `init()`, the client sends a probe request to verify the service is up:
|
|
63
|
+
```json
|
|
64
|
+
POST /v1/embed
|
|
65
|
+
{ "input": "health check", "model": "sentence-transformers/all-MiniLM-L6-v2" }
|
|
66
|
+
```
|
|
67
|
+
If this returns `200 OK`, the client switches from `keyword` mode to `http` mode. If it fails or times out (5s), the client silently falls back to keyword matching. So the endpoint must handle tiny inputs gracefully.
|
|
68
|
+
|
|
69
|
+
## Implementation approach
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
server/
|
|
73
|
+
├── embedding.js ← new file: load model once, expose embed(text) function
|
|
74
|
+
├── routes/
|
|
75
|
+
│ └── embed.js ← new file: POST /v1/embed route
|
|
76
|
+
└── index.js ← add: import + mount embed route
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `embedding.js` — Singleton model loader:
|
|
80
|
+
```javascript
|
|
81
|
+
import { pipeline } from '@xenova/transformers';
|
|
82
|
+
|
|
83
|
+
let embedder = null;
|
|
84
|
+
let loading = false;
|
|
85
|
+
let loadError = null;
|
|
86
|
+
|
|
87
|
+
export async function initEmbedding() {
|
|
88
|
+
if (embedder || loading) return;
|
|
89
|
+
loading = true;
|
|
90
|
+
try {
|
|
91
|
+
embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
92
|
+
console.log('[embedding] model loaded');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
loadError = err;
|
|
95
|
+
console.error('[embedding] failed to load model:', err.message);
|
|
96
|
+
}
|
|
97
|
+
loading = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function embed(text) {
|
|
101
|
+
if (!embedder) throw new Error('Model not loaded');
|
|
102
|
+
const result = await embedder(text, { pooling: 'mean', normalize: true });
|
|
103
|
+
return Array.from(result.data);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isReady() {
|
|
107
|
+
return embedder !== null;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `routes/embed.js`:
|
|
112
|
+
```javascript
|
|
113
|
+
import { Router } from 'express';
|
|
114
|
+
import { embed, isReady } from '../embedding.js';
|
|
115
|
+
|
|
116
|
+
export function createEmbedRoutes() {
|
|
117
|
+
const router = Router();
|
|
118
|
+
|
|
119
|
+
router.post('/v1/embed', async (req, res) => {
|
|
120
|
+
if (!isReady()) {
|
|
121
|
+
return res.status(503).json({ error: 'Model loading' });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { input } = req.body;
|
|
125
|
+
if (!input || typeof input !== 'string') {
|
|
126
|
+
return res.status(400).json({ error: 'Missing input field' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const text = input.slice(0, 512);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const vector = await embed(text);
|
|
133
|
+
res.json({
|
|
134
|
+
data: [{ embedding: vector, index: 0 }],
|
|
135
|
+
model: 'sentence-transformers/all-MiniLM-L6-v2',
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
res.status(500).json({ error: err.message });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return router;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `index.js` — add to existing server setup:
|
|
147
|
+
```javascript
|
|
148
|
+
import { createEmbedRoutes } from './routes/embed.js';
|
|
149
|
+
import { initEmbedding } from './embedding.js';
|
|
150
|
+
|
|
151
|
+
// Mount alongside existing routes
|
|
152
|
+
app.use(createEmbedRoutes());
|
|
153
|
+
|
|
154
|
+
// Load model in background (don't block server startup)
|
|
155
|
+
initEmbedding();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Client configuration
|
|
159
|
+
|
|
160
|
+
Once the endpoint is live, Groove clients connect by setting one env var:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
export EMBEDDING_SERVICE_URL=https://api.groovedev.ai/v1/embed
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The `DomainTagger` constructor reads `process.env.EMBEDDING_SERVICE_URL` automatically. No client code changes needed.
|
|
167
|
+
|
|
168
|
+
## What changes on the Groove side
|
|
169
|
+
|
|
170
|
+
Nothing. The client code already supports this. When `EMBEDDING_SERVICE_URL` is set:
|
|
171
|
+
1. `DomainTagger.init()` probes the URL → switches to `http` mode
|
|
172
|
+
2. `tag()` uses cosine similarity against domain centroids instead of keywords
|
|
173
|
+
3. `embed()` returns `{ model, vector, source_text }` instead of `null`
|
|
174
|
+
4. `trajectory-capture.js` writes the vector into `session_embedding` on SESSION_CLOSE
|
|
175
|
+
|
|
176
|
+
## Performance notes
|
|
177
|
+
|
|
178
|
+
- Model loads once at startup (~2-5 seconds)
|
|
179
|
+
- Inference: ~5-15ms per embedding on CPU, <2ms on GPU
|
|
180
|
+
- Each session close triggers ~45 embed calls (40 domain centroids + routing text + session embedding) during `init()`, then 1-2 per session close
|
|
181
|
+
- The 40 centroid embeddings are computed once in `_buildCentroids()` during init and cached in memory — not per-request
|
|
182
|
+
- Memory footprint: ~200MB for model + runtime
|
|
183
|
+
|
|
184
|
+
## Verification
|
|
185
|
+
|
|
186
|
+
After deploying, test with:
|
|
187
|
+
```bash
|
|
188
|
+
curl -X POST https://api.groovedev.ai/v1/embed \
|
|
189
|
+
-H "Content-Type: application/json" \
|
|
190
|
+
-d '{"input": "React TypeScript frontend development"}' \
|
|
191
|
+
| jq '.data[0].embedding | length'
|
|
192
|
+
# Should return: 384
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Then on any Groove client machine:
|
|
196
|
+
```bash
|
|
197
|
+
export EMBEDDING_SERVICE_URL=https://api.groovedev.ai/v1/embed
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Next agent spawn will show `session_embedding` populated in training data instead of `null`, and domain tags will have much higher confidence scores.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{"envelope_id":"env_b4523c8f-73b0-43f6-a423-27c6988b9c5d","session_id":"sess_0f97cb5e-2d2f-4e09-b2e5-2cf41ab47fc1","chunk_sequence":0,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"f7c2d016b3570fc7bfe88c1fb98ce3e9c8e7e0d47b7294fc3f5f33e65b8a46cf","sequence":0,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"gpt-5.5","provider":"codex","agent_role":"planner","agent_id":"810292fe","task_complexity":"medium","team_size":1,"session_quality":0,"groove_version":"0.27.110","leaf_context":null},"trajectory_log":[{"step":1,"type":"thought","timestamp":1777327787.368,"content":"I’m planner-4, ready to plan and route work for this GROOVE project once you tell me what you’d like done.","token_count":27},{"step":2,"type":"resolution","timestamp":1777327787.421,"content":"","token_count":1}]}
|
|
2
|
+
{"envelope_id":"env_5afdaf0c-3d52-4529-8be2-2aa1c1210390","session_id":"sess_0f97cb5e-2d2f-4e09-b2e5-2cf41ab47fc1","type":"SESSION_CLOSE","attestation":{"session_hmac":"69cf84148ef5a42c04822c5c38c35243a709e162e953bd903c0b0af9b6cb9a39","sequence":1,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"gpt-5.5","provider":"codex","agent_role":"planner","agent_id":"810292fe","task_complexity":"medium","team_size":1,"session_quality":50,"groove_version":"0.27.110","leaf_context":null,"domain_tags":null,"session_embedding":null},"outcome":{"status":"SUCCESS","session_quality":50,"quality_tier":"TIER_B","quality_tier_reason":"moderate_quality","user_interventions":0,"total_steps":2,"total_chunks":1,"total_tokens":28,"duration_seconds":3,"files_modified":0,"errors_encountered":0,"errors_recovered":0,"coordination_events":0,"training_eligible":false,"training_exclusion_reason":"too_few_steps"}}
|
|
3
|
+
{"envelope_id":"env_404c446e-7914-470c-9d0f-dc5cac635598","session_id":"sess_83876a7f-7877-4990-9c31-fd09a02ec5ad","chunk_sequence":0,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"0ac1cddcfd5cf2549dad1d6afa1dbbaa4e4df09314d919eadc1f0f08c64118ff","sequence":0,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"gpt-5.5","provider":"codex","agent_role":"planner","agent_id":"cca5946b","task_complexity":"medium","team_size":1,"session_quality":0,"groove_version":"0.27.110","leaf_context":null},"trajectory_log":[{"step":1,"type":"thought","timestamp":1777327824.179,"content":"I’ll quickly check the existing team registry, then route the browser game work without touching source code.","token_count":28},{"step":2,"type":"action","timestamp":1777327824.783,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"pwd && ls -la && if [ -f AGENTS_REGISTRY.md ]; then sed -n '1,220p' AGENTS_REGISTRY.md; fi && if [ -f .groove/recommended-team.json ]; then printf '\\\\n--- recommended-team ---\\\\n'; cat .groove/recommended-team.json; fi\""},"content":"Executing: /bin/zsh -lc \"pwd && ls -la && if [ -f AGENTS_REGISTRY.md ]; then sed -n '1,220p' AGENTS_REGISTRY.md; fi && if [ -f .groove/recommended-team.json ]; then printf '\\\\n--- recommended-team ---\\\\n'; cat .groove/recommended-team.json; fi\"","token_count":125},{"step":3,"type":"observation","timestamp":1777327824.833,"content":"~/Desktop/groove-project/groove/codex\ntotal 0\ndrwxr-xr-x@ 2 rok staff 64 Apr 27 15:09 .\ndrwxr-xr-x@ 44 rok staff 1408 Apr 27 15:10 ..\n","truncated":false,"original_token_count":38,"token_count":36},{"step":4,"type":"thought","timestamp":1777327833.467,"content":"No existing team config is present here, so I’m creating a new frontend-focused team plan for the browser racing game.","token_count":30},{"step":5,"type":"action","timestamp":1777327838.739,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"mkdir -p .groove && cat > .groove/recommended-team.json <<'EOF'\n{\n \\\"projectDir\\\": \\\"browser-racing-game\\\",\n \\\"agents\\\": [\n {\n \\\"role\\\": \\\"frontend\\\",\n \\\"phase\\\": 1,\n \\\"scope\\\": [\n \\\"package.json\\\",\n \\\"index.html\\\",\n \\\"src/**\\\",\n \\\"public/**\\\",\n \\\"README.md\\\",\n \\\"tsconfig.json\\\",\n \\\"vite.config.*\\\"\n ],\n \\\"prompt\\\": \\\"Build a complete browser racing game inspired by F1/NASCAR. Create a new Vite + TypeScript browser app in this project directory using ESM import/export. Include FSL-1.1-Apache-2.0 license headers in all source files. Implement a polished playable top-down or pseudo-3D racing experience with keyboard controls, acceleration/brake/steering physics, lap timing, checkpoint/lap validation, AI opponent cars, collision/track-boundary handling, speedometer/tach/lap HUD, start countdown, pause/restart flow, and responsive canvas rendering. Add visual details such as track markings, grass/asphalt/curbs, car sprites or canvas-drawn cars, skid/speed feedback, and simple sound toggles if practical. Keep the implementation dependency-light and runnable with npm install and npm run dev. Document controls and development commands in README.md. Do not delete unrelated files.\\\"\n },\n {\n \\\"role\\\": \\\"fullstack\\\",\n \\\"phase\\\": 2,\n \\\"scope\\\": [],\n \\\"prompt\\\": \\\"QC Senior Dev: Audit all changes from the phase 1 frontend agent. Verify the game is playable and meets the requested F1/NASCAR-style browser racing requirements. Check code organization, TypeScript correctness, ESM usage, license headers on source files, README instructions, and responsive behavior. Fix any issues found, run npm install if needed, run tests/build/typecheck available in package.json, and verify npm run build compiles. Do NOT start long-running dev servers. Do NOT open files in a browser — no open commands. Commit all changes.\\\"\n }\n ],\n \\\"preview\\\": {\n \\\"kind\\\": \\\"dev-server\\\",\n \\\"command\\\": \\\"npm run dev\\\",\n \\\"cwd\\\": \\\"browser-racing-game\\\",\n \\\"urlPattern\\\": \\\"https?://(localhost|127\\\\\\\\.0\\\\\\\\.0\\\\\\\\.1):\\\\\\\\d+\\\",\n \\\"readyText\\\": \\\"Local:\\\",\n \\\"openPath\\\": \\\"/\\\"\n }\n}\nEOF\ncat .groove/recommended-team.json\""},"content":"Executing: /bin/zsh -lc \"mkdir -p .groove && cat > .groove/recommended-team.json <<'EOF'\n{\n \\\"projectDir\\\": \\\"browser-racing-game\\\",\n \\\"agents\\\": [\n {\n \\\"role\\\": \\\"frontend\\\",\n \\\"phase\\\": 1,\n \\\"scope\\\": [\n \\\"package.json\\\",\n \\\"index.html\\\",\n \\\"src/**\\\",\n \\\"public/**\\\",\n \\\"README.md\\\",\n \\\"tsconfig.json\\\",\n \\\"vite.config.*\\\"\n ],\n \\\"prompt\\\": \\\"Build a complete browser racing game inspired by F1/NASCAR. Create a new Vite + TypeScript browser app in this project directory using ESM import/export. Include FSL-1.1-Apache-2.0 license headers in all source files. Implement a polished playable top-down or pseudo-3D racing experience with keyboard controls, acceleration/brake/steering physics, lap timing, checkpoint/lap validation, AI opponent cars, collision/track-boundary handling, speedometer/tach/lap HUD, start countdown, pause/restart flow, and responsive canvas rendering. Add visual details such as track markings, grass/asphalt/curbs, car sprites or canvas-drawn cars, skid/speed feedback, and simple sound toggles if practical. Keep the implementation dependency-light and runnable with npm install and npm run dev. Document controls and development commands in README.md. Do not delete unrelated files.\\\"\n },\n {\n \\\"role\\\": \\\"fullstack\\\",\n \\\"phase\\\": 2,\n \\\"scope\\\": [],\n \\\"prompt\\\": \\\"QC Senior Dev: Audit all changes from the phase 1 frontend agent. Verify the game is playable and meets the requested F1/NASCAR-style browser racing requirements. Check code organization, TypeScript correctness, ESM usage, license headers on source files, README instructions, and responsive behavior. Fix any issues found, run npm install if needed, run tests/build/typecheck available in package.json, and verify npm run build compiles. Do NOT start long-running dev servers. Do NOT open files in a browser — no open commands. Commit all changes.\\\"\n }\n ],\n \\\"preview\\\": {\n \\\"kind\\\": \\\"dev-server\\\",\n \\\"command\\\": \\\"npm run dev\\\",\n \\\"cwd\\\": \\\"browser-racing-game\\\",\n \\\"urlPattern\\\": \\\"https?://(localhost|127\\\\\\\\.0\\\\\\\\.0\\\\\\\\.1):\\\\\\\\d+\\\",\n \\\"readyText\\\": \\\"Local:\\\",\n \\\"openPath\\\": \\\"/\\\"\n }\n}\nEOF\ncat .groove/recommended-team.json\"","token_count":1177},{"step":6,"type":"observation","timestamp":1777327838.791,"content":"{\n \"projectDir\": \"browser-racing-game\",\n \"agents\": [\n {\n \"role\": \"frontend\",\n \"phase\": 1,\n \"scope\": [\n \"package.json\",\n \"index.html\",\n \"src/**\",\n \"public/**\",\n \"README.md\",\n \"tsconfig.json\",\n \"vite.config.*\"\n ],\n \"prompt\": \"Build a complete browser racing game inspired by F1/NASCAR. Create a new Vite + TypeScript browser app in this project directory using ESM import/export. Include FSL-1.1-Apache-2.0 license headers in all source files. Implement a polished playable top-down or pseudo-3D racing experience with keyboard controls, acceleration/brake/steering physics, lap timing, checkpoint/lap validation, AI opponent cars, collision/track-boundary handling, speedometer/tach/lap HUD, start countdown, pause/restart flow, and responsive canvas rendering. Add visual details such as track markings, grass/asphalt/curbs, car sprites or canvas-drawn cars, skid/speed feedback, and simple sound toggles if practical. Keep the implementation dependency-light and runnable with npm install and npm run dev. Document controls and development commands in README.md. Do not delete unrelated files.\"\n },\n {\n \"role\": \"fullstack\",\n \"phase\": 2,\n \"scope\": [],\n \"prompt\": \"QC Senior Dev: Audit all changes from the phase 1 frontend agent. Verify the game is playable and meets the requested F1/NASCAR-style browser racing requirements. Check code organization, TypeScript correctness, ESM usage, license headers on source files, README instructions, and responsive behavior. Fix any issues found, run npm install if needed, run tests/build/typecheck available in package.json, and verify npm run build compiles. Do NOT start long-running dev servers. Do NOT open files in a browser — no open commands. Commit all changes.\"\n }\n ],\n \"preview\": {\n \"kind\": \"dev-server\",\n \"command\": \"npm run dev\",\n \"cwd\": \"browser-racing-game\",\n \"urlPattern\": \"https?://(localhost|127\\\\.0\\\\.0\\\\.1):\\\\d+\",\n \"readyText\": \"Local:\",\n \"openPath\": \"/\"\n }\n}\n","truncated":false,"original_token_count":513,"token_count":513},{"step":7,"type":"thought","timestamp":1777327845.284,"content":"Routing plan created in .groove/recommended-team.json.\n\nAssigned:\n- frontend phase 1: build the Vite + TypeScript browser racing game\n- fullstack phase 2: QC, fix issues, verify build, and commit changes\n\nPreview configured:\n- npm run dev\n- cwd: browser-racing-game\n- opens at / once Vite reports Local:","token_count":76},{"step":8,"type":"resolution","timestamp":1777327845.334,"content":"","token_count":1}]}
|
|
4
|
+
{"envelope_id":"env_5772e288-c4fc-4530-9574-c7b95d818f60","session_id":"sess_83876a7f-7877-4990-9c31-fd09a02ec5ad","type":"SESSION_CLOSE","attestation":{"session_hmac":"4038e0c33747770c83c2f35456b3a8ca78a1f00b5e6ae29894dec8d359d35330","sequence":1,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"gpt-5.5","provider":"codex","agent_role":"planner","agent_id":"cca5946b","task_complexity":"medium","team_size":1,"session_quality":60,"groove_version":"0.27.110","leaf_context":null,"domain_tags":{"primary":{"domain":"react_frontend","confidence":0.0667},"secondary":{"domain":"python","confidence":0},"tertiary":{"domain":"typescript_node","confidence":0}},"session_embedding":null},"outcome":{"status":"SUCCESS","session_quality":60,"quality_tier":"TIER_B","quality_tier_reason":"moderate_quality","user_interventions":0,"total_steps":8,"total_chunks":1,"total_tokens":1986,"duration_seconds":26,"files_modified":0,"errors_encountered":0,"errors_recovered":0,"coordination_events":0,"training_eligible":true,"training_exclusion_reason":null}}
|
|
5
|
+
{"envelope_id":"env_4a8457c5-c865-4b9d-aeef-449fdc2cb9dd","session_id":"sess_ff149191-44eb-42e2-b97a-e63d5b42be14","chunk_sequence":0,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"78a43556ae2617923ab660c07599b04968396f77804f1c5df4746fc7970f649b","sequence":0,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"gpt-5.5","provider":"codex","agent_role":"frontend","agent_id":"117955e3","task_complexity":"medium","team_size":1,"session_quality":0,"groove_version":"0.27.110","leaf_context":null},"trajectory_log":[{"step":1,"type":"thought","timestamp":1777327861.461,"content":"I’ll first inspect the project structure and any existing styling/instructions, then scaffold the racing app within the allowed files.","token_count":34},{"step":2,"type":"action","timestamp":1777327864.47,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"pwd && rg --files -g '\"'!*node_modules*'\"' -g '\"'!MEMORY.md'\"' -g '\"'!.groove/**'\"' | sed -n '1,120p'\""},"content":"Executing: /bin/zsh -lc \"pwd && rg --files -g '\"'!*node_modules*'\"' -g '\"'!MEMORY.md'\"' -g '\"'!.groove/**'\"' | sed -n '1,120p'\"","token_count":67},{"step":3,"type":"observation","timestamp":1777327864.522,"content":"~/Desktop/groove-project/groove/codex/browser-racing-game\n","truncated":false,"original_token_count":17,"token_count":15},{"step":4,"type":"thought","timestamp":1777327866.882,"content":"The directory appears empty from `rg`; I’ll verify hidden files before creating the app.","token_count":22},{"step":5,"type":"action","timestamp":1777327867.618,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"find . -maxdepth 3 -not -path './node_modules/*' -print | sort\""},"content":"Executing: /bin/zsh -lc \"find . -maxdepth 3 -not -path './node_modules/*' -print | sort\"","token_count":46},{"step":6,"type":"observation","timestamp":1777327867.67,"content":".\n","truncated":false,"original_token_count":1,"token_count":1},{"step":7,"type":"thought","timestamp":1777327882.537,"content":"No existing files or `app.css` are present, so I’m creating a fresh Vite TypeScript canvas app with scoped styling.","token_count":29},{"step":8,"type":"thought","timestamp":1777327924.239,"content":"Base scaffolding is in place. Next I’ll add the game loop, physics, AI, rendering, and HUD wiring.","token_count":25},{"step":9,"type":"thought","timestamp":1777328033.245,"content":"Gameplay code is now added. I’ll document controls and dev commands, then build-check the app.","token_count":24},{"step":10,"type":"thought","timestamp":1777328049.438,"content":"Docs are added. I’ll install dependencies and run the production build to catch TypeScript or bundling issues.","token_count":28},{"step":11,"type":"action","timestamp":1777328054.551,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc 'npm install && npm run build'"},"content":"Executing: /bin/zsh -lc 'npm install && npm run build'","token_count":28},{"step":12,"type":"error","timestamp":1777328065.372,"content":"\nadded 18 packages, and audited 19 packages in 10s\n\n8 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities\n\n> browser-racing-game@0.1.0 build\n> tsc && vite build\n\ntsconfig.json(13,25): error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '\"ignoreDeprecations\": \"6.0\"' to silence this error.\n Visit https://aka.ms/ts6 for migration information.\n","token_count":114},{"step":13,"type":"thought","timestamp":1777328076.523,"content":"The build surfaced a TypeScript 6 deprecation setting; I’ll switch to Vite’s bundler resolution and remove the unused React plugin dependency.","token_count":36},{"step":14,"type":"action","timestamp":1777328078.417,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"apply_patch <<'PATCH'\n*** Begin Patch\n*** Update File: tsconfig.json\n@@\n- \\\"moduleResolution\\\": \\\"Node\\\",\n+ \\\"moduleResolution\\\": \\\"Bundler\\\",\n*** Update File: package.json\n@@\n- \\\"dependencies\\\": {\n- \\\"@vitejs/plugin-react\\\": \\\"latest\\\",\n- \\\"typescript\\\": \\\"latest\\\",\n- \\\"vite\\\": \\\"latest\\\"\n- },\n- \\\"devDependencies\\\": {}\n+ \\\"dependencies\\\": {},\n+ \\\"devDependencies\\\": {\n+ \\\"typescript\\\": \\\"latest\\\",\n+ \\\"vite\\\": \\\"latest\\\"\n+ }\n }\n*** End Patch\nPATCH\nrm -rf node_modules package-lock.json\""},"content":"Executing: /bin/zsh -lc \"apply_patch <<'PATCH'\n*** Begin Patch\n*** Update File: tsconfig.json\n@@\n- \\\"moduleResolution\\\": \\\"Node\\\",\n+ \\\"moduleResolution\\\": \\\"Bundler\\\",\n*** Update File: package.json\n@@\n- \\\"dependencies\\\": {\n- \\\"@vitejs/plugin-react\\\": \\\"latest\\\",\n- \\\"typescript\\\": \\\"latest\\\",\n- \\\"vite\\\": \\\"latest\\\"\n- },\n- \\\"devDependencies\\\": {}\n+ \\\"dependencies\\\": {},\n+ \\\"devDependencies\\\": {\n+ \\\"typescript\\\": \\\"latest\\\",\n+ \\\"vite\\\": \\\"latest\\\"\n+ }\n }\n*** End Patch\nPATCH\nrm -rf node_modules package-lock.json\"","token_count":296},{"step":15,"type":"observation","timestamp":1777328078.469,"content":"Success. Updated the following files:\nM tsconfig.json\nM package.json\n","truncated":false,"original_token_count":18,"token_count":18},{"step":16,"type":"thought","timestamp":1777328082.621,"content":"Config is fixed and temporary install artifacts were cleaned up. I’ll rebuild with no lockfile left behind.","token_count":27},{"step":17,"type":"action","timestamp":1777328083.123,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc 'npm install --no-package-lock && npm run build; status=$?; rm -rf node_modules package-lock.json; exit $status'"},"content":"Executing: /bin/zsh -lc 'npm install --no-package-lock && npm run build; status=$?; rm -rf node_modules package-lock.json; exit $status'","token_count":69},{"step":18,"type":"error","timestamp":1777328085.541,"content":"\nadded 16 packages, and audited 17 packages in 1s\n\n8 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities\n\n> browser-racing-game@0.1.0 build\n> tsc && vite build\n\nsrc/main.ts(5,8): error TS2882: Cannot find module or type declarations for side-effect import of './style.css'.\nsrc/main.ts(516,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(518,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(519,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(520,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(521,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(528,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(533,20): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(537,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(538,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(540,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(541,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(542,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(543,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(545,5): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(546,5): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(547,5): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(548,5): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(550,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(566,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(567,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(568,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(569,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(570,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(572,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(573,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(577,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(578,3): error TS18047: 'ctx' is possibly 'null'.\nsrc/main.ts(579,3): error TS18047: 'ctx' is","token_count":500},{"step":19,"type":"thought","timestamp":1777328094.904,"content":"The next build found strict nullability issues and a missing CSS module declaration. I’ll tighten the canvas/context initialization and add Vite env types.","token_count":39},{"step":20,"type":"action","timestamp":1777328095.718,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"sed -n '1,80p' src/main.ts && sed -n '500,530p' src/main.ts && ls -la | sed -n '1,80p'\""},"content":"Executing: /bin/zsh -lc \"sed -n '1,80p' src/main.ts && sed -n '500,530p' src/main.ts && ls -la | sed -n '1,80p'\"","token_count":58},{"step":21,"type":"error","timestamp":1777328095.77,"content":"/*\n * Copyright (c) 2026.\n * Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License.\n */\nimport './style.css';\n\ntype Vec = { x: number; y: number };\ntype RacePhase = 'ready' | 'countdown' | 'racing' | 'paused' | 'finished';\n\ntype Checkpoint = {\n position: Vec;\n radius: number;\n label: string;\n};\n\ntype Car = {\n name: string;\n color: string;\n accent: string;\n position: Vec;\n previousPosition: Vec;\n velocity: Vec;\n heading: number;\n speed: number;\n rpm: number;\n lap: number;\n checkpoint: number;\n lapStart: number;\n lapTimes: number[];\n invalidLap: boolean;\n offTrackTimer: number;\n skidTimer: number;\n trail: Vec[];\n ai: boolean;\n aiTarget: number;\n finished: boolean;\n};\n\nconst WORLD_WIDTH = 2400;\nconst WORLD_HEIGHT = 1600;\nconst TRACK_WIDTH = 250;\nconst ROAD_HALF_WIDTH = TRACK_WIDTH / 2;\nconst PLAYER_MAX_SPEED = 710;\nconst AI_MAX_SPEED = 610;\nconst TOTAL_LAPS = 3;\nconst FINISH_LINE_X = 1280;\nconst FINISH_LINE_Y = 360;\nconst FINISH_HALF_WIDTH = 118;\n\nconst racingLine: Vec[] = [\n { x: 1280, y: 360 },\n { x: 1680, y: 360 },\n { x: 2030, y: 560 },\n { x: 2000, y: 900 },\n { x: 1650, y: 1110 },\n { x: 1320, y: 1260 },\n { x: 890, y: 1200 },\n { x: 520, y: 950 },\n { x: 420, y: 650 },\n { x: 690, y: 410 },\n { x: 1040, y: 340 },\n];\n\nconst checkpoints: Checkpoint[] = [\n { position: { x: 1830, y: 440 }, radius: 180, label: 'Sector 1' },\n { position: { x: 1780, y: 1040 }, radius: 200, label: 'Sector 2' },\n { position: { x: 780, y: 1090 }, radius: 210, label: 'Sector 3' },\n { position: { x: 760, y: 430 }, radius: 185, label: 'Back Straight' },\n];\n\nconst keys = new Set<string>();\nconst pointer = { x: 0, y: 0 };\n\nconst app = document.querySelector<HTMLDivElement>('#app');\nif (!app) {\n throw new Error('App root not found');\n}\n\napp.innerHTML = `\n <main class=\"game-shell\" aria-label=\"Apex Circuit Racing\">\n }\n }\n}\n\nfunction updateTrail(car: Car): void {\n if (car.skidTimer > 0 || car.speed > 520) {\n car.trail.push({ ...car.position });\n if (car.trail.length > 22) {\n car.trail.shift();\n }\n } else if (car.trail.length > 0) {\n car.trail.shift();\n }\n}\n\nfunction render(now: number): void {\n ctx.clearRect(0, 0, canvasWidth, canvasHeight);\n const camera = getCamera();\n ctx.save();\n ctx.translate(canvasWidth / 2, canvasHeight / 2);\n ctx.scale(camera.zoom, camera.zoom);\n ctx.translate(-camera.x, -camera.y);\n\n drawWorldBackground(now);\n drawTrack();\n drawCheckpoints();\n drawCars();\n\n ctx.restore();\n drawVignette();\n}\ntotal 40\ndrwxr-xr-x@ 9 rok staff 288 Apr 27 15:14 .\ndrwxr-xr-x@ 4 rok staff 128 Apr 27 15:10 ..\n-rw-r--r--@ 1 rok staff 2059 Apr 27 15:14 README.md\n-rw-r--r--@ 1 rok staff 473 Apr 27 15:11 index.html\ndrwxr-xr-x@ 22 rok staff 704 Apr 27 15:14 node_modules\n-rw-r--r--@ 1 rok staff 299 Apr 27 15:14 package.json\ndrwxr-xr-x@ 4 rok staff 128 Apr 27 15:13 src\n-rw-r--r--@ 1 rok staff 503 Apr 27 15:14 tsconfig.json\n-rw-r--r--@ 1 rok staff 231 Apr 27 15:11 vite.config.ts\n","truncated":false,"original_token_count":757,"token_count":757},{"step":22,"type":"action","timestamp":1777328100.054,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"sed -n '150,220p' src/main.ts\""},"content":"Executing: /bin/zsh -lc \"sed -n '150,220p' src/main.ts\"","token_count":29},{"step":23,"type":"error","timestamp":1777328100.106,"content":"}\n\nconst ctx = canvas.getContext('2d');\nif (!ctx) {\n throw new Error('2D context not available');\n}\n\nconst hud = {\n speed: document.querySelector<HTMLElement>('#speedValue'),\n gear: document.querySelector<HTMLElement>('#gearValue'),\n rpm: document.querySelector<HTMLElement>('#rpmValue'),\n rpmBar: document.querySelector<HTMLElement>('#rpmBar'),\n lap: document.querySelector<HTMLElement>('#lapValue'),\n valid: document.querySelector<HTMLElement>('#validValue'),\n best: document.querySelector<HTMLElement>('#bestValue'),\n position: document.querySelector<HTMLElement>('#positionValue'),\n currentLap: document.querySelector<HTMLElement>('#currentLapValue'),\n checkpoint: document.querySelector<HTMLElement>('#checkpointValue'),\n sound: document.querySelector<HTMLElement>('#soundValue'),\n message: document.querySelector<HTMLElement>('#raceMessage'),\n messageTitle: document.querySelector<HTMLElement>('#messageTitle'),\n messageCopy: document.querySelector<HTMLElement>('#messageCopy'),\n};\n\nlet canvasWidth = 0;\nlet canvasHeight = 0;\nlet pixelRatio = 1;\nlet phase: RacePhase = 'ready';\nlet countdown = 0;\nlet raceStart = 0;\nlet lastTime = performance.now();\nlet audioEnabled = false;\nlet audioContext: AudioContext | null = null;\nlet engineOscillator: OscillatorNode | null = null;\nlet engineGain: GainNode | null = null;\nlet beepCooldown = 0;\n\nconst player = createCar('You', '#f43f5e', '#fef2f2', { x: FINISH_LINE_X - 22, y: FINISH_LINE_Y + 34 }, -Math.PI / 2, false);\nconst cars: Car[] = [\n player,\n createCar('Vega', '#33afbc', '#ecfeff', { x: FINISH_LINE_X - 72, y: FINISH_LINE_Y + 72 }, -Math.PI / 2, true),\n createCar('Blaze', '#fb923c', '#fff7ed', { x: FINISH_LINE_X + 34, y: FINISH_LINE_Y + 82 }, -Math.PI / 2, true),\n createCar('Nova', '#a78bfa', '#faf5ff', { x: FINISH_LINE_X + 86, y: FINISH_LINE_Y + 124 }, -Math.PI / 2, true),\n];\n\nwindow.addEventListener('resize', resize);\nwindow.addEventListener('keydown', handleKeyDown);\nwindow.addEventListener('keyup', (event) => keys.delete(event.key.toLowerCase()));\ncanvas.addEventListener('pointermove', (event) => {\n pointer.x = event.clientX;\n pointer.y = event.clientY;\n});\n\nresize();\nrequestAnimationFrame(loop);\n\nfunction createCar(name: string, color: string, accent: string, position: Vec, heading: number, ai: boolean): Car {\n return {\n name,\n color,\n accent,\n position: { ...position },\n previousPosition: { ...position },\n velocity: { x: 0, y: 0 },\n heading,\n speed: 0,\n rpm: 0,\n lap: 1,\n checkpoint: 0,\n lapStart: 0,\n lapTimes: [],\n","truncated":false,"original_token_count":640,"token_count":640},{"step":24,"type":"action","timestamp":1777328104.051,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"sed -n '130,165p' src/main.ts\""},"content":"Executing: /bin/zsh -lc \"sed -n '130,165p' src/main.ts\"","token_count":29},{"step":25,"type":"error","timestamp":1777328104.103,"content":" <span class=\"key\">A,D / ←,→ steer</span>\n <span class=\"key\">Space handbrake</span>\n <span class=\"key\">P pause</span>\n <span class=\"key\">R restart</span>\n <span class=\"key\">M sound</span>\n </div>\n </div>\n <div class=\"panel status-panel\">\n <div class=\"status-line\"><span>Position</span><strong id=\"positionValue\">P1</strong></div>\n <div class=\"status-line\"><span>Current</span><strong id=\"currentLapValue\">00:00.000</strong></div>\n <div class=\"status-line\"><span>Checkpoint</span><strong id=\"checkpointValue\">1 / 4</strong></div>\n <div class=\"status-line\"><span>Sound</span><strong id=\"soundValue\">Off</strong></div>\n </div>\n </section>\n </main>\n`;\n\nconst canvas = document.querySelector<HTMLCanvasElement>('.race-canvas');\nif (!canvas) {\n throw new Error('Canvas not found');\n}\n\nconst ctx = canvas.getContext('2d');\nif (!ctx) {\n throw new Error('2D context not available');\n}\n\nconst hud = {\n speed: document.querySelector<HTMLElement>('#speedValue'),\n gear: document.querySelector<HTMLElement>('#gearValue'),\n rpm: document.querySelector<HTMLElement>('#rpmValue'),\n rpmBar: document.querySelector<HTMLElement>('#rpmBar'),\n lap: document.querySelector<HTMLElement>('#lapValue'),\n valid: document.querySelector<HTMLElement>('#validValue'),\n best: document.querySelector<HTMLElement>('#bestValue'),\n position: document.querySelector<HTMLElement>('#positionValue'),\n","truncated":false,"original_token_count":368,"token_count":368},{"step":26,"type":"error","timestamp":1777328114.576,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"apply_patch <<'PATCH'\n*** Begin Patch\n*** Update File: src/main.ts\n@@\n-const canvas = document.querySelector<HTMLCanvasElement>('.race-canvas');\n-if (\"'!canvas) {\n- throw new Error('\"'Canvas not found');\n-}\n-\n-const ctx = canvas.getContext('2d');\n-if (\"'!ctx) {\n- throw new Error('\"'2D context not available');\n-}\n+const canvas = getRequiredElement<HTMLCanvasElement>('.race-canvas');\n+const ctx = getRequiredContext(canvas);\n@@\n function createCar(name: string, color: string, accent: string, position: Vec, heading: number, ai: boolean): Car {\n@@\n }\n+\n+function getRequiredElement<T extends Element>(selector: string): T {\n+ const element = document.querySelector<T>(selector);\n+ if (\"'!element) {\n+ throw new Error(`Required element not found: ${selector}`);\n+ }\n+ return element;\n+}\n+\n+function getRequiredContext(targetCanvas: HTMLCanvasElement): CanvasRenderingContext2D {\n+ const context = targetCanvas.getContext('\"'2d');\n+ if (\"'!context) {\n+ throw new Error('\"'2D context not available');\n+ }\n+ return context;\n+}\n*** Add File: src/vite-env.d.ts\n+/*\n+ * Copyright (c) 2026.\n+ * Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License.\n+ */\n+/// <reference types=\\\"vite/client\\\" />\n*** End Patch\nPATCH\nrm -rf node_modules package-lock.json\""},"content":"Executing: /bin/zsh -lc \"apply_patch <<'PATCH'\n*** Begin Patch\n*** Update File: src/main.ts\n@@\n-const canvas = document.querySelector<HTMLCanvasElement>('.race-canvas');\n-if (\"'!canvas) {\n- throw new Error('\"'Canvas not found');\n-}\n-\n-const ctx = canvas.getContext('2d');\n-if (\"'!ctx) {\n- throw new Error('\"'2D context not available');\n-}\n+const canvas = getRequiredElement<HTMLCanvasElement>('.race-canvas');\n+const ctx = getRequiredContext(canvas);\n@@\n function createCar(name: string, color: string, accent: string, position: Vec, heading: number, ai: boolean): Car {\n@@\n }\n+\n+function getRequiredElement<T extends Element>(selector: string): T {\n+ const element = document.querySelector<T>(selector);\n+ if (\"'!element) {\n+ throw new Error(`Required element not found: ${selector}`);\n+ }\n+ return element;\n+}\n+\n+function getRequiredContext(targetCanvas: HTMLCanvasElement): CanvasRenderingContext2D {\n+ const context = targetCanvas.getContext('\"'2d');\n+ if (\"'!context) {\n+ throw new Error('\"'2D context not available');\n+ }\n+ return context;\n+}\n*** Add File: src/vite-env.d.ts\n+/*\n+ * Copyright (c) 2026.\n+ * Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License.\n+ */\n+/// <reference types=\\\"vite/client\\\" />\n*** End Patch\nPATCH\nrm -rf node_modules package-lock.json\"","token_count":675},{"step":27,"type":"observation","timestamp":1777328114.628,"content":"Success. Updated the following files:\nA src/vite-env.d.ts\nM src/main.ts\n","truncated":false,"original_token_count":18,"token_count":18},{"step":28,"type":"thought","timestamp":1777328119.112,"content":"The nullability fixes are in. I’ll rerun the build and clean temporary install artifacts afterward.","token_count":25},{"step":29,"type":"action","timestamp":1777328119.835,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc 'npm install --no-package-lock && npm run build; code=$?; rm -rf node_modules package-lock.json; exit $code'"},"content":"Executing: /bin/zsh -lc 'npm install --no-package-lock && npm run build; code=$?; rm -rf node_modules package-lock.json; exit $code'","token_count":67},{"step":30,"type":"observation","timestamp":1777328123.384,"content":"\nadded 16 packages, and audited 17 packages in 1s\n\n8 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities\n\n> browser-racing-game@0.1.0 build\n> tsc && vite build\n\nvite v8.0.10 building client environment for production...\n\u001b[2K\rtransforming...✓ 5 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.57 kB │ gzip: 0.37 kB\ndist/assets/index-DMwD2rvk.css 4.52 kB │ gzip: 1.69 kB\ndist/assets/index-c4w_9u7Y.js 16.82 kB │ gzip: 6.39 kB\n\n✓ built in 299ms\n","truncated":false,"original_token_count":133,"token_count":133},{"step":31,"type":"thought","timestamp":1777328132.116,"content":"Build passes. I’ll remove generated build artifacts so only scoped source files remain.","token_count":22},{"step":32,"type":"action","timestamp":1777328138.621,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc \"node -e \\\"const fs=require('fs'); for (const p of ['dist','node_modules','package-lock.json']) if (fs.existsSync(p)) fs.rmSync(p,{recursive:true,force:true});\\\" && find . -maxdepth 3 -print | sort\""},"content":"Executing: /bin/zsh -lc \"node -e \\\"const fs=require('fs'); for (const p of ['dist','node_modules','package-lock.json']) if (fs.existsSync(p)) fs.rmSync(p,{recursive:true,force:true});\\\" && find . -maxdepth 3 -print | sort\"","token_count":114},{"step":33,"type":"observation","timestamp":1777328138.723,"content":".\n./README.md\n./index.html\n./package.json\n./src\n./src/main.ts\n./src/style.css\n./src/vite-env.d.ts\n./tsconfig.json\n./vite.config.ts\n","truncated":false,"original_token_count":33,"token_count":33},{"step":34,"type":"thought","timestamp":1777328142.95,"content":"Temporary artifacts are gone. I’ll do a final status check for the touched files.","token_count":21},{"step":35,"type":"action","timestamp":1777328143.463,"tool":"command_execution","arguments":{"command":"/bin/zsh -lc 'git status --short -- package.json index.html src README.md tsconfig.json vite.config.ts && git diff --stat -- package.json index.html src README.md tsconfig.json vite.config.ts'"},"content":"Executing: /bin/zsh -lc 'git status --short -- package.json index.html src README.md tsconfig.json vite.config.ts && git diff --stat -- package.json index.html src README.md tsconfig.json vite.config.ts'","token_count":103},{"step":36,"type":"observation","timestamp":1777328143.515,"content":"","truncated":false,"original_token_count":0,"token_count":1}]}
|
|
6
|
+
{"envelope_id":"env_d7a2ca46-d8c4-46c5-8fff-9b3241294a73","session_id":"sess_ff149191-44eb-42e2-b97a-e63d5b42be14","chunk_sequence":1,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"d195ba3f4a86d797daca0d00e44cd3f1c232e25bef456c9cc63d555cefa80468","sequence":1,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"gpt-5.5","provider":"codex","agent_role":"frontend","agent_id":"117955e3","task_complexity":"medium","team_size":1,"session_quality":0,"groove_version":"0.27.110","leaf_context":null},"trajectory_log":[{"step":37,"type":"thought","timestamp":1777328155.739,"content":"Implemented a complete Vite + TypeScript canvas racing game.\n\n**What Changed**\n- Added Vite/TypeScript project scaffolding in `package.json:1`, `tsconfig.json:1`, `vite.config.ts:1`, and `index.html:1`.\n- Built the full racing game loop, physics, AI opponents, collisions, lap validation, countdown, pause/restart, HUD, and audio toggle in `src/main.ts:1`.\n- Added responsive polished UI/HUD styling, track visuals, telemetry panels, and mobile layout in `src/style.css:1`.\n- Added Vite CSS import typing in `src/vite-env.d.ts:1`.\n- Documented controls, features, and dev commands in `README.md:1`.\n\n**Validation**\n- Ran `npm install --no-package-lock && npm run build` successfully.\n- Cleaned generated `node_modules`, `dist`, and lockfile artifacts afterward to stay within scope.\n\nRun it with:\n- `npm install`\n- `npm run dev`","token_count":207},{"step":38,"type":"resolution","timestamp":1777328155.913,"content":"","token_count":1}]}
|
|
7
|
+
{"envelope_id":"env_1a4360b3-c9b2-4b65-b37d-2c34dca1305b","session_id":"sess_ff149191-44eb-42e2-b97a-e63d5b42be14","type":"SESSION_CLOSE","attestation":{"session_hmac":"139f45470ff09ffcb1f5bf6dbefc95922a5429c32374342eeb6279f823de7f7a","sequence":2,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"gpt-5.5","provider":"codex","agent_role":"frontend","agent_id":"117955e3","task_complexity":"medium","team_size":1,"session_quality":80,"groove_version":"0.27.110","leaf_context":null,"domain_tags":{"primary":{"domain":"react_frontend","confidence":0.0667},"secondary":{"domain":"python","confidence":0},"tertiary":{"domain":"typescript_node","confidence":0}},"session_embedding":null},"outcome":{"status":"SUCCESS","session_quality":80,"quality_tier":"TIER_B","quality_tier_reason":"moderate_quality","user_interventions":0,"total_steps":38,"total_chunks":2,"total_tokens":4719,"duration_seconds":310,"files_modified":0,"errors_encountered":6,"errors_recovered":1,"coordination_events":0,"training_eligible":true,"training_exclusion_reason":null}}
|
|
8
|
+
{"envelope_id":"env_e45e63b4-7d26-4f75-8422-8fdbcc7be150","session_id":"sess_d613fd56-c70a-4445-bfa6-0e2442f0ac88","chunk_sequence":0,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"c961f4b54be616402d2e80afb8bdd4ab61e813495e88156d1b9f91c3d865d9b0","sequence":0,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"planner","agent_id":"18664b07","task_complexity":"medium","team_size":1,"session_quality":0,"groove_version":"0.27.110","leaf_context":null},"trajectory_log":[{"step":1,"type":"thought","timestamp":1777329693.088,"content":"I'm planner-7, your planning agent — I'll design team structures and route tasks to the right agents. What would you like me to work on?","token_count":34},{"step":2,"type":"resolution","timestamp":1777329693.132,"content":"I'm planner-7, your planning agent — I'll design team structures and route tasks to the right agents. What would you like me to work on?","token_count":34}]}
|
|
9
|
+
{"envelope_id":"env_6d9f9889-180c-4525-a21f-eaf91ffe1010","session_id":"sess_d613fd56-c70a-4445-bfa6-0e2442f0ac88","type":"SESSION_CLOSE","attestation":{"session_hmac":"ac93a65f451511e9faa8951d89e5135ba48bc60226ef900e2f647ea992f0584d","sequence":1,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"planner","agent_id":"18664b07","task_complexity":"medium","team_size":1,"session_quality":50,"groove_version":"0.27.110","leaf_context":null,"domain_tags":null,"session_embedding":null},"outcome":{"status":"SUCCESS","session_quality":50,"quality_tier":"TIER_B","quality_tier_reason":"moderate_quality","user_interventions":0,"total_steps":2,"total_chunks":1,"total_tokens":68,"duration_seconds":9,"files_modified":0,"errors_encountered":0,"errors_recovered":0,"coordination_events":0,"training_eligible":false,"training_exclusion_reason":"too_few_steps"}}
|
|
@@ -1,44 +1,46 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
-
import Database from 'better-sqlite3';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
5
|
import { join } from 'node:path';
|
|
7
6
|
import { homedir } from 'node:os';
|
|
8
7
|
import { CURRENT_CONSENT_VERSION } from '../shared/constants.js';
|
|
9
8
|
|
|
9
|
+
function ensureDir(filePath) {
|
|
10
|
+
const dir = filePath.replace(/[/\\][^/\\]+$/, '');
|
|
11
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readJSON(filePath) {
|
|
15
|
+
if (!existsSync(filePath)) return null;
|
|
16
|
+
try { return JSON.parse(readFileSync(filePath, 'utf-8')); } catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeJSON(filePath, data) {
|
|
20
|
+
ensureDir(filePath);
|
|
21
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
export class ConsentManager {
|
|
11
|
-
constructor(
|
|
12
|
-
this.
|
|
13
|
-
const dir = this._dbPath.replace(/[/\\][^/\\]+$/, '');
|
|
14
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
15
|
-
this._db = new Database(this._dbPath);
|
|
16
|
-
this._db.pragma('journal_mode = WAL');
|
|
17
|
-
this._db.exec(`
|
|
18
|
-
CREATE TABLE IF NOT EXISTS consent_history (
|
|
19
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
-
user_id TEXT NOT NULL,
|
|
21
|
-
opted_in INTEGER NOT NULL,
|
|
22
|
-
consent_version TEXT NOT NULL,
|
|
23
|
-
metadata TEXT,
|
|
24
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
25
|
-
)
|
|
26
|
-
`);
|
|
25
|
+
constructor(consentPath) {
|
|
26
|
+
this._path = consentPath || join(homedir(), '.groove', 'consent.json');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
recordConsent(userId, optedIn, consentVersion
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
recordConsent(userId, optedIn, consentVersion) {
|
|
30
|
+
const data = {
|
|
31
|
+
user_id: userId,
|
|
32
|
+
opted_in: !!optedIn,
|
|
33
|
+
consent_version: consentVersion,
|
|
34
|
+
updated_at: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
writeJSON(this._path, data);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
isOptedIn(userId) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (row.consent_version !== CURRENT_CONSENT_VERSION) return false;
|
|
41
|
-
return row.opted_in === 1;
|
|
40
|
+
const data = readJSON(this._path);
|
|
41
|
+
if (!data) return false;
|
|
42
|
+
if (data.consent_version !== CURRENT_CONSENT_VERSION) return false;
|
|
43
|
+
return data.opted_in === true;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
revokeConsent(userId) {
|
|
@@ -46,34 +48,28 @@ export class ConsentManager {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
getOptedInCount() {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
AND consent_version = ?
|
|
53
|
-
AND id = (SELECT MAX(id) FROM consent_history ch2 WHERE ch2.user_id = ch1.user_id)
|
|
54
|
-
`).get(CURRENT_CONSENT_VERSION);
|
|
55
|
-
return row?.cnt || 0;
|
|
51
|
+
const data = readJSON(this._path);
|
|
52
|
+
if (!data || !data.opted_in || data.consent_version !== CURRENT_CONSENT_VERSION) return 0;
|
|
53
|
+
return 1;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
getConsentHistory(userId) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
const data = readJSON(this._path);
|
|
58
|
+
if (!data) return [];
|
|
59
|
+
return [{
|
|
60
|
+
user_id: data.user_id,
|
|
61
|
+
opted_in: data.opted_in,
|
|
62
|
+
consent_version: data.consent_version,
|
|
63
|
+
created_at: data.updated_at,
|
|
64
|
+
metadata: null,
|
|
65
|
+
}];
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
close() {
|
|
70
|
-
this._db.close();
|
|
71
|
-
}
|
|
68
|
+
close() {}
|
|
72
69
|
|
|
73
70
|
static getOrCreateUserId(userIdPath) {
|
|
74
71
|
const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
|
|
75
|
-
|
|
76
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
72
|
+
ensureDir(filePath);
|
|
77
73
|
if (existsSync(filePath)) {
|
|
78
74
|
return readFileSync(filePath, 'utf-8').trim();
|
|
79
75
|
}
|
|
@@ -82,15 +78,11 @@ export class ConsentManager {
|
|
|
82
78
|
return uid;
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
static isCaptureEnabled(userIdPath,
|
|
81
|
+
static isCaptureEnabled(userIdPath, consentPath) {
|
|
86
82
|
const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
|
|
87
83
|
if (!existsSync(filePath)) return false;
|
|
84
|
+
const manager = new ConsentManager(consentPath);
|
|
88
85
|
const userId = readFileSync(filePath, 'utf-8').trim();
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
return manager.isOptedIn(userId);
|
|
92
|
-
} finally {
|
|
93
|
-
manager.close();
|
|
94
|
-
}
|
|
86
|
+
return manager.isOptedIn(userId);
|
|
95
87
|
}
|
|
96
88
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
+
import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';
|
|
4
|
+
|
|
3
5
|
const DEFAULT_MODEL = 'sentence-transformers/all-MiniLM-L6-v2';
|
|
4
6
|
const DEFAULT_TOP_K = 3;
|
|
5
7
|
|
|
@@ -171,7 +173,7 @@ const DOMAIN_TAXONOMY = {
|
|
|
171
173
|
|
|
172
174
|
export class DomainTagger {
|
|
173
175
|
constructor(options = {}) {
|
|
174
|
-
this._serviceUrl = options.serviceUrl
|
|
176
|
+
this._serviceUrl = options.serviceUrl !== undefined ? options.serviceUrl : EMBEDDING_SERVICE_URL;
|
|
175
177
|
this._registryUrl = options.registryUrl || process.env.LEAF_REGISTRY_URL || null;
|
|
176
178
|
this._registry = options.registry || null;
|
|
177
179
|
this._model = options.model || DEFAULT_MODEL;
|
|
@@ -298,9 +298,10 @@ export class TrajectoryCapture {
|
|
|
298
298
|
|
|
299
299
|
if (this._domainTagger) {
|
|
300
300
|
const role = ctx.metadata.agent_role || '';
|
|
301
|
-
const firstPrompt = ctx.allSteps.find((s) => s.type === 'thought')?.content || '';
|
|
302
301
|
const thoughtSteps = ctx.allSteps.filter((s) => s.type === 'thought');
|
|
303
|
-
const
|
|
302
|
+
const firstPrompt = thoughtSteps[0]?.content || '';
|
|
303
|
+
const remainingThoughts = thoughtSteps.slice(1);
|
|
304
|
+
const routingText = DomainTagger.buildRoutingText(role, firstPrompt, remainingThoughts);
|
|
304
305
|
ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);
|
|
305
306
|
ctx.metadata.session_embedding = await this._domainTagger.embed(routingText);
|
|
306
307
|
}
|
|
@@ -39,3 +39,4 @@ export const TRAINING_EXCLUSION_REASONS = ['too_few_steps', 'no_actions', 'no_ob
|
|
|
39
39
|
export const USER_MESSAGE_MAX_CHARS = 2000;
|
|
40
40
|
|
|
41
41
|
export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';
|
|
42
|
+
export const EMBEDDING_SERVICE_URL = process.env.EMBEDDING_SERVICE_URL || `${CENTRAL_COMMAND_URL}/v1/embed`;
|
|
@@ -8,12 +8,12 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { ConsentManager } from '../../client/consent.js';
|
|
9
9
|
|
|
10
10
|
describe('ConsentManager', () => {
|
|
11
|
-
let tmpDir,
|
|
11
|
+
let tmpDir, consentPath, manager;
|
|
12
12
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
tmpDir = mkdtempSync(join(tmpdir(), 'consent-test-'));
|
|
15
|
-
|
|
16
|
-
manager = new ConsentManager(
|
|
15
|
+
consentPath = join(tmpDir, 'consent.json');
|
|
16
|
+
manager = new ConsentManager(consentPath);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
afterEach(() => {
|
|
@@ -43,19 +43,24 @@ describe('ConsentManager', () => {
|
|
|
43
43
|
|
|
44
44
|
it('getOptedInCount counts correctly', () => {
|
|
45
45
|
manager.recordConsent('user1', true, '1.0');
|
|
46
|
-
manager.
|
|
47
|
-
manager.recordConsent('user3', false, '1.0');
|
|
48
|
-
assert.equal(manager.getOptedInCount(), 2);
|
|
46
|
+
assert.equal(manager.getOptedInCount(), 1);
|
|
49
47
|
});
|
|
50
48
|
|
|
51
|
-
it('getConsentHistory returns
|
|
52
|
-
manager.recordConsent('user1', true, '1.0'
|
|
53
|
-
manager.revokeConsent('user1');
|
|
49
|
+
it('getConsentHistory returns current state', () => {
|
|
50
|
+
manager.recordConsent('user1', true, '1.0');
|
|
54
51
|
const history = manager.getConsentHistory('user1');
|
|
55
|
-
assert.equal(history.length,
|
|
52
|
+
assert.equal(history.length, 1);
|
|
56
53
|
assert.equal(history[0].opted_in, true);
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('consent.json is written with 0o600 permissions', () => {
|
|
57
|
+
manager.recordConsent('user1', true, '1.0');
|
|
58
|
+
assert.ok(existsSync(consentPath));
|
|
59
|
+
const data = JSON.parse(readFileSync(consentPath, 'utf-8'));
|
|
60
|
+
assert.equal(data.opted_in, true);
|
|
61
|
+
assert.equal(data.consent_version, '1.0');
|
|
62
|
+
assert.equal(data.user_id, 'user1');
|
|
63
|
+
assert.ok(data.updated_at);
|
|
59
64
|
});
|
|
60
65
|
});
|
|
61
66
|
|
|
@@ -96,26 +101,24 @@ describe('ConsentManager.isCaptureEnabled', () => {
|
|
|
96
101
|
});
|
|
97
102
|
|
|
98
103
|
it('returns false when no user_id file exists', () => {
|
|
99
|
-
const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, '
|
|
104
|
+
const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, 'consent.json'));
|
|
100
105
|
assert.equal(result, false);
|
|
101
106
|
});
|
|
102
107
|
|
|
103
108
|
it('returns false when user not opted in', () => {
|
|
104
109
|
const uidPath = join(tmpDir, 'user_id');
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
|
|
110
|
+
ConsentManager.getOrCreateUserId(uidPath);
|
|
111
|
+
const result = ConsentManager.isCaptureEnabled(uidPath, join(tmpDir, 'consent.json'));
|
|
108
112
|
assert.equal(result, false);
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
it('returns true when user is opted in', () => {
|
|
112
116
|
const uidPath = join(tmpDir, 'user_id');
|
|
113
117
|
const uid = ConsentManager.getOrCreateUserId(uidPath);
|
|
114
|
-
const
|
|
115
|
-
const mgr = new ConsentManager(
|
|
118
|
+
const consentPath = join(tmpDir, 'consent.json');
|
|
119
|
+
const mgr = new ConsentManager(consentPath);
|
|
116
120
|
mgr.recordConsent(uid, true, '1.0');
|
|
117
|
-
|
|
118
|
-
const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
|
|
121
|
+
const result = ConsentManager.isCaptureEnabled(uidPath, consentPath);
|
|
119
122
|
assert.equal(result, true);
|
|
120
123
|
});
|
|
121
124
|
});
|
|
@@ -8,7 +8,7 @@ describe('DomainTagger', () => {
|
|
|
8
8
|
let tagger;
|
|
9
9
|
|
|
10
10
|
beforeEach(async () => {
|
|
11
|
-
tagger = new DomainTagger();
|
|
11
|
+
tagger = new DomainTagger({ serviceUrl: null });
|
|
12
12
|
await tagger.init();
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -176,7 +176,7 @@ describe('DomainTagger', () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it('returns null when not initialized', async () => {
|
|
179
|
-
const uninit = new DomainTagger();
|
|
179
|
+
const uninit = new DomainTagger({ serviceUrl: null });
|
|
180
180
|
const result = await uninit.tag('Build a Python Django app');
|
|
181
181
|
assert.equal(result, null);
|
|
182
182
|
});
|
|
@@ -203,6 +203,7 @@ describe('DomainTagger', () => {
|
|
|
203
203
|
|
|
204
204
|
it('accepts registry option and falls back to keyword mode without embedding service', async () => {
|
|
205
205
|
const registryTagger = new DomainTagger({
|
|
206
|
+
serviceUrl: null,
|
|
206
207
|
registry: [
|
|
207
208
|
{ id: 'quantum_computing', domain_description: 'Quantum computing, qubits, quantum gates' },
|
|
208
209
|
{ id: 'bioinformatics', domain_description: 'Biology, genomics, DNA analysis' },
|
|
@@ -215,6 +216,7 @@ describe('DomainTagger', () => {
|
|
|
215
216
|
|
|
216
217
|
it('accepts registryUrl option and falls back to keyword mode when unavailable', async () => {
|
|
217
218
|
const registryTagger = new DomainTagger({
|
|
219
|
+
serviceUrl: null,
|
|
218
220
|
registryUrl: 'http://localhost:99999/api/leaves',
|
|
219
221
|
});
|
|
220
222
|
await registryTagger.init();
|
|
@@ -223,7 +225,7 @@ describe('DomainTagger', () => {
|
|
|
223
225
|
});
|
|
224
226
|
|
|
225
227
|
it('uses expanded taxonomy — has 40 domains by default', async () => {
|
|
226
|
-
const defaultTagger = new DomainTagger();
|
|
228
|
+
const defaultTagger = new DomainTagger({ serviceUrl: null });
|
|
227
229
|
await defaultTagger.init();
|
|
228
230
|
assert.ok(defaultTagger._domains.length >= 35, `Expected 35+ domains, got ${defaultTagger._domains.length}`);
|
|
229
231
|
});
|
|
@@ -239,7 +241,7 @@ describe('DomainTagger', () => {
|
|
|
239
241
|
});
|
|
240
242
|
|
|
241
243
|
it('embed() returns null when not initialized', async () => {
|
|
242
|
-
const uninit = new DomainTagger();
|
|
244
|
+
const uninit = new DomainTagger({ serviceUrl: null });
|
|
243
245
|
const result = await uninit.embed('Build a Python app');
|
|
244
246
|
assert.equal(result, null);
|
|
245
247
|
});
|