gsd-pi 2.18.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-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +690 -39
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +654 -36
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- 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 +51 -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 +84 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
- package/dist/resources/extensions/gsd/preferences.ts +198 -150
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- 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/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- 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/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -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/feature-branch-lifecycle-integration.test.ts +434 -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/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -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 +488 -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/routing-history.test.ts +215 -62
- 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/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -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 +290 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
- 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/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -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 +505 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
- 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 +35 -4
- package/dist/resources/extensions/remote-questions/format.ts +166 -14
- package/dist/resources/extensions/remote-questions/manager.ts +14 -4
- 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-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +690 -39
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +654 -36
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- 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 +51 -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 +84 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
- package/src/resources/extensions/gsd/preferences.ts +198 -150
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- 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/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- 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/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -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/feature-branch-lifecycle-integration.test.ts +434 -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/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -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 +488 -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/routing-history.test.ts +215 -62
- 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/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -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 +290 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
- 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/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -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 +505 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
- 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 +35 -4
- package/src/resources/extensions/remote-questions/format.ts +166 -14
- package/src/resources/extensions/remote-questions/manager.ts +14 -4
- 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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { createTestContext } from './test-helpers.ts';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
openDatabase,
|
|
7
|
+
closeDatabase,
|
|
8
|
+
getDecisionById,
|
|
9
|
+
getActiveDecisions,
|
|
10
|
+
getRequirementById,
|
|
11
|
+
getActiveRequirements,
|
|
12
|
+
insertArtifact,
|
|
13
|
+
_getAdapter,
|
|
14
|
+
} from '../gsd-db.ts';
|
|
15
|
+
import {
|
|
16
|
+
parseDecisionsTable,
|
|
17
|
+
parseRequirementsSections,
|
|
18
|
+
migrateFromMarkdown,
|
|
19
|
+
} from '../md-importer.ts';
|
|
20
|
+
|
|
21
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
22
|
+
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
// Fixtures
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
const DECISIONS_MD = `# Decisions Register
|
|
28
|
+
|
|
29
|
+
| # | When | Scope | Decision | Choice | Rationale | Revisable? |
|
|
30
|
+
|---|------|-------|----------|--------|-----------|------------|
|
|
31
|
+
| D001 | M001 | library | SQLite library | better-sqlite3 | Sync API | No |
|
|
32
|
+
| D002 | M001 | arch | DB location | .gsd/gsd.db | Derived state | No |
|
|
33
|
+
| D010 | M001/S01 | library | Provider strategy (amends D001) | node:sqlite fallback | Zero deps | No |
|
|
34
|
+
| D020 | M001/S02 | library | Importer approach (amends D010) | Direct parse | Simple | Yes |
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const REQUIREMENTS_MD = `# Requirements
|
|
38
|
+
|
|
39
|
+
## Active
|
|
40
|
+
|
|
41
|
+
### R001 — SQLite DB layer
|
|
42
|
+
- Class: core-capability
|
|
43
|
+
- Status: active
|
|
44
|
+
- Description: A SQLite database with typed wrappers
|
|
45
|
+
- Why it matters: Foundation for storage
|
|
46
|
+
- Source: user
|
|
47
|
+
- Primary owning slice: M001/S01
|
|
48
|
+
- Supporting slices: none
|
|
49
|
+
- Validation: unmapped
|
|
50
|
+
- Notes: WAL mode enabled
|
|
51
|
+
|
|
52
|
+
### R002 — Graceful fallback
|
|
53
|
+
- Class: failure-visibility
|
|
54
|
+
- Status: active
|
|
55
|
+
- Description: Falls back to markdown if SQLite unavailable
|
|
56
|
+
- Why it matters: Must not break on exotic platforms
|
|
57
|
+
- Source: user
|
|
58
|
+
- Primary owning slice: M001/S01
|
|
59
|
+
- Supporting slices: M001/S03
|
|
60
|
+
- Validation: unmapped
|
|
61
|
+
- Notes: Transparent fallback
|
|
62
|
+
|
|
63
|
+
## Validated
|
|
64
|
+
|
|
65
|
+
### R017 — Sub-5ms query latency
|
|
66
|
+
- Validated by: M001/S01
|
|
67
|
+
- Proof: 50 decisions queried in 0.62ms
|
|
68
|
+
|
|
69
|
+
## Deferred
|
|
70
|
+
|
|
71
|
+
### R030 — Vector search
|
|
72
|
+
- Class: differentiator
|
|
73
|
+
- Status: deferred
|
|
74
|
+
- Description: Rust crate for embeddings
|
|
75
|
+
- Why it matters: Semantic retrieval
|
|
76
|
+
- Source: user
|
|
77
|
+
- Primary owning slice: none
|
|
78
|
+
- Supporting slices: none
|
|
79
|
+
- Validation: unmapped
|
|
80
|
+
- Notes: Deferred to M002
|
|
81
|
+
|
|
82
|
+
## Out of Scope
|
|
83
|
+
|
|
84
|
+
### R040 — Web UI
|
|
85
|
+
- Class: anti-feature
|
|
86
|
+
- Status: out-of-scope
|
|
87
|
+
- Description: No web interface for DB
|
|
88
|
+
- Why it matters: Prevents scope creep
|
|
89
|
+
- Source: user
|
|
90
|
+
- Primary owning slice: none
|
|
91
|
+
- Supporting slices: none
|
|
92
|
+
- Validation: n/a
|
|
93
|
+
- Notes: Excluded in PRD
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
// Helpers
|
|
98
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
99
|
+
|
|
100
|
+
function createFixtureTree(baseDir: string): void {
|
|
101
|
+
const gsd = path.join(baseDir, '.gsd');
|
|
102
|
+
fs.mkdirSync(gsd, { recursive: true });
|
|
103
|
+
fs.writeFileSync(path.join(gsd, 'DECISIONS.md'), DECISIONS_MD);
|
|
104
|
+
fs.writeFileSync(path.join(gsd, 'REQUIREMENTS.md'), REQUIREMENTS_MD);
|
|
105
|
+
fs.writeFileSync(path.join(gsd, 'PROJECT.md'), '# Test Project\nA test project.');
|
|
106
|
+
|
|
107
|
+
// Create milestone hierarchy
|
|
108
|
+
const m001 = path.join(gsd, 'milestones', 'M001');
|
|
109
|
+
fs.mkdirSync(m001, { recursive: true });
|
|
110
|
+
fs.writeFileSync(path.join(m001, 'M001-ROADMAP.md'), '# M001 Roadmap\nTest roadmap content.');
|
|
111
|
+
fs.writeFileSync(path.join(m001, 'M001-CONTEXT.md'), '# M001 Context\nTest context.');
|
|
112
|
+
|
|
113
|
+
// Create slice
|
|
114
|
+
const s01 = path.join(m001, 'slices', 'S01');
|
|
115
|
+
fs.mkdirSync(s01, { recursive: true });
|
|
116
|
+
fs.writeFileSync(path.join(s01, 'S01-PLAN.md'), '# S01 Plan\nTest plan.');
|
|
117
|
+
fs.writeFileSync(path.join(s01, 'S01-SUMMARY.md'), '# S01 Summary\nTest summary.');
|
|
118
|
+
|
|
119
|
+
// Create tasks
|
|
120
|
+
const tasks = path.join(s01, 'tasks');
|
|
121
|
+
fs.mkdirSync(tasks, { recursive: true });
|
|
122
|
+
fs.writeFileSync(path.join(tasks, 'T01-PLAN.md'), '# T01 Plan\nTask plan.');
|
|
123
|
+
fs.writeFileSync(path.join(tasks, 'T01-SUMMARY.md'), '# T01 Summary\nTask summary.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cleanupDir(dir: string): void {
|
|
127
|
+
try {
|
|
128
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
} catch {
|
|
130
|
+
// best effort
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
135
|
+
// md-importer: parseDecisionsTable
|
|
136
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
137
|
+
|
|
138
|
+
console.log('\n=== md-importer: parseDecisionsTable ===');
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
const decisions = parseDecisionsTable(DECISIONS_MD);
|
|
142
|
+
assertEq(decisions.length, 4, 'should parse 4 decisions');
|
|
143
|
+
assertEq(decisions[0].id, 'D001', 'first decision should be D001');
|
|
144
|
+
assertEq(decisions[0].decision, 'SQLite library', 'D001 decision text');
|
|
145
|
+
assertEq(decisions[0].choice, 'better-sqlite3', 'D001 choice');
|
|
146
|
+
assertEq(decisions[0].scope, 'library', 'D001 scope');
|
|
147
|
+
assertEq(decisions[0].revisable, 'No', 'D001 revisable');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log('=== md-importer: supersession detection ===');
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
const decisions = parseDecisionsTable(DECISIONS_MD);
|
|
154
|
+
|
|
155
|
+
// D010 amends D001 → D001.superseded_by = D010
|
|
156
|
+
const d001 = decisions.find(d => d.id === 'D001');
|
|
157
|
+
assertEq(d001?.superseded_by, 'D010', 'D001 should be superseded by D010');
|
|
158
|
+
|
|
159
|
+
// D020 amends D010 → D010.superseded_by = D020
|
|
160
|
+
const d010 = decisions.find(d => d.id === 'D010');
|
|
161
|
+
assertEq(d010?.superseded_by, 'D020', 'D010 should be superseded by D020');
|
|
162
|
+
|
|
163
|
+
// D002 is not amended
|
|
164
|
+
const d002 = decisions.find(d => d.id === 'D002');
|
|
165
|
+
assertEq(d002?.superseded_by, null, 'D002 should not be superseded');
|
|
166
|
+
|
|
167
|
+
// D020 is the latest in chain, not superseded
|
|
168
|
+
const d020 = decisions.find(d => d.id === 'D020');
|
|
169
|
+
assertEq(d020?.superseded_by, null, 'D020 should not be superseded');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log('=== md-importer: malformed/empty rows skipped ===');
|
|
173
|
+
|
|
174
|
+
{
|
|
175
|
+
const malformedInput = `# Decisions
|
|
176
|
+
|
|
177
|
+
| # | When | Scope | Decision | Choice | Rationale | Revisable? |
|
|
178
|
+
|---|------|-------|----------|--------|-----------|------------|
|
|
179
|
+
| D001 | M001 | lib | Pick lib | sqlite | Fast | No |
|
|
180
|
+
| not-a-decision | bad | x | y | z | w | q |
|
|
181
|
+
| | | | | | | |
|
|
182
|
+
| D003 | M001 | arch | Config | JSON | Simple | Yes |
|
|
183
|
+
`;
|
|
184
|
+
const decisions = parseDecisionsTable(malformedInput);
|
|
185
|
+
assertEq(decisions.length, 2, 'should skip rows without D-prefix IDs');
|
|
186
|
+
assertEq(decisions[0].id, 'D001', 'first valid row');
|
|
187
|
+
assertEq(decisions[1].id, 'D003', 'second valid row (skipping malformed)');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
191
|
+
// md-importer: parseRequirementsSections
|
|
192
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
193
|
+
|
|
194
|
+
console.log('=== md-importer: parseRequirementsSections ===');
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
const reqs = parseRequirementsSections(REQUIREMENTS_MD);
|
|
198
|
+
assertEq(reqs.length, 5, 'should parse 5 unique requirements');
|
|
199
|
+
|
|
200
|
+
const r001 = reqs.find(r => r.id === 'R001');
|
|
201
|
+
assertTrue(!!r001, 'R001 should exist');
|
|
202
|
+
assertEq(r001?.class, 'core-capability', 'R001 class');
|
|
203
|
+
assertEq(r001?.status, 'active', 'R001 status');
|
|
204
|
+
assertEq(r001?.description, 'A SQLite database with typed wrappers', 'R001 description');
|
|
205
|
+
assertEq(r001?.why, 'Foundation for storage', 'R001 why');
|
|
206
|
+
assertEq(r001?.source, 'user', 'R001 source');
|
|
207
|
+
assertEq(r001?.primary_owner, 'M001/S01', 'R001 primary_owner');
|
|
208
|
+
assertEq(r001?.supporting_slices, 'none', 'R001 supporting_slices');
|
|
209
|
+
assertEq(r001?.validation, 'unmapped', 'R001 validation');
|
|
210
|
+
assertEq(r001?.notes, 'WAL mode enabled', 'R001 notes');
|
|
211
|
+
assertTrue(r001?.full_content?.includes('### R001') ?? false, 'R001 full_content should have heading');
|
|
212
|
+
|
|
213
|
+
// Validated section — R017 (abbreviated format with "Validated by" / "Proof" bullets)
|
|
214
|
+
const r017 = reqs.find(r => r.id === 'R017');
|
|
215
|
+
assertTrue(!!r017, 'R017 should exist');
|
|
216
|
+
assertEq(r017?.status, 'validated', 'R017 status from validated section');
|
|
217
|
+
assertEq(r017?.validation, 'M001/S01', 'R017 validation (from "Validated by" bullet)');
|
|
218
|
+
assertEq(r017?.notes, '50 decisions queried in 0.62ms', 'R017 notes (from "Proof" bullet)');
|
|
219
|
+
|
|
220
|
+
// Deferred requirement
|
|
221
|
+
const r030 = reqs.find(r => r.id === 'R030');
|
|
222
|
+
assertEq(r030?.status, 'deferred', 'R030 status should be deferred');
|
|
223
|
+
assertEq(r030?.class, 'differentiator', 'R030 class');
|
|
224
|
+
assertEq(r030?.description, 'Rust crate for embeddings', 'R030 description');
|
|
225
|
+
|
|
226
|
+
// Out of scope
|
|
227
|
+
const r040 = reqs.find(r => r.id === 'R040');
|
|
228
|
+
assertEq(r040?.status, 'out-of-scope', 'R040 status should be out-of-scope');
|
|
229
|
+
assertEq(r040?.class, 'anti-feature', 'R040 class');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
233
|
+
// md-importer: migrateFromMarkdown orchestrator
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
235
|
+
|
|
236
|
+
console.log('=== md-importer: migrateFromMarkdown orchestrator ===');
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-import-test-'));
|
|
240
|
+
createFixtureTree(tmpDir);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
openDatabase(':memory:');
|
|
244
|
+
const result = migrateFromMarkdown(tmpDir);
|
|
245
|
+
|
|
246
|
+
assertEq(result.decisions, 4, 'should import 4 decisions');
|
|
247
|
+
assertEq(result.requirements, 5, 'should import 5 requirements');
|
|
248
|
+
assertTrue(result.artifacts > 0, 'should import some artifacts');
|
|
249
|
+
|
|
250
|
+
// Verify decisions queryable
|
|
251
|
+
const d001 = getDecisionById('D001');
|
|
252
|
+
assertTrue(!!d001, 'D001 should be queryable');
|
|
253
|
+
assertEq(d001?.superseded_by, 'D010', 'D001 superseded_by should be D010');
|
|
254
|
+
|
|
255
|
+
// Verify requirements queryable
|
|
256
|
+
const r001 = getRequirementById('R001');
|
|
257
|
+
assertTrue(!!r001, 'R001 should be queryable');
|
|
258
|
+
assertEq(r001?.status, 'active', 'R001 status from DB');
|
|
259
|
+
|
|
260
|
+
// Verify active views
|
|
261
|
+
const activeD = getActiveDecisions();
|
|
262
|
+
assertEq(activeD.length, 2, 'should have 2 active decisions (D002, D020)');
|
|
263
|
+
|
|
264
|
+
// Verify artifacts table
|
|
265
|
+
const adapter = _getAdapter();
|
|
266
|
+
const artifacts = adapter?.prepare('SELECT count(*) as c FROM artifacts').get();
|
|
267
|
+
assertTrue((artifacts?.c as number) > 0, 'artifacts table should have rows');
|
|
268
|
+
|
|
269
|
+
// Verify hierarchy correctness
|
|
270
|
+
const roadmap = adapter?.prepare('SELECT * FROM artifacts WHERE artifact_type = :type').get({ ':type': 'ROADMAP' });
|
|
271
|
+
assertTrue(!!roadmap, 'ROADMAP artifact should exist');
|
|
272
|
+
assertEq(roadmap?.milestone_id, 'M001', 'ROADMAP should be in M001');
|
|
273
|
+
|
|
274
|
+
const taskPlan = adapter?.prepare('SELECT * FROM artifacts WHERE task_id = :taskId AND artifact_type = :type').get({
|
|
275
|
+
':taskId': 'T01',
|
|
276
|
+
':type': 'PLAN',
|
|
277
|
+
});
|
|
278
|
+
assertTrue(!!taskPlan, 'T01-PLAN artifact should exist');
|
|
279
|
+
|
|
280
|
+
closeDatabase();
|
|
281
|
+
} finally {
|
|
282
|
+
cleanupDir(tmpDir);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
287
|
+
// md-importer: idempotent re-import
|
|
288
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
289
|
+
|
|
290
|
+
console.log('=== md-importer: idempotent re-import ===');
|
|
291
|
+
|
|
292
|
+
{
|
|
293
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-idemp-test-'));
|
|
294
|
+
createFixtureTree(tmpDir);
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
openDatabase(':memory:');
|
|
298
|
+
const r1 = migrateFromMarkdown(tmpDir);
|
|
299
|
+
const r2 = migrateFromMarkdown(tmpDir);
|
|
300
|
+
|
|
301
|
+
assertEq(r1.decisions, r2.decisions, 'double import should produce same decision count');
|
|
302
|
+
assertEq(r1.requirements, r2.requirements, 'double import should produce same requirement count');
|
|
303
|
+
assertEq(r1.artifacts, r2.artifacts, 'double import should produce same artifact count');
|
|
304
|
+
|
|
305
|
+
// Verify no duplicates
|
|
306
|
+
const adapter = _getAdapter();
|
|
307
|
+
const dc = adapter?.prepare('SELECT count(*) as c FROM decisions').get()?.c as number;
|
|
308
|
+
const rc = adapter?.prepare('SELECT count(*) as c FROM requirements').get()?.c as number;
|
|
309
|
+
const ac = adapter?.prepare('SELECT count(*) as c FROM artifacts').get()?.c as number;
|
|
310
|
+
|
|
311
|
+
assertEq(dc, r1.decisions, 'DB decision count matches import count');
|
|
312
|
+
assertEq(rc, r1.requirements, 'DB requirement count matches import count');
|
|
313
|
+
assertEq(ac, r1.artifacts, 'DB artifact count matches import count');
|
|
314
|
+
|
|
315
|
+
closeDatabase();
|
|
316
|
+
} finally {
|
|
317
|
+
cleanupDir(tmpDir);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
322
|
+
// md-importer: missing file graceful handling
|
|
323
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
324
|
+
|
|
325
|
+
console.log('=== md-importer: missing file handling ===');
|
|
326
|
+
|
|
327
|
+
{
|
|
328
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-empty-test-'));
|
|
329
|
+
// Create empty .gsd/ with no files
|
|
330
|
+
fs.mkdirSync(path.join(tmpDir, '.gsd'), { recursive: true });
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
openDatabase(':memory:');
|
|
334
|
+
const result = migrateFromMarkdown(tmpDir);
|
|
335
|
+
|
|
336
|
+
assertEq(result.decisions, 0, 'missing DECISIONS.md → 0 decisions');
|
|
337
|
+
assertEq(result.requirements, 0, 'missing REQUIREMENTS.md → 0 requirements');
|
|
338
|
+
assertEq(result.artifacts, 0, 'empty tree → 0 artifacts');
|
|
339
|
+
|
|
340
|
+
closeDatabase();
|
|
341
|
+
} finally {
|
|
342
|
+
cleanupDir(tmpDir);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
347
|
+
// md-importer: schema v1→v2 migration on existing DBs
|
|
348
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
349
|
+
|
|
350
|
+
console.log('=== md-importer: schema v1→v2 migration ===');
|
|
351
|
+
|
|
352
|
+
{
|
|
353
|
+
// This test verifies that opening a v1 DB auto-migrates to v2
|
|
354
|
+
// (The actual migration is tested via the gsd-db.test.ts schema version assertion = 2)
|
|
355
|
+
openDatabase(':memory:');
|
|
356
|
+
const adapter = _getAdapter();
|
|
357
|
+
const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
358
|
+
assertEq(version?.v, 2, 'new DB should be at schema version 2');
|
|
359
|
+
|
|
360
|
+
// Artifacts table should exist
|
|
361
|
+
const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
|
|
362
|
+
assertEq(tableCheck?.c, 1, 'artifacts table should exist');
|
|
363
|
+
|
|
364
|
+
closeDatabase();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
368
|
+
// md-importer: round-trip fidelity
|
|
369
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
370
|
+
|
|
371
|
+
console.log('=== md-importer: round-trip fidelity ===');
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-roundtrip-test-'));
|
|
375
|
+
createFixtureTree(tmpDir);
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
openDatabase(':memory:');
|
|
379
|
+
migrateFromMarkdown(tmpDir);
|
|
380
|
+
|
|
381
|
+
// Round-trip: verify imported field values match source
|
|
382
|
+
const d002 = getDecisionById('D002');
|
|
383
|
+
assertEq(d002?.when_context, 'M001', 'D002 when_context round-trip');
|
|
384
|
+
assertEq(d002?.scope, 'arch', 'D002 scope round-trip');
|
|
385
|
+
assertEq(d002?.decision, 'DB location', 'D002 decision round-trip');
|
|
386
|
+
assertEq(d002?.choice, '.gsd/gsd.db', 'D002 choice round-trip');
|
|
387
|
+
assertEq(d002?.rationale, 'Derived state', 'D002 rationale round-trip');
|
|
388
|
+
|
|
389
|
+
const r002 = getRequirementById('R002');
|
|
390
|
+
assertEq(r002?.class, 'failure-visibility', 'R002 class round-trip');
|
|
391
|
+
assertEq(r002?.description, 'Falls back to markdown if SQLite unavailable', 'R002 description round-trip');
|
|
392
|
+
assertEq(r002?.why, 'Must not break on exotic platforms', 'R002 why round-trip');
|
|
393
|
+
assertEq(r002?.primary_owner, 'M001/S01', 'R002 primary_owner round-trip');
|
|
394
|
+
assertEq(r002?.supporting_slices, 'M001/S03', 'R002 supporting_slices round-trip');
|
|
395
|
+
assertEq(r002?.notes, 'Transparent fallback', 'R002 notes round-trip');
|
|
396
|
+
assertEq(r002?.validation, 'unmapped', 'R002 validation round-trip');
|
|
397
|
+
|
|
398
|
+
// Verify artifact content is stored
|
|
399
|
+
const adapter = _getAdapter();
|
|
400
|
+
const project = adapter?.prepare("SELECT * FROM artifacts WHERE path = :path").get({ ':path': 'PROJECT.md' });
|
|
401
|
+
assertTrue((project?.full_content as string)?.includes('Test Project'), 'PROJECT.md content round-trip');
|
|
402
|
+
|
|
403
|
+
closeDatabase();
|
|
404
|
+
} finally {
|
|
405
|
+
cleanupDir(tmpDir);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
410
|
+
|
|
411
|
+
report();
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import {
|
|
7
7
|
type UnitMetrics,
|
|
8
8
|
type TokenCounts,
|
|
9
|
+
type BudgetInfo,
|
|
9
10
|
classifyUnitPhase,
|
|
10
11
|
aggregateByPhase,
|
|
11
12
|
aggregateBySlice,
|
|
@@ -183,6 +184,202 @@ assertEq(formatTokenCount(1500), "1.5k", "1.5k");
|
|
|
183
184
|
assertEq(formatTokenCount(150000), "150.0k", "150k");
|
|
184
185
|
assertEq(formatTokenCount(1500000), "1.50M", "1.5M");
|
|
185
186
|
|
|
187
|
+
// ─── Backward compat: UnitMetrics without budget fields ───────────────────────
|
|
188
|
+
|
|
189
|
+
console.log("\n=== Backward compat: UnitMetrics without budget fields ===");
|
|
190
|
+
|
|
191
|
+
{
|
|
192
|
+
// Simulate old metrics.json data — no budget fields present
|
|
193
|
+
const oldUnit: UnitMetrics = {
|
|
194
|
+
type: "execute-task",
|
|
195
|
+
id: "M001/S01/T01",
|
|
196
|
+
model: "claude-sonnet-4-20250514",
|
|
197
|
+
startedAt: 1000,
|
|
198
|
+
finishedAt: 2000,
|
|
199
|
+
tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
|
|
200
|
+
cost: 0.05,
|
|
201
|
+
toolCalls: 3,
|
|
202
|
+
assistantMessages: 2,
|
|
203
|
+
userMessages: 1,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// All aggregation functions must work with old data
|
|
207
|
+
const phases = aggregateByPhase([oldUnit]);
|
|
208
|
+
assertEq(phases.length, 1, "backward compat: aggregateByPhase works");
|
|
209
|
+
assertEq(phases[0].phase, "execution", "backward compat: correct phase");
|
|
210
|
+
|
|
211
|
+
const slices = aggregateBySlice([oldUnit]);
|
|
212
|
+
assertEq(slices.length, 1, "backward compat: aggregateBySlice works");
|
|
213
|
+
assertEq(slices[0].sliceId, "M001/S01", "backward compat: correct sliceId");
|
|
214
|
+
|
|
215
|
+
const models = aggregateByModel([oldUnit]);
|
|
216
|
+
assertEq(models.length, 1, "backward compat: aggregateByModel works");
|
|
217
|
+
|
|
218
|
+
const totals = getProjectTotals([oldUnit]);
|
|
219
|
+
assertEq(totals.units, 1, "backward compat: getProjectTotals works");
|
|
220
|
+
assertClose(totals.cost, 0.05, 0.001, "backward compat: cost preserved");
|
|
221
|
+
|
|
222
|
+
// Budget fields should be undefined
|
|
223
|
+
assertEq(oldUnit.contextWindowTokens, undefined, "backward compat: no contextWindowTokens");
|
|
224
|
+
assertEq(oldUnit.truncationSections, undefined, "backward compat: no truncationSections");
|
|
225
|
+
assertEq(oldUnit.continueHereFired, undefined, "backward compat: no continueHereFired");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── UnitMetrics with budget fields populated ─────────────────────────────────
|
|
229
|
+
|
|
230
|
+
console.log("\n=== UnitMetrics with budget fields ===");
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
const unitWithBudget: UnitMetrics = {
|
|
234
|
+
type: "execute-task",
|
|
235
|
+
id: "M002/S01/T03",
|
|
236
|
+
model: "claude-sonnet-4-20250514",
|
|
237
|
+
startedAt: 5000,
|
|
238
|
+
finishedAt: 10000,
|
|
239
|
+
tokens: { input: 3000, output: 1500, cacheRead: 600, cacheWrite: 300, total: 5400 },
|
|
240
|
+
cost: 0.12,
|
|
241
|
+
toolCalls: 8,
|
|
242
|
+
assistantMessages: 4,
|
|
243
|
+
userMessages: 3,
|
|
244
|
+
contextWindowTokens: 200000,
|
|
245
|
+
truncationSections: 3,
|
|
246
|
+
continueHereFired: true,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Budget fields are present
|
|
250
|
+
assertEq(unitWithBudget.contextWindowTokens, 200000, "budget: contextWindowTokens present");
|
|
251
|
+
assertEq(unitWithBudget.truncationSections, 3, "budget: truncationSections present");
|
|
252
|
+
assertEq(unitWithBudget.continueHereFired, true, "budget: continueHereFired present");
|
|
253
|
+
|
|
254
|
+
// Aggregation still works correctly with budget fields present
|
|
255
|
+
const phases = aggregateByPhase([unitWithBudget]);
|
|
256
|
+
assertEq(phases.length, 1, "budget: aggregateByPhase works");
|
|
257
|
+
assertClose(phases[0].cost, 0.12, 0.001, "budget: cost aggregated correctly");
|
|
258
|
+
|
|
259
|
+
const slices = aggregateBySlice([unitWithBudget]);
|
|
260
|
+
assertEq(slices.length, 1, "budget: aggregateBySlice works");
|
|
261
|
+
assertEq(slices[0].sliceId, "M002/S01", "budget: sliceId correct");
|
|
262
|
+
|
|
263
|
+
const models = aggregateByModel([unitWithBudget]);
|
|
264
|
+
assertEq(models.length, 1, "budget: aggregateByModel works");
|
|
265
|
+
|
|
266
|
+
const totals = getProjectTotals([unitWithBudget]);
|
|
267
|
+
assertEq(totals.units, 1, "budget: getProjectTotals works");
|
|
268
|
+
assertEq(totals.toolCalls, 8, "budget: toolCalls aggregated");
|
|
269
|
+
|
|
270
|
+
// Mix old and new units together
|
|
271
|
+
const oldUnit = makeUnit(); // no budget fields
|
|
272
|
+
const mixed = [oldUnit, unitWithBudget];
|
|
273
|
+
const mixedTotals = getProjectTotals(mixed);
|
|
274
|
+
assertEq(mixedTotals.units, 2, "mixed: 2 units total");
|
|
275
|
+
assertClose(mixedTotals.cost, 0.17, 0.001, "mixed: costs summed correctly");
|
|
276
|
+
|
|
277
|
+
const mixedPhases = aggregateByPhase(mixed);
|
|
278
|
+
assertEq(mixedPhases.length, 1, "mixed: both are execution phase");
|
|
279
|
+
assertEq(mixedPhases[0].units, 2, "mixed: both counted");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── aggregateByModel: contextWindowTokens pick logic ─────────────────────────
|
|
283
|
+
|
|
284
|
+
console.log("\n=== aggregateByModel: contextWindowTokens pick logic ===");
|
|
285
|
+
|
|
286
|
+
{
|
|
287
|
+
// Single unit with contextWindowTokens — aggregate picks it
|
|
288
|
+
const units = [
|
|
289
|
+
makeUnit({ model: "claude-sonnet-4-20250514", contextWindowTokens: 200000, cost: 0.05 }),
|
|
290
|
+
];
|
|
291
|
+
const models = aggregateByModel(units);
|
|
292
|
+
assertEq(models.length, 1, "ctxWindow: one model");
|
|
293
|
+
assertEq(models[0].contextWindowTokens, 200000, "ctxWindow: picks value from unit");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
{
|
|
297
|
+
// Two units same model with different context windows — first defined value wins
|
|
298
|
+
const units = [
|
|
299
|
+
makeUnit({ model: "claude-sonnet-4-20250514", contextWindowTokens: 200000, cost: 0.05 }),
|
|
300
|
+
makeUnit({ model: "claude-sonnet-4-20250514", contextWindowTokens: 150000, cost: 0.04 }),
|
|
301
|
+
];
|
|
302
|
+
const models = aggregateByModel(units);
|
|
303
|
+
assertEq(models.length, 1, "ctxWindow first-wins: one model");
|
|
304
|
+
assertEq(models[0].contextWindowTokens, 200000, "ctxWindow first-wins: first value kept");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
{
|
|
308
|
+
// First unit undefined, second has value — second is picked
|
|
309
|
+
const units = [
|
|
310
|
+
makeUnit({ model: "claude-sonnet-4-20250514", cost: 0.05 }),
|
|
311
|
+
makeUnit({ model: "claude-sonnet-4-20250514", contextWindowTokens: 200000, cost: 0.04 }),
|
|
312
|
+
];
|
|
313
|
+
const models = aggregateByModel(units);
|
|
314
|
+
assertEq(models[0].contextWindowTokens, 200000, "ctxWindow: picks first defined, not first unit");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
{
|
|
318
|
+
// Old units without contextWindowTokens — aggregate has undefined
|
|
319
|
+
const units = [
|
|
320
|
+
makeUnit({ model: "claude-sonnet-4-20250514", cost: 0.05 }),
|
|
321
|
+
makeUnit({ model: "claude-sonnet-4-20250514", cost: 0.04 }),
|
|
322
|
+
];
|
|
323
|
+
const models = aggregateByModel(units);
|
|
324
|
+
assertEq(models[0].contextWindowTokens, undefined, "ctxWindow: undefined when no unit has it");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
{
|
|
328
|
+
// Multiple models — each gets its own context window
|
|
329
|
+
const units = [
|
|
330
|
+
makeUnit({ model: "claude-sonnet-4-20250514", contextWindowTokens: 200000, cost: 0.05 }),
|
|
331
|
+
makeUnit({ model: "claude-opus-4-20250514", contextWindowTokens: 200000, cost: 0.30 }),
|
|
332
|
+
];
|
|
333
|
+
const models = aggregateByModel(units);
|
|
334
|
+
assertEq(models.length, 2, "ctxWindow multi-model: 2 models");
|
|
335
|
+
const opus = models.find(m => m.model === "claude-opus-4-20250514");
|
|
336
|
+
const sonnet = models.find(m => m.model === "claude-sonnet-4-20250514");
|
|
337
|
+
assertEq(opus!.contextWindowTokens, 200000, "ctxWindow multi-model: opus has value");
|
|
338
|
+
assertEq(sonnet!.contextWindowTokens, 200000, "ctxWindow multi-model: sonnet has value");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── getProjectTotals: budget field aggregation ───────────────────────────────
|
|
342
|
+
|
|
343
|
+
console.log("\n=== getProjectTotals: budget field aggregation ===");
|
|
344
|
+
|
|
345
|
+
{
|
|
346
|
+
// Units with truncationSections and continueHereFired — verify sums/counts
|
|
347
|
+
const units = [
|
|
348
|
+
makeUnit({ truncationSections: 3, continueHereFired: true }),
|
|
349
|
+
makeUnit({ truncationSections: 2, continueHereFired: false }),
|
|
350
|
+
makeUnit({ truncationSections: 1, continueHereFired: true }),
|
|
351
|
+
];
|
|
352
|
+
const totals = getProjectTotals(units);
|
|
353
|
+
assertEq(totals.totalTruncationSections, 6, "budget totals: truncation sections summed");
|
|
354
|
+
assertEq(totals.continueHereFiredCount, 2, "budget totals: continueHereFired counted");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
{
|
|
358
|
+
// Old units without budget fields — verify 0 defaults
|
|
359
|
+
const units = [makeUnit(), makeUnit()];
|
|
360
|
+
const totals = getProjectTotals(units);
|
|
361
|
+
assertEq(totals.totalTruncationSections, 0, "budget totals backward compat: truncation = 0");
|
|
362
|
+
assertEq(totals.continueHereFiredCount, 0, "budget totals backward compat: continueHere = 0");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
{
|
|
366
|
+
// Mixed old and new units
|
|
367
|
+
const units = [
|
|
368
|
+
makeUnit(), // old, no budget fields
|
|
369
|
+
makeUnit({ truncationSections: 5, continueHereFired: true }),
|
|
370
|
+
];
|
|
371
|
+
const totals = getProjectTotals(units);
|
|
372
|
+
assertEq(totals.totalTruncationSections, 5, "budget totals mixed: only new unit contributes");
|
|
373
|
+
assertEq(totals.continueHereFiredCount, 1, "budget totals mixed: only one fired");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
{
|
|
377
|
+
// Empty input — safe defaults
|
|
378
|
+
const totals = getProjectTotals([]);
|
|
379
|
+
assertEq(totals.totalTruncationSections, 0, "budget totals empty: truncation = 0");
|
|
380
|
+
assertEq(totals.continueHereFiredCount, 0, "budget totals empty: continueHere = 0");
|
|
381
|
+
}
|
|
382
|
+
|
|
186
383
|
// ─── Summary ──────────────────────────────────────────────────────────────────
|
|
187
384
|
|
|
188
385
|
report();
|