gsd-pi 2.19.0 → 2.20.0
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/README.md +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +424 -30
- package/dist/resources/extensions/gsd/commands.ts +518 -36
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +39 -3
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/dist/resources/extensions/gsd/preferences.ts +125 -150
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/dist/resources/extensions/remote-questions/format.ts +154 -8
- package/dist/resources/extensions/remote-questions/manager.ts +9 -7
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +424 -30
- package/src/resources/extensions/gsd/commands.ts +518 -36
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +39 -3
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/src/resources/extensions/gsd/preferences.ts +125 -150
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/src/resources/extensions/remote-questions/format.ts +154 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -7
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -1,35 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
// Only rewrites relative imports from our own source files — not from node_modules.
|
|
3
|
-
//
|
|
4
|
-
// Handles two patterns:
|
|
5
|
-
// 1. .js → .ts (pi bundler convention: source files use .js specifiers)
|
|
6
|
-
// 2. extensionless → .ts (some source files omit extensions in relative imports)
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const isFromNodeModules = parentURL.includes('/node_modules/');
|
|
11
|
-
const isFromPackages = parentURL.includes('/packages/');
|
|
12
|
-
|
|
13
|
-
if (!isFromNodeModules && !isFromPackages && !specifier.startsWith('node:')) {
|
|
14
|
-
// Rewrite .js → .ts
|
|
15
|
-
if (specifier.endsWith('.js')) {
|
|
16
|
-
const tsSpecifier = specifier.replace(/\.js$/, '.ts');
|
|
17
|
-
try {
|
|
18
|
-
return nextResolve(tsSpecifier, context);
|
|
19
|
-
} catch {
|
|
20
|
-
// fall through to default resolution
|
|
21
|
-
}
|
|
22
|
-
}
|
|
3
|
+
const ROOT = new URL("../../../../../", import.meta.url);
|
|
4
|
+
const PACKAGES_ROOT = fileURLToPath(new URL("packages/", ROOT));
|
|
23
5
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
6
|
+
export function resolve(specifier, context, nextResolve) {
|
|
7
|
+
let tsSpecifier = specifier;
|
|
8
|
+
if (specifier.includes('@gsd/')) {
|
|
9
|
+
tsSpecifier = specifier.replace('@gsd/', PACKAGES_ROOT).replace('/dist/', '/src/');
|
|
10
|
+
if (tsSpecifier.includes('/packages/pi-ai') && !tsSpecifier.endsWith('.ts')) {
|
|
11
|
+
tsSpecifier = tsSpecifier.replace(/\/packages\/pi-ai$/, '/packages/pi-ai/src/index.ts');
|
|
12
|
+
} else if (!tsSpecifier.includes('/src/') && !tsSpecifier.endsWith('.ts')) {
|
|
13
|
+
// Fallback for other gsd packages like pi-coding-agent, pi-tui, pi-agent-core
|
|
14
|
+
tsSpecifier = tsSpecifier.replace(/\/packages\/([^\/]+)$/, '/packages/$1/src/index.ts');
|
|
15
|
+
} else if (!tsSpecifier.endsWith('.ts') && !tsSpecifier.endsWith('.js') && !tsSpecifier.endsWith('.mjs')) {
|
|
16
|
+
tsSpecifier += '/index.ts';
|
|
31
17
|
}
|
|
18
|
+
} else if (specifier.endsWith('.js')) {
|
|
19
|
+
tsSpecifier = specifier.replace(/\.js$/, '.ts');
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
return nextResolve(
|
|
22
|
+
return nextResolve(tsSpecifier, context);
|
|
35
23
|
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
// Custom ESM resolver: rewrites .js imports to .ts for node --test with TypeScript sources.
|
|
2
|
-
// Usage: node --import ./agent/extensions/gsd/tests/resolve-ts.mjs --test ...
|
|
3
|
-
//
|
|
4
|
-
// This is needed because pi extension source files use .js import specifiers
|
|
5
|
-
// (the pi runtime bundler convention), but only .ts files exist on disk.
|
|
6
|
-
// Node's built-in TypeScript support strips types but doesn't rewrite specifiers.
|
|
7
|
-
|
|
8
1
|
import { register } from 'node:module';
|
|
9
2
|
import { pathToFileURL } from 'node:url';
|
|
10
3
|
|
|
11
|
-
|
|
4
|
+
// Register hook to redirect imports to the dist directory
|
|
5
|
+
register(new URL('./dist-redirect.mjs', import.meta.url), pathToFileURL('./'));
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for skill telemetry and skill health (#599).
|
|
3
|
+
* Tests the pure functions — no file I/O, no extension context.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, beforeEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import type { UnitMetrics } from "../metrics.js";
|
|
9
|
+
|
|
10
|
+
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function makeUnit(overrides: Partial<UnitMetrics> = {}): UnitMetrics {
|
|
13
|
+
return {
|
|
14
|
+
type: "execute-task",
|
|
15
|
+
id: "M001/S01/T01",
|
|
16
|
+
model: "claude-sonnet-4-20250514",
|
|
17
|
+
startedAt: 1000,
|
|
18
|
+
finishedAt: 2000,
|
|
19
|
+
tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
|
|
20
|
+
cost: 0.05,
|
|
21
|
+
toolCalls: 3,
|
|
22
|
+
assistantMessages: 5,
|
|
23
|
+
userMessages: 2,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Skill Telemetry ──────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
describe("skill-telemetry", () => {
|
|
31
|
+
// Note: captureAvailableSkills/getAndClearSkills depend on filesystem (getAgentDir)
|
|
32
|
+
// so we test the data flow via getSkillLastUsed and detectStaleSkills which are pure
|
|
33
|
+
|
|
34
|
+
it("getSkillLastUsed returns most recent timestamp per skill", async () => {
|
|
35
|
+
const { getSkillLastUsed } = await import("../skill-telemetry.js");
|
|
36
|
+
|
|
37
|
+
const units = [
|
|
38
|
+
makeUnit({ finishedAt: 1000, skills: ["rust-core", "axum-web-framework"] }),
|
|
39
|
+
makeUnit({ finishedAt: 2000, skills: ["rust-core"] }),
|
|
40
|
+
makeUnit({ finishedAt: 3000, skills: ["axum-web-framework"] }),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const result = getSkillLastUsed(units);
|
|
44
|
+
assert.equal(result.get("rust-core"), 2000);
|
|
45
|
+
assert.equal(result.get("axum-web-framework"), 3000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("getSkillLastUsed returns empty map for units without skills", async () => {
|
|
49
|
+
const { getSkillLastUsed } = await import("../skill-telemetry.js");
|
|
50
|
+
|
|
51
|
+
const units = [makeUnit(), makeUnit()];
|
|
52
|
+
const result = getSkillLastUsed(units);
|
|
53
|
+
assert.equal(result.size, 0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── Skill Health ─────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe("skill-health", () => {
|
|
60
|
+
it("buildHealSkillPrompt includes unit ID", async () => {
|
|
61
|
+
const { buildHealSkillPrompt } = await import("../skill-health.js");
|
|
62
|
+
const prompt = buildHealSkillPrompt("M001/S01/T01");
|
|
63
|
+
assert.ok(prompt.includes("M001/S01/T01"));
|
|
64
|
+
assert.ok(prompt.includes("Skill Heal Analysis"));
|
|
65
|
+
assert.ok(prompt.includes("skill-review-queue.md"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("computeStaleAvoidList excludes already-avoided skills", async () => {
|
|
69
|
+
// This test requires filesystem access for loadLedgerFromDisk
|
|
70
|
+
// so we test the filtering logic conceptually
|
|
71
|
+
const { computeStaleAvoidList } = await import("../skill-health.js");
|
|
72
|
+
|
|
73
|
+
// With no metrics file, should return empty
|
|
74
|
+
const result = computeStaleAvoidList("/nonexistent/path", ["some-skill"]);
|
|
75
|
+
assert.ok(Array.isArray(result));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ─── UnitMetrics skills field ─────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe("UnitMetrics skills field", () => {
|
|
82
|
+
it("skills field is optional and accepts string array", () => {
|
|
83
|
+
const unit = makeUnit({ skills: ["rust-core", "axum-web-framework"] });
|
|
84
|
+
assert.deepEqual(unit.skills, ["rust-core", "axum-web-framework"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("skills field is undefined when not provided", () => {
|
|
88
|
+
const unit = makeUnit();
|
|
89
|
+
assert.equal(unit.skills, undefined);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─── Preferences ──────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
describe("skill_staleness_days preference", () => {
|
|
96
|
+
it("validates valid staleness days", async () => {
|
|
97
|
+
const { validatePreferences } = await import("../preferences.js");
|
|
98
|
+
|
|
99
|
+
const result = validatePreferences({ skill_staleness_days: 30 });
|
|
100
|
+
assert.equal(result.preferences.skill_staleness_days, 30);
|
|
101
|
+
assert.equal(result.errors.length, 0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("validates zero (disabled) staleness days", async () => {
|
|
105
|
+
const { validatePreferences } = await import("../preferences.js");
|
|
106
|
+
|
|
107
|
+
const result = validatePreferences({ skill_staleness_days: 0 });
|
|
108
|
+
assert.equal(result.preferences.skill_staleness_days, 0);
|
|
109
|
+
assert.equal(result.errors.length, 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects negative staleness days", async () => {
|
|
113
|
+
const { validatePreferences } = await import("../preferences.js");
|
|
114
|
+
|
|
115
|
+
const result = validatePreferences({ skill_staleness_days: -5 });
|
|
116
|
+
assert.equal(result.preferences.skill_staleness_days, undefined);
|
|
117
|
+
assert.ok(result.errors.some(e => e.includes("skill_staleness_days")));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("floors fractional days", async () => {
|
|
121
|
+
const { validatePreferences } = await import("../preferences.js");
|
|
122
|
+
|
|
123
|
+
const result = validatePreferences({ skill_staleness_days: 30.7 });
|
|
124
|
+
assert.equal(result.preferences.skill_staleness_days, 30);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -4,7 +4,7 @@ import { mkdirSync, rmSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
8
8
|
|
|
9
9
|
import { writeFileSync } from "node:fs";
|
|
10
10
|
import {
|
|
@@ -25,6 +25,27 @@ function cleanup(base: string): void {
|
|
|
25
25
|
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function waitForChildExit(child: ChildProcess, timeoutMs = 5000): Promise<number | null> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
if (child.exitCode !== null) {
|
|
31
|
+
resolve(child.exitCode);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
child.off("exit", onExit);
|
|
37
|
+
resolve(child.exitCode);
|
|
38
|
+
}, timeoutMs);
|
|
39
|
+
|
|
40
|
+
const onExit = (code: number | null) => {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
resolve(code);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
child.once("exit", onExit);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
// ─── stopAutoRemote ──────────────────────────────────────────────────────
|
|
29
50
|
|
|
30
51
|
test("stopAutoRemote returns found:false when no lock file exists", () => {
|
|
@@ -63,12 +84,16 @@ test("stopAutoRemote sends SIGTERM to a live process and returns found:true", as
|
|
|
63
84
|
const base = makeTmpBase();
|
|
64
85
|
|
|
65
86
|
// Spawn a child process that sleeps, acting as a fake auto-mode session
|
|
66
|
-
const child =
|
|
67
|
-
|
|
68
|
-
["process.on('SIGTERM', () => process.exit(0)); setTimeout(() => process.exit(1), 30000);"],
|
|
87
|
+
const child = spawn(
|
|
88
|
+
process.execPath,
|
|
89
|
+
["-e", "process.on('SIGTERM', () => process.exit(0)); setTimeout(() => process.exit(1), 30000);"],
|
|
69
90
|
{ stdio: "ignore", detached: false },
|
|
70
91
|
);
|
|
71
92
|
|
|
93
|
+
if (!child.pid) {
|
|
94
|
+
throw new Error("failed to spawn child process for stopAutoRemote test");
|
|
95
|
+
}
|
|
96
|
+
|
|
72
97
|
try {
|
|
73
98
|
// Wait for child to be ready
|
|
74
99
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
@@ -84,15 +109,13 @@ test("stopAutoRemote sends SIGTERM to a live process and returns found:true", as
|
|
|
84
109
|
};
|
|
85
110
|
writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2), "utf-8");
|
|
86
111
|
|
|
112
|
+
const exitPromise = waitForChildExit(child);
|
|
87
113
|
const result = stopAutoRemote(base);
|
|
88
114
|
assert.equal(result.found, true, "should find running auto-mode");
|
|
89
115
|
assert.equal(result.pid, child.pid, "should return the PID");
|
|
90
116
|
|
|
91
117
|
// Wait for child to exit (it should receive SIGTERM)
|
|
92
|
-
const exitCode = await
|
|
93
|
-
child.on("exit", (code) => resolve(code));
|
|
94
|
-
setTimeout(() => resolve(null), 5000);
|
|
95
|
-
});
|
|
118
|
+
const exitCode = await exitPromise;
|
|
96
119
|
// On Windows, SIGTERM is not interceptable — the process exits with code 1
|
|
97
120
|
// rather than running the handler. Accept either clean exit (0) or forced (1).
|
|
98
121
|
assert.ok(exitCode !== null, "child should have exited after SIGTERM");
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// Token Savings Validation Test
|
|
2
|
+
//
|
|
3
|
+
// Proves ≥30% character savings when using DB-scoped content vs full-markdown
|
|
4
|
+
// for planning/research prompt types. Uses realistic fixture data:
|
|
5
|
+
// 24 decisions across 3 milestones, 21 requirements across 5 slices in 2 milestones.
|
|
6
|
+
//
|
|
7
|
+
// Retires R016 (≥30% savings target) and provides evidence for R019 (no quality regression).
|
|
8
|
+
|
|
9
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
import { openDatabase, closeDatabase } from '../gsd-db.ts';
|
|
14
|
+
import { migrateFromMarkdown } from '../md-importer.ts';
|
|
15
|
+
import {
|
|
16
|
+
queryDecisions,
|
|
17
|
+
queryRequirements,
|
|
18
|
+
formatDecisionsForPrompt,
|
|
19
|
+
formatRequirementsForPrompt,
|
|
20
|
+
} from '../context-store.ts';
|
|
21
|
+
import { createTestContext } from './test-helpers.ts';
|
|
22
|
+
|
|
23
|
+
const { assertEq, assertTrue, assertMatch, assertNoMatch, report } = createTestContext();
|
|
24
|
+
|
|
25
|
+
// ─── Fixture Generators ────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a realistic DECISIONS.md with `count` decisions spread across milestones.
|
|
29
|
+
* Each decision has realistic-length text in each column to produce meaningful size.
|
|
30
|
+
*/
|
|
31
|
+
function generateDecisionsMarkdown(count: number, milestones: string[]): string {
|
|
32
|
+
const lines: string[] = [
|
|
33
|
+
'# Decisions Register',
|
|
34
|
+
'',
|
|
35
|
+
'<!-- Append-only. Never edit or remove existing rows. -->',
|
|
36
|
+
'',
|
|
37
|
+
'| # | When | Scope | Decision | Choice | Rationale | Revisable? |',
|
|
38
|
+
'|---|------|-------|----------|--------|-----------|------------|',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (let i = 1; i <= count; i++) {
|
|
42
|
+
const id = `D${String(i).padStart(3, '0')}`;
|
|
43
|
+
const milestone = milestones[(i - 1) % milestones.length];
|
|
44
|
+
const sliceNum = ((i - 1) % 5) + 1;
|
|
45
|
+
const when = `${milestone}/S${String(sliceNum).padStart(2, '0')}`;
|
|
46
|
+
const scope = ['architecture', 'testing', 'observability', 'security', 'performance'][(i - 1) % 5];
|
|
47
|
+
const decision = `${scope} decision ${i}: implement ${scope}-level ${['caching', 'validation', 'retry logic', 'circuit breaker', 'rate limiting'][(i - 1) % 5]} for the ${['API layer', 'data pipeline', 'auth subsystem', 'notification service', 'background workers'][(i - 1) % 5]}`;
|
|
48
|
+
const choice = `Use ${['SQLite', 'Redis', 'in-memory cache', 'exponential backoff', 'token bucket'][(i - 1) % 5]} with ${['WAL mode', 'cluster mode', 'LRU eviction', 'jitter', 'sliding window'][(i - 1) % 5]} configuration for optimal ${scope} characteristics`;
|
|
49
|
+
const rationale = `${['Built-in Node.js support eliminates external dependency', 'Sub-millisecond latency meets P99 requirement', 'Memory-efficient with bounded growth prevents OOM', 'Prevents thundering herd during recovery', 'Protects downstream services from burst traffic'][(i - 1) % 5]}. This aligns with our ${scope} principles established in the architecture review and satisfies the non-functional requirements for the ${milestone} milestone.`;
|
|
50
|
+
const revisable = i % 3 === 0 ? 'no' : 'yes';
|
|
51
|
+
|
|
52
|
+
lines.push(`| ${id} | ${when} | ${scope} | ${decision} | ${choice} | ${rationale} | ${revisable} |`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate a realistic REQUIREMENTS.md with `count` requirements spread across slices.
|
|
60
|
+
* Each requirement has multiple detailed fields producing meaningful character content.
|
|
61
|
+
*/
|
|
62
|
+
function generateRequirementsMarkdown(count: number, sliceAssignments: { milestone: string; slice: string }[]): string {
|
|
63
|
+
const lines: string[] = [
|
|
64
|
+
'# Requirements',
|
|
65
|
+
'',
|
|
66
|
+
'## Active',
|
|
67
|
+
'',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (let i = 1; i <= count; i++) {
|
|
71
|
+
const id = `R${String(i).padStart(3, '0')}`;
|
|
72
|
+
const assignment = sliceAssignments[(i - 1) % sliceAssignments.length];
|
|
73
|
+
const reqClass = ['functional', 'non-functional', 'constraint', 'functional', 'non-functional'][(i - 1) % 5];
|
|
74
|
+
const description = `${['Response latency', 'Data consistency', 'Error recovery', 'Access control', 'Audit logging', 'Cache invalidation', 'Schema migration'][(i - 1) % 7]} requirement for ${assignment.milestone}/${assignment.slice}`;
|
|
75
|
+
const why = `Critical for ${['user experience', 'data integrity', 'system reliability', 'security compliance', 'regulatory requirements', 'operational visibility', 'deployment safety'][(i - 1) % 7]}. Without this, the system would ${['degrade under load', 'lose data during failures', 'fail to recover from crashes', 'expose unauthorized data', 'violate compliance mandates', 'have stale data issues', 'break during schema changes'][(i - 1) % 7]}, which is unacceptable for production readiness.`;
|
|
76
|
+
const source = `Architecture review ${milestone_shorthand((i - 1) % 3)}, stakeholder feedback round ${((i - 1) % 4) + 1}`;
|
|
77
|
+
const primaryOwner = assignment.slice;
|
|
78
|
+
const supportingSlices = sliceAssignments
|
|
79
|
+
.filter(a => a.slice !== assignment.slice && a.milestone === assignment.milestone)
|
|
80
|
+
.map(a => a.slice)
|
|
81
|
+
.slice(0, 2)
|
|
82
|
+
.join(', ');
|
|
83
|
+
const validation = `${['Automated test suite covers all edge cases', 'Load test confirms P99 < 200ms under 1000 RPS', 'Chaos test proves recovery within 30s', 'Penetration test shows no unauthorized access paths', 'Audit log review confirms complete event capture', 'Integration test validates cache consistency', 'Migration test verifies zero-downtime upgrade'][(i - 1) % 7]}. Additionally, manual review by ${['architecture team', 'security team', 'SRE team', 'product owner', 'tech lead'][(i - 1) % 5]} confirms adherence to standards.`;
|
|
84
|
+
const notes = `Tracked in ${['JIRA-123', 'JIRA-456', 'JIRA-789', 'JIRA-012', 'JIRA-345'][(i - 1) % 5]}. See also ${['ADR-001', 'ADR-002', 'ADR-003', 'ADR-004', 'ADR-005'][(i - 1) % 5]} for background context on this requirement domain.`;
|
|
85
|
+
|
|
86
|
+
lines.push(`### ${id} — ${description}`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push(`- Class: ${reqClass}`);
|
|
89
|
+
lines.push(`- Status: active`);
|
|
90
|
+
lines.push(`- Why it matters: ${why}`);
|
|
91
|
+
lines.push(`- Source: ${source}`);
|
|
92
|
+
lines.push(`- Primary owning slice: ${primaryOwner}`);
|
|
93
|
+
if (supportingSlices) {
|
|
94
|
+
lines.push(`- Supporting slices: ${supportingSlices}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(`- Validation: ${validation}`);
|
|
97
|
+
lines.push(`- Notes: ${notes}`);
|
|
98
|
+
lines.push('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function milestone_shorthand(index: number): string {
|
|
105
|
+
return ['alpha', 'beta', 'GA'][index] ?? 'alpha';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Fixture Setup ─────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
const MILESTONES = ['M001', 'M002', 'M003'];
|
|
111
|
+
|
|
112
|
+
// Slice assignments: 5 slices spread across M001 and M002
|
|
113
|
+
const SLICE_ASSIGNMENTS = [
|
|
114
|
+
{ milestone: 'M001', slice: 'S01' },
|
|
115
|
+
{ milestone: 'M001', slice: 'S02' },
|
|
116
|
+
{ milestone: 'M001', slice: 'S03' },
|
|
117
|
+
{ milestone: 'M002', slice: 'S04' },
|
|
118
|
+
{ milestone: 'M002', slice: 'S05' },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const DECISIONS_COUNT = 24;
|
|
122
|
+
const REQUIREMENTS_COUNT = 21;
|
|
123
|
+
|
|
124
|
+
const decisionsMarkdown = generateDecisionsMarkdown(DECISIONS_COUNT, MILESTONES);
|
|
125
|
+
const requirementsMarkdown = generateRequirementsMarkdown(REQUIREMENTS_COUNT, SLICE_ASSIGNMENTS);
|
|
126
|
+
|
|
127
|
+
const PROJECT_CONTENT = `# Test Project
|
|
128
|
+
|
|
129
|
+
A test project for validating token savings with DB-scoped content.
|
|
130
|
+
|
|
131
|
+
## Goals
|
|
132
|
+
- Validate ≥30% character savings on planning prompts
|
|
133
|
+
- Ensure quality of scoped content (correct items, no cross-contamination)
|
|
134
|
+
|
|
135
|
+
## Architecture
|
|
136
|
+
- SQLite-backed artifact storage with markdown import
|
|
137
|
+
- Milestone/slice-scoped queries for prompt injection
|
|
138
|
+
- Fallback to full markdown when DB unavailable
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
142
|
+
// Test: Plan-slice savings (≥30%)
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
144
|
+
|
|
145
|
+
console.log('\n=== token-savings: plan-slice prompt ≥30% character savings ===');
|
|
146
|
+
{
|
|
147
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-token-savings-'));
|
|
148
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
149
|
+
writeFileSync(join(base, '.gsd', 'DECISIONS.md'), decisionsMarkdown);
|
|
150
|
+
writeFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), requirementsMarkdown);
|
|
151
|
+
writeFileSync(join(base, '.gsd', 'PROJECT.md'), PROJECT_CONTENT);
|
|
152
|
+
|
|
153
|
+
// Open :memory: DB and import
|
|
154
|
+
openDatabase(':memory:');
|
|
155
|
+
const result = migrateFromMarkdown(base);
|
|
156
|
+
|
|
157
|
+
assertTrue(result.decisions === DECISIONS_COUNT, `imported ${result.decisions} decisions, expected ${DECISIONS_COUNT}`);
|
|
158
|
+
assertTrue(result.requirements === REQUIREMENTS_COUNT, `imported ${result.requirements} requirements, expected ${REQUIREMENTS_COUNT}`);
|
|
159
|
+
|
|
160
|
+
// ── DB-scoped content for plan-slice (M001 decisions + S01 requirements) ──
|
|
161
|
+
const scopedDecisions = queryDecisions({ milestoneId: 'M001' });
|
|
162
|
+
const scopedRequirements = queryRequirements({ sliceId: 'S01' });
|
|
163
|
+
const dbDecisionsContent = formatDecisionsForPrompt(scopedDecisions);
|
|
164
|
+
const dbRequirementsContent = formatRequirementsForPrompt(scopedRequirements);
|
|
165
|
+
|
|
166
|
+
// ── Full-markdown equivalents (what inlineGsdRootFile would return) ──
|
|
167
|
+
const fullDecisionsContent = readFileSync(join(base, '.gsd', 'DECISIONS.md'), 'utf-8');
|
|
168
|
+
const fullRequirementsContent = readFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), 'utf-8');
|
|
169
|
+
|
|
170
|
+
// DB-scoped total vs full-markdown total
|
|
171
|
+
const dbTotal = dbDecisionsContent.length + dbRequirementsContent.length;
|
|
172
|
+
const fullTotal = fullDecisionsContent.length + fullRequirementsContent.length;
|
|
173
|
+
|
|
174
|
+
const savingsPercent = ((fullTotal - dbTotal) / fullTotal) * 100;
|
|
175
|
+
console.log(` Plan-slice savings: ${savingsPercent.toFixed(1)}% (DB: ${dbTotal} chars, full: ${fullTotal} chars)`);
|
|
176
|
+
|
|
177
|
+
assertTrue(dbTotal > 0, 'DB-scoped content is non-empty');
|
|
178
|
+
assertTrue(dbDecisionsContent.length > 0, 'DB-scoped decisions content is non-empty');
|
|
179
|
+
assertTrue(dbRequirementsContent.length > 0, 'DB-scoped requirements content is non-empty');
|
|
180
|
+
assertTrue(savingsPercent >= 30, `plan-slice savings ≥30% (actual: ${savingsPercent.toFixed(1)}%)`);
|
|
181
|
+
assertTrue(dbTotal < fullTotal * 0.70, `DB total (${dbTotal}) < 70% of full total (${fullTotal})`);
|
|
182
|
+
|
|
183
|
+
// ── Verify correct scoping: decisions ──
|
|
184
|
+
// M001 decisions: those with when_context containing 'M001' — indices 1,4,7,10,13,16,19,22
|
|
185
|
+
// (24 decisions round-robin across M001/M002/M003 → 8 for M001)
|
|
186
|
+
assertTrue(scopedDecisions.length === 8, `M001 decisions: expected 8, got ${scopedDecisions.length}`);
|
|
187
|
+
for (const d of scopedDecisions) {
|
|
188
|
+
assertTrue(d.when_context.includes('M001'), `decision ${d.id} should have M001 in when_context, got "${d.when_context}"`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Verify NO decisions from other milestones leak in
|
|
192
|
+
for (const d of scopedDecisions) {
|
|
193
|
+
assertNoMatch(d.when_context, /M002|M003/, `decision ${d.id} should not contain M002 or M003`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Verify correct scoping: requirements ──
|
|
197
|
+
// S01 requirements: those assigned to S01 as primary_owner
|
|
198
|
+
// S01 appears in positions 1,6,11,16,21 (5 assignments cycling, 21 reqs → indices 0,5,10,15,20)
|
|
199
|
+
assertTrue(scopedRequirements.length > 0, 'S01 requirements non-empty');
|
|
200
|
+
for (const r of scopedRequirements) {
|
|
201
|
+
assertTrue(
|
|
202
|
+
r.primary_owner.includes('S01') || r.supporting_slices.includes('S01'),
|
|
203
|
+
`requirement ${r.id} should be owned by or support S01`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Verify specific expected IDs are present
|
|
208
|
+
const scopedDecisionIds = scopedDecisions.map(d => d.id);
|
|
209
|
+
assertTrue(scopedDecisionIds.includes('D001'), 'M001 scoped decisions includes D001');
|
|
210
|
+
assertTrue(scopedDecisionIds.includes('D004'), 'M001 scoped decisions includes D004');
|
|
211
|
+
assertTrue(!scopedDecisionIds.includes('D002'), 'M001 scoped decisions excludes D002 (M002)');
|
|
212
|
+
assertTrue(!scopedDecisionIds.includes('D003'), 'M001 scoped decisions excludes D003 (M003)');
|
|
213
|
+
|
|
214
|
+
const scopedReqIds = scopedRequirements.map(r => r.id);
|
|
215
|
+
assertTrue(scopedReqIds.includes('R001'), 'S01 scoped requirements includes R001');
|
|
216
|
+
|
|
217
|
+
closeDatabase();
|
|
218
|
+
rmSync(base, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
222
|
+
// Test: Research-milestone savings
|
|
223
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
+
|
|
225
|
+
console.log('\n=== token-savings: research-milestone prompt shows meaningful savings ===');
|
|
226
|
+
{
|
|
227
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-token-savings-'));
|
|
228
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
229
|
+
writeFileSync(join(base, '.gsd', 'DECISIONS.md'), decisionsMarkdown);
|
|
230
|
+
writeFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), requirementsMarkdown);
|
|
231
|
+
writeFileSync(join(base, '.gsd', 'PROJECT.md'), PROJECT_CONTENT);
|
|
232
|
+
|
|
233
|
+
openDatabase(':memory:');
|
|
234
|
+
migrateFromMarkdown(base);
|
|
235
|
+
|
|
236
|
+
// ── Research-milestone: M001 decisions + ALL requirements ──
|
|
237
|
+
const scopedDecisions = queryDecisions({ milestoneId: 'M001' });
|
|
238
|
+
const allRequirements = queryRequirements(); // no filter — all requirements
|
|
239
|
+
const dbDecisionsContent = formatDecisionsForPrompt(scopedDecisions);
|
|
240
|
+
const dbRequirementsContent = formatRequirementsForPrompt(allRequirements);
|
|
241
|
+
|
|
242
|
+
const fullDecisionsContent = readFileSync(join(base, '.gsd', 'DECISIONS.md'), 'utf-8');
|
|
243
|
+
const fullRequirementsContent = readFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), 'utf-8');
|
|
244
|
+
|
|
245
|
+
// Decisions should still show savings (8 of 24 scoped to M001)
|
|
246
|
+
const decisionsSavings = ((fullDecisionsContent.length - dbDecisionsContent.length) / fullDecisionsContent.length) * 100;
|
|
247
|
+
console.log(` Decisions savings (M001): ${decisionsSavings.toFixed(1)}% (DB: ${dbDecisionsContent.length}, full: ${fullDecisionsContent.length})`);
|
|
248
|
+
|
|
249
|
+
assertTrue(decisionsSavings > 0, `decisions savings > 0% (actual: ${decisionsSavings.toFixed(1)}%)`);
|
|
250
|
+
assertTrue(scopedDecisions.length === 8, `M001 decisions: 8 of 24 total`);
|
|
251
|
+
assertTrue(allRequirements.length === REQUIREMENTS_COUNT, `all requirements returned: ${allRequirements.length}`);
|
|
252
|
+
|
|
253
|
+
// Requirements: DB-formatted vs raw markdown — formatted output may differ in size
|
|
254
|
+
// but decisions savings alone should make the composite meaningful
|
|
255
|
+
const dbTotal = dbDecisionsContent.length + dbRequirementsContent.length;
|
|
256
|
+
const fullTotal = fullDecisionsContent.length + fullRequirementsContent.length;
|
|
257
|
+
const compositeSavings = ((fullTotal - dbTotal) / fullTotal) * 100;
|
|
258
|
+
console.log(` Research-milestone composite savings: ${compositeSavings.toFixed(1)}% (DB: ${dbTotal}, full: ${fullTotal})`);
|
|
259
|
+
|
|
260
|
+
// With 8/24 decisions = 66% reduction in decisions, even if requirements are equal,
|
|
261
|
+
// the composite should show meaningful savings
|
|
262
|
+
assertTrue(compositeSavings > 10, `research-milestone shows >10% composite savings (actual: ${compositeSavings.toFixed(1)}%)`);
|
|
263
|
+
assertTrue(decisionsSavings >= 30, `decisions-only savings ≥30% for M001 scope (actual: ${decisionsSavings.toFixed(1)}%)`);
|
|
264
|
+
|
|
265
|
+
closeDatabase();
|
|
266
|
+
rmSync(base, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
270
|
+
// Test: Quality — correct content, no cross-contamination
|
|
271
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
272
|
+
|
|
273
|
+
console.log('\n=== token-savings: quality — correct scoping, no cross-contamination ===');
|
|
274
|
+
{
|
|
275
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-token-savings-'));
|
|
276
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
277
|
+
writeFileSync(join(base, '.gsd', 'DECISIONS.md'), decisionsMarkdown);
|
|
278
|
+
writeFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), requirementsMarkdown);
|
|
279
|
+
writeFileSync(join(base, '.gsd', 'PROJECT.md'), PROJECT_CONTENT);
|
|
280
|
+
|
|
281
|
+
openDatabase(':memory:');
|
|
282
|
+
migrateFromMarkdown(base);
|
|
283
|
+
|
|
284
|
+
// ── M002-scoped decisions should not contain M001/M003 items ──
|
|
285
|
+
const m002Decisions = queryDecisions({ milestoneId: 'M002' });
|
|
286
|
+
assertTrue(m002Decisions.length === 8, `M002 decisions: expected 8, got ${m002Decisions.length}`);
|
|
287
|
+
for (const d of m002Decisions) {
|
|
288
|
+
assertTrue(d.when_context.includes('M002'), `M002 decision ${d.id} has M002 in when_context`);
|
|
289
|
+
assertNoMatch(d.when_context, /M001|M003/, `M002 decision ${d.id} should not contain M001/M003`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── S04-scoped requirements should only include S04-related items ──
|
|
293
|
+
const s04Requirements = queryRequirements({ sliceId: 'S04' });
|
|
294
|
+
assertTrue(s04Requirements.length > 0, 'S04 requirements non-empty');
|
|
295
|
+
for (const r of s04Requirements) {
|
|
296
|
+
assertTrue(
|
|
297
|
+
r.primary_owner.includes('S04') || r.supporting_slices.includes('S04'),
|
|
298
|
+
`S04 requirement ${r.id} should be owned by or support S04`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Verify formatted output is well-formed and non-empty ──
|
|
303
|
+
const formattedDecisions = formatDecisionsForPrompt(m002Decisions);
|
|
304
|
+
assertTrue(formattedDecisions.length > 0, 'formatted M002 decisions is non-empty');
|
|
305
|
+
assertMatch(formattedDecisions, /\| D/, 'formatted decisions contains decision rows');
|
|
306
|
+
assertMatch(formattedDecisions, /\| # \|/, 'formatted decisions has table header');
|
|
307
|
+
|
|
308
|
+
const formattedReqs = formatRequirementsForPrompt(s04Requirements);
|
|
309
|
+
assertTrue(formattedReqs.length > 0, 'formatted S04 requirements is non-empty');
|
|
310
|
+
assertMatch(formattedReqs, /### R\d+/, 'formatted requirements has requirement headings');
|
|
311
|
+
|
|
312
|
+
// ── Verify all milestones have decisions and counts add up ──
|
|
313
|
+
const m001Count = queryDecisions({ milestoneId: 'M001' }).length;
|
|
314
|
+
const m002Count = queryDecisions({ milestoneId: 'M002' }).length;
|
|
315
|
+
const m003Count = queryDecisions({ milestoneId: 'M003' }).length;
|
|
316
|
+
const allCount = queryDecisions().length;
|
|
317
|
+
|
|
318
|
+
assertTrue(m001Count === 8, `M001: 8 decisions (got ${m001Count})`);
|
|
319
|
+
assertTrue(m002Count === 8, `M002: 8 decisions (got ${m002Count})`);
|
|
320
|
+
assertTrue(m003Count === 8, `M003: 8 decisions (got ${m003Count})`);
|
|
321
|
+
assertTrue(allCount === DECISIONS_COUNT, `all: ${DECISIONS_COUNT} decisions (got ${allCount})`);
|
|
322
|
+
assertTrue(m001Count + m002Count + m003Count === allCount, 'milestone decision counts sum to total');
|
|
323
|
+
|
|
324
|
+
// ── Verify all slices have requirements ──
|
|
325
|
+
const s01Reqs = queryRequirements({ sliceId: 'S01' });
|
|
326
|
+
const s02Reqs = queryRequirements({ sliceId: 'S02' });
|
|
327
|
+
const s03Reqs = queryRequirements({ sliceId: 'S03' });
|
|
328
|
+
const s04Reqs = queryRequirements({ sliceId: 'S04' });
|
|
329
|
+
const s05Reqs = queryRequirements({ sliceId: 'S05' });
|
|
330
|
+
|
|
331
|
+
assertTrue(s01Reqs.length > 0, 'S01 has requirements');
|
|
332
|
+
assertTrue(s02Reqs.length > 0, 'S02 has requirements');
|
|
333
|
+
assertTrue(s03Reqs.length > 0, 'S03 has requirements');
|
|
334
|
+
assertTrue(s04Reqs.length > 0, 'S04 has requirements');
|
|
335
|
+
assertTrue(s05Reqs.length > 0, 'S05 has requirements');
|
|
336
|
+
|
|
337
|
+
closeDatabase();
|
|
338
|
+
rmSync(base, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
342
|
+
// Test: Fixture data realism — sufficient volume and distribution
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
344
|
+
|
|
345
|
+
console.log('\n=== token-savings: fixture data realism ===');
|
|
346
|
+
{
|
|
347
|
+
// Verify fixture generators produce sufficient volume
|
|
348
|
+
assertTrue(DECISIONS_COUNT >= 20, `decisions count ≥ 20 (actual: ${DECISIONS_COUNT})`);
|
|
349
|
+
assertTrue(REQUIREMENTS_COUNT >= 20, `requirements count ≥ 20 (actual: ${REQUIREMENTS_COUNT})`);
|
|
350
|
+
assertTrue(MILESTONES.length >= 3, `milestones ≥ 3 (actual: ${MILESTONES.length})`);
|
|
351
|
+
assertTrue(SLICE_ASSIGNMENTS.length >= 5, `slice assignments ≥ 5 (actual: ${SLICE_ASSIGNMENTS.length})`);
|
|
352
|
+
|
|
353
|
+
// Verify markdown content is substantial
|
|
354
|
+
assertTrue(decisionsMarkdown.length > 1000, `decisions markdown > 1000 chars (actual: ${decisionsMarkdown.length})`);
|
|
355
|
+
assertTrue(requirementsMarkdown.length > 1000, `requirements markdown > 1000 chars (actual: ${requirementsMarkdown.length})`);
|
|
356
|
+
|
|
357
|
+
// Verify content structure
|
|
358
|
+
assertMatch(decisionsMarkdown, /\| D001 \|/, 'decisions markdown has D001');
|
|
359
|
+
assertMatch(decisionsMarkdown, /\| D024 \|/, 'decisions markdown has D024');
|
|
360
|
+
assertMatch(requirementsMarkdown, /### R001/, 'requirements markdown has R001');
|
|
361
|
+
assertMatch(requirementsMarkdown, /### R021/, 'requirements markdown has R021');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ─── Report ────────────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
report();
|