project-knowledge 0.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INDEX.md +53 -0
  3. package/README.md +79 -0
  4. package/_site/README.md +63 -0
  5. package/_site/_test/ai-profile-test.js +199 -0
  6. package/_site/_test/baseline-schema-test.js +132 -0
  7. package/_site/_test/commit-analysis-test.js +184 -0
  8. package/_site/_test/context-pack-test.js +199 -0
  9. package/_site/_test/draft-apply-test.js +363 -0
  10. package/_site/_test/git-validation-test.js +171 -0
  11. package/_site/_test/hook-trigger-test.js +257 -0
  12. package/_site/_test/initial-analysis-test.js +228 -0
  13. package/_site/_test/job-orchestrator-test.js +297 -0
  14. package/_site/_test/kb-v2-templates-test.js +189 -0
  15. package/_site/_test/pr-consumer-contract-test.js +236 -0
  16. package/_site/_test/run-all-tests.js +135 -0
  17. package/_site/_test/scanner-test.js +206 -0
  18. package/_site/_test/ui-smoke-test.js +237 -0
  19. package/_site/_test/ui-test.js +237 -0
  20. package/_site/index.html +1166 -0
  21. package/_site/lib/ai-adapter.js +287 -0
  22. package/_site/lib/analysis-orchestrator.js +433 -0
  23. package/_site/lib/context-pack-builder.js +290 -0
  24. package/_site/lib/draft-apply.js +219 -0
  25. package/_site/lib/git-runner.js +26 -0
  26. package/_site/lib/hook-manager.js +148 -0
  27. package/_site/lib/job-orchestrator.js +231 -0
  28. package/_site/lib/kb-validator.js +224 -0
  29. package/_site/lib/llm-client.js +126 -0
  30. package/_site/lib/scanner.js +94 -0
  31. package/_site/scripts/hook-trigger.js +133 -0
  32. package/_site/scripts/safe-runner.js +151 -0
  33. package/_site/server.js +1058 -0
  34. package/_site/start.bat +26 -0
  35. package/_site/stop.bat +11 -0
  36. package/ai-profiles.json +18 -0
  37. package/docs/ai-knowledge-base-system-design.md +395 -0
  38. package/docs/pr-consumer-contract.md +198 -0
  39. package/docs/project-goal.md +72 -0
  40. package/docs/project-registry-schema.md +46 -0
  41. package/docs/testing-strategy.md +169 -0
  42. package/iterations.json +23 -0
  43. package/package.json +47 -0
  44. package/scripts/gen-commit-doc.ps1 +178 -0
  45. package/scripts/gen-commit-doc.sh +197 -0
  46. package/scripts/list-features.ps1 +41 -0
  47. package/scripts/register-scheduled-task.bat +5 -0
  48. package/templates/change.md +59 -0
  49. package/templates/commit-feature.md +56 -0
  50. package/templates/feature.md +44 -0
  51. package/templates/framework.md +80 -0
  52. package/templates/index-header.md +3 -0
  53. package/templates/kb-manifest.json +38 -0
  54. package/templates/module.md +58 -0
  55. package/templates/project-analysis.md +48 -0
  56. package/templates/project-goal.md +55 -0
  57. package/templates/project-readme.md +60 -0
  58. package/templates/quality-review-rules.md +37 -0
  59. package/templates/update-entry.md +7 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # 知识库变更日志
2
+
3
+ 记录 `D:\SanQian.Xu\project-knowledge-base\` 自身的结构性变更,不记录各项目内文档的内容变更(项目内变更见各项目的 `git-summary/changelog/` 或 git history)。
4
+
5
+ ## 格式
6
+
7
+ - **Added**:新增文件 / 目录 / 脚本
8
+ - **Changed**:现有文件 / 目录结构调整
9
+ - **Removed**:删除文件 / 目录
10
+ - **Fixed**:修复错误
11
+
12
+ ## 2026-06-08
13
+
14
+ ### Changed
15
+ - 项目从 `kb` 重命名为 `project-knowledge-base`,更新所有内部路径引用
16
+
17
+ ## 2026-06-07
18
+
19
+ ### Added
20
+ - 建立 `project-knowledge-base/` 顶层结构(`README.md`、`INDEX.md`、`CHANGELOG.md`、`projects.json`、`iterations.json`、`.gitignore`)
21
+ - 建立 5 个项目子目录的标准化模板(`README.md`、`framework.md`、`architecture/`、`modules/`、`commits/`、`operations/`、`references/`)
22
+ - 创建 4 个文档模板(`templates/project-readme.md`、`framework.md`、`module.md`、`commit-feature.md`)
23
+ - 迁移 3 个项目的 `knowledge/` 内容到 `project-knowledge-base/projects/<slug>/`:
24
+ - Claude-DevSprite(5 篇 project-overview + 50+ 篇 changelog)
25
+ - Token Consumption Leaderboard(5 篇 features + 6 篇 changelog)
26
+ - 迁移 quant-platform 的 `docs/` 内容(`docs/progress/`、`docs/backend/`)
27
+ - 提取 Claude-Code-UI(参考项目)的 README.zh-CN.md + CHANGELOG.md
28
+ - 编写 `scripts/gen-commit-doc.ps1` 与 `gen-commit-doc.sh`,实现 git log → 功能文档
29
+ - 首次对 5 个项目执行 `gen-commit-doc.ps1`,生成 `commits/*.md`
30
+
31
+ ### Removed
32
+ - 删除 `D:\SanQian.Xu\Claude-DevSprite\knowledge\`(已备份到 `_backup/2026-06-07/`)
33
+ - 删除 `D:\SanQian.Xu\Token Consumption Leaderboard\knowledge\`(已备份到 `_backup/2026-06-07/`)
34
+ - 删除 `D:\SanQian.Xu\web-remote-control\knowledge\`(原为空目录)
package/INDEX.md ADDED
@@ -0,0 +1,53 @@
1
+ # 项目索引
2
+
3
+ > 5 个项目的速查表。按问题反查:「我想找 Python + 金融的」→ `quant-platform`;「我想找 WebRTC 的」→ `web-remote-control`。
4
+
5
+ ## 按角色反查
6
+
7
+ | 我想找… | 项目 |
8
+ |---------|------|
9
+ | 量化金融数据平台 | [quant-platform](./projects/quant-platform/README.md) |
10
+ | WebRTC 远程桌面控制 | [web-remote-control](./projects/web-remote-control/README.md) |
11
+ | Claude Code CLI 工具 | [claude-devsprite](./projects/claude-devsprite/README.md) |
12
+ | Claude Code Web UI(参考) | [claude-code-ui](./projects/claude-code-ui/README.md) |
13
+ | Token 消耗排行榜 / 统计 | [token-consumption-leaderboard](./projects/token-consumption-leaderboard/README.md) |
14
+
15
+ ## 按技术栈反查
16
+
17
+ | 技术栈 | 项目 |
18
+ |--------|------|
19
+ | Python + FastAPI + Vue3(CDN) + ECharts | [quant-platform](./projects/quant-platform/README.md) |
20
+ | TypeScript + WebRTC + PeerJS + rrweb | [web-remote-control](./projects/web-remote-control/README.md) |
21
+ | TypeScript + Node + Anthropic SDK + Vue 3 | [claude-devsprite](./projects/claude-devsprite/README.md) |
22
+ | React + Vite + Express + PostgreSQL + pnpm | [token-consumption-leaderboard](./projects/token-consumption-leaderboard/README.md) |
23
+ | React + Vite + Express(参考项目) | [claude-code-ui](./projects/claude-code-ui/README.md) |
24
+
25
+ ## 按标签反查
26
+
27
+ | 标签 | 项目 |
28
+ |------|------|
29
+ | `node` `claude` `skill` `knowledge-graph` | claude-devsprite |
30
+ | `react` `vite` `postgresql` `pnpm` `monorepo` | token-consumption-leaderboard |
31
+ | `fastapi` `vue3` `echarts` `akshare` `tushare` | quant-platform |
32
+ | `webrtc` `peerjs` `rrweb` `desktop-control` | web-remote-control |
33
+ | `react` `vite` `websocket` `claude-code` | claude-code-ui |
34
+
35
+ ## 项目活跃度(最近一次提交时间)
36
+
37
+ > 自动从 `projects.json` 与 `git log -1` 读取,此处为人工填充的快照。
38
+
39
+ | 项目 | 最近提交 | 状态 |
40
+ |------|---------|------|
41
+ | Claude-DevSprite | 2026-05-17 | 活跃 |
42
+ | Token Consumption Leaderboard | 2026-05-19 | 活跃 |
43
+ | web-remote-control | 2026-05-19 | 活跃 |
44
+ | Claude-Code-UI | — | 参考项目(不修改) |
45
+ | quant-platform | 2026-03-14 | 较慢 |
46
+
47
+ ## 详细导航
48
+
49
+ - [Claude Code UI →](./projects/claude-code-ui/README.md)
50
+ - [Claude-DevSprite →](./projects/claude-devsprite/README.md)
51
+ - [Token Consumption Leaderboard →](./projects/token-consumption-leaderboard/README.md)
52
+ - [quant-platform →](./projects/quant-platform/README.md)
53
+ - [web-remote-control →](./projects/web-remote-control/README.md)
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # SanQian.Xu 项目知识库
2
+
3
+ > 统一管理 `D:\SanQian.Xu` 下多个项目的项目框架、功能模块、git 提交的功能变更。
4
+
5
+ ## 目录速览
6
+
7
+ ```
8
+ project-knowledge-base/
9
+ ├── INDEX.md # 项目速查表(按角色/技术栈/标签)
10
+ ├── projects.json # 项目元信息(脚本读取)
11
+ ├── iterations.json # 里程碑定义
12
+ ├── projects/
13
+ │ ├── claude-code-ui/ # Claude Code UI(克隆参考项目)
14
+ │ ├── claude-devsprite/ # Claude-DevSprite(CLI + Web Dashboard)
15
+ │ ├── token-consumption-leaderboard/ # Token 消耗排行榜
16
+ │ ├── quant-platform/ # 量化金融数据平台
17
+ │ ├── web-remote-control/ # WebRTC 远程桌面
18
+ │ └── tokenrank-cloud/ # TokenRank Cloud
19
+ ├── scripts/
20
+ │ ├── gen-commit-doc.ps1 # 核心:git log → 功能文档
21
+ │ ├── gen-commit-doc.sh # Bash 版
22
+ │ ├── list-features.ps1 # 列出某项目所有功能
23
+ │ └── register-scheduled-task.bat # 注册每日定时任务
24
+ └── templates/ # 文档模板
25
+ ```
26
+
27
+ ## 阅读路径
28
+
29
+ 1. **第一次来** → 看 [INDEX.md](./INDEX.md) 了解项目概况
30
+ 2. **找某个项目的框架** → `projects/<slug>/framework.md`
31
+ 3. **找某个功能模块** → `projects/<slug>/modules/00-index.md`
32
+ 4. **找某次 git 提交的功能变更** → `projects/<slug>/commits/00-index.md`
33
+ 5. **找项目与源码的对应关系** → `projects/<slug>/references/source-map.md`
34
+
35
+ ## 项目 slug 对照
36
+
37
+ | slug | 项目目录 | 显示名 |
38
+ |------|----------|--------|
39
+ | `claude-code-ui` | `Claude-Code-UI\Reference Project\claudecodeui` | Claude Code UI |
40
+ | `claude-devsprite` | `Claude-DevSprite` | Claude-DevSprite |
41
+ | `token-consumption-leaderboard` | `Token Consumption Leaderboard` | Token Consumption Leaderboard |
42
+ | `quant-platform` | `quant-platform` | quant-platform |
43
+ | `web-remote-control` | `web-remote-control` | web-remote-control |
44
+ | `tokenrank-cloud` | `TokenRank Cloud` | TokenRank Cloud |
45
+
46
+ ## 如何更新知识库
47
+
48
+ ### 手动更新
49
+
50
+ - 改 `projects/<slug>/modules/*.md` 的「最近一次重要变更」段:PR 作者
51
+ - 改 `projects/<slug>/framework.md`:项目 owner(季度审查)
52
+ - 改 `INDEX.md`、`projects.json`、`iterations.json`:知识库 owner
53
+
54
+ ### 自动更新(git 提交 → 功能文档)
55
+
56
+ ```powershell
57
+ cd D:\SanQian.Xu\project-knowledge-base\scripts
58
+ .\gen-commit-doc.ps1 -ProjectSlug claude-devsprite # 单项目
59
+ .\gen-commit-doc.ps1 -ProjectSlug ALL # 全部项目
60
+ ```
61
+
62
+ 脚本行为:
63
+ - `feat` / `fix` / `refactor` / `perf` → 写入 `commits/`
64
+ - `docs` / `chore` / `test` / `style` → 跳过(不写入 commits/)
65
+ - 同 feature-slug 的提交 → **追加到已有功能文件**(不新建)
66
+ - `docs:` / `chore:` 视为维护类变更,不进 `commits/`
67
+
68
+ 详见 `templates/commit-feature.md` 的输出格式。
69
+
70
+ ## 维护约定
71
+
72
+ - **路径风格**:所有内部链接用 POSIX 风格(`/` 而非 `\`)
73
+ - **命名风格**:文件名 kebab-case 全小写 ASCII
74
+ - **Frontmatter**:每个 `.md` 顶部带 YAML frontmatter(用 `---` 包围)
75
+ - **每文件一职责**:一个模块一文件、一次功能交付一文件
76
+
77
+ ## 知识库自身的变更
78
+
79
+ 见 [CHANGELOG.md](./CHANGELOG.md)。
@@ -0,0 +1,63 @@
1
+ # KB · Management Site
2
+
3
+ Visual management UI for the knowledge base at `D:\SanQian.Xu\project-knowledge-base\`.
4
+
5
+ ## Quick start
6
+
7
+ 1. Double-click `start.bat`
8
+ 2. Browser opens to <http://localhost:7777/>
9
+ 3. To stop: double-click `stop.bat` (or close the minimized "KB-Site" console)
10
+
11
+ If port 7777 is busy, set `KB_SITE_PORT=8888` (or any free port) before launching.
12
+
13
+ ## What it does
14
+
15
+ The site has four tabs:
16
+
17
+ | Tab | Purpose |
18
+ |-----|---------|
19
+ | **Projects** | List of all projects in `project-knowledge-base/projects.json` with status badges (KB ready / missing), tags, links to README, and a tree viewer. "Remove" deletes from `projects.json` only (KB files untouched). |
20
+ | **+ Add** | Form to register a new project: slug, display name, local/git path, language, tags, optional reference flag. With "Create KB directory structure now" checked, it also seeds `architecture/`, `modules/`, `commits/`, `operations/`, `references/` plus README/framework from templates. |
21
+ | **Schedule & Run** | Live read of `KB-GitCommits-Daily` Windows task (state, next run, last result). Change frequency (off / hourly / every 6h / every 12h / daily / weekly). Run `gen-commit-doc.ps1` manually for a single project or ALL. |
22
+ | **Log** | Tail of the last script run (output + status). |
23
+
24
+ The page auto-refreshes every 30 seconds. The "Refresh" button forces an immediate reload.
25
+
26
+ ## Architecture
27
+
28
+ ```
29
+ _start.bat (double-click) → node server.js (port 7777)
30
+
31
+ ├─ GET / → index.html
32
+ ├─ GET /api/state → projects.json + schedule + lastRun
33
+ ├─ GET /api/projects
34
+ ├─ PUT /api/projects (add or replace)
35
+ ├─ POST /api/projects/:slug/init
36
+ ├─ GET /api/dirs/:slug (tree)
37
+ ├─ GET /api/schedule (Get-ScheduledTaskInfo)
38
+ ├─ PUT /api/schedule (schtasks /create or /delete)
39
+ ├─ POST /api/script/run (spawn gen-commit-doc.ps1)
40
+ └─ GET /api/script/status
41
+ ```
42
+
43
+ The server is **zero npm install** — uses only Node built-ins (`http`, `fs`, `path`, `child_process`). The frontend is **Vue 3 + Tailwind from CDN** — no build step.
44
+
45
+ ## Requirements
46
+
47
+ - Node.js 18+ (already present on this machine via the pnpm monorepo)
48
+ - Windows (uses `schtasks` and `Get-ScheduledTask` PowerShell cmdlets)
49
+ - Knowledge base initialized at `D:\SanQian.Xu\project-knowledge-base\` with `projects.json`
50
+
51
+ ## Security note
52
+
53
+ The server binds to `127.0.0.1` only — it is **not accessible from the network**. The endpoints can write to `projects.json` and invoke schtasks/PowerShell; do not expose this port.
54
+
55
+ ## Files
56
+
57
+ | File | Purpose |
58
+ |------|---------|
59
+ | `index.html` | Vue 3 SPA (loaded from CDN) |
60
+ | `server.js` | Node HTTP server, ~270 lines |
61
+ | `start.bat` | Launcher (open browser, start minimized console) |
62
+ | `stop.bat` | Kill the listening process |
63
+ | `README.md` | This file |
@@ -0,0 +1,199 @@
1
+ // TASK-005: AI profile and adapter test
2
+ // Run: node _site/_test/ai-profile-test.js
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+ const {
7
+ ADAPTERS, getAdapter, listAdapters,
8
+ validateInitialOutput, validateCommitBatchOutput,
9
+ } = require('../lib/ai-adapter');
10
+
11
+ const ROOT = path.resolve(__dirname, '..', '..');
12
+ const SERVER = path.join(ROOT, '_site', 'server.js');
13
+ const PROJECTS_JSON = path.join(ROOT, 'projects.json');
14
+ const AI_PROFILES_JSON = path.join(ROOT, 'ai-profiles.json');
15
+ const PORT = process.env.KB_AI_TEST_PORT || '7795';
16
+ const BASE_URL = `http://127.0.0.1:${PORT}`;
17
+ const TEMP_SLUG = 'task-005-temp';
18
+
19
+ function assert(cond, msg) { if (!cond) throw new Error(msg); }
20
+
21
+ async function waitForServer() {
22
+ const deadline = Date.now() + 15000;
23
+ let lastError;
24
+ while (Date.now() < deadline) {
25
+ try {
26
+ const res = await fetch(`${BASE_URL}/api/state`);
27
+ if (res.ok) return;
28
+ lastError = new Error(`HTTP ${res.status}`);
29
+ } catch (e) { lastError = e; }
30
+ await new Promise(r => setTimeout(r, 250));
31
+ }
32
+ throw lastError || new Error('server did not start');
33
+ }
34
+
35
+ async function json(method, url, body) {
36
+ const res = await fetch(`${BASE_URL}${url}`, {
37
+ method,
38
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
39
+ body: body ? JSON.stringify(body) : undefined,
40
+ });
41
+ const text = await res.text();
42
+ let data = {};
43
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
44
+ return { res, data };
45
+ }
46
+
47
+ async function cleanup() {
48
+ const base = path.join(ROOT, 'projects', TEMP_SLUG);
49
+ fs.rmSync(base, { recursive: true, force: true });
50
+ const cur = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
51
+ if (cur[TEMP_SLUG]) {
52
+ delete cur[TEMP_SLUG];
53
+ fs.writeFileSync(PROJECTS_JSON, JSON.stringify(cur, null, 2) + '\n', 'utf-8');
54
+ }
55
+ }
56
+
57
+ (async () => {
58
+ // 1. Static / unit tests on the adapter
59
+ assert(fs.existsSync(AI_PROFILES_JSON), 'ai-profiles.json should exist');
60
+ const cfg = JSON.parse(fs.readFileSync(AI_PROFILES_JSON, 'utf-8'));
61
+ assert(cfg.schema === 'ai-profiles/v1', 'ai-profiles schema should be v1');
62
+ assert(cfg.defaultProfileId, 'defaultProfileId should be set');
63
+ assert(Array.isArray(cfg.profiles) && cfg.profiles.length >= 1, 'at least one profile');
64
+
65
+ const adapters = listAdapters();
66
+ assert(adapters.length >= 1, 'at least one adapter');
67
+ assert(adapters.find(a => a.id === 'mock-agent'), 'mock-agent should be listed');
68
+
69
+ const mock = getAdapter('mock-agent');
70
+ assert(mock, 'getAdapter should return mock-agent');
71
+ assert(typeof mock.analyzeInitialProject === 'function', 'analyzeInitialProject should be a function');
72
+ assert(typeof mock.analyzeCommitBatch === 'function', 'analyzeCommitBatch should be a function');
73
+ assert(typeof mock.validateOutput === 'function', 'validateOutput should be a function');
74
+
75
+ // 2. Mock initial analysis: deterministic, valid output
76
+ const initialOut = await mock.analyzeInitialProject({
77
+ project: { slug: 'demo' },
78
+ contextPack: { entries: [{ path: 'README.md' }, { path: 'src/index.ts' }] },
79
+ });
80
+ const v1 = validateInitialOutput(initialOut);
81
+ assert(v1.valid, `mock initial output should validate: ${v1.errors.join('; ')}`);
82
+ assert(initialOut.goalDraft.oneSentenceGoal.includes('demo'), 'mock should mention project slug');
83
+ assert(initialOut.analysisDraft.evidence.length > 0, 'mock should cite evidence');
84
+
85
+ // 3. Mock commit batch: deterministic, valid output
86
+ const batchOut = await mock.analyzeCommitBatch({
87
+ project: { slug: 'demo' },
88
+ commits: [
89
+ { hash: 'aaaaaaaaaaaaaaaa', short: 'aaaaaaa', subject: 'feat: add foo' },
90
+ { hash: 'bbbbbbbbbbbbbbbb', short: 'bbbbbbb', subject: 'fix: repair foo' },
91
+ { hash: 'cccccccccccccccc', short: 'ccccccc', subject: 'test: add foo test' },
92
+ { hash: 'dddddddddddddddd', short: 'ddddddd', subject: 'refactor: rename foo' },
93
+ { hash: 'eeeeeeeeeeeeeeee', short: 'eeeeeee', subject: 'docs: update readme' },
94
+ ],
95
+ contextPack: { entries: [{ path: 'src/foo.ts' }] },
96
+ });
97
+ const v2 = validateCommitBatchOutput(batchOut);
98
+ assert(v2.valid, `mock commit output should validate: ${v2.errors.join('; ')}`);
99
+ assert(batchOut.changes.length === 5, 'should produce one change per commit');
100
+ const classes = batchOut.changes.map(c => c.classification);
101
+ assert(classes.includes('new-feature'), 'feat → new-feature');
102
+ assert(classes.includes('bug-fix'), 'fix → bug-fix');
103
+ assert(classes.includes('test-only'), 'test → test-only');
104
+ assert(classes.includes('refactor'), 'refactor → refactor');
105
+ assert(classes.includes('docs-only'), 'docs → docs-only');
106
+
107
+ // 4. Invalid output is rejected
108
+ const bad = validateInitialOutput({ goalDraft: {}, analysisDraft: {}, modules: 'nope', features: [] });
109
+ assert(!bad.valid, 'invalid output should be rejected');
110
+ const bad2 = validateCommitBatchOutput({ changes: [{ commit: 'a' }] });
111
+ assert(!bad2.valid, 'incomplete change entry should be rejected');
112
+
113
+ // 5. Unknown adapter id
114
+ assert(!getAdapter('nope'), 'unknown adapter should return null');
115
+
116
+ // 6. Server tests
117
+ assert(fs.existsSync(SERVER), 'server.js missing');
118
+ JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
119
+ const child = spawn(process.execPath, [SERVER], {
120
+ cwd: ROOT,
121
+ env: { ...process.env, KB_SITE_PORT: PORT, KB_SITE_HOST: '127.0.0.1' },
122
+ stdio: ['ignore', 'pipe', 'pipe'],
123
+ windowsHide: true,
124
+ });
125
+ let serverOutput = '';
126
+ child.stdout.on('data', d => { serverOutput += d.toString(); });
127
+ child.stderr.on('data', d => { serverOutput += d.toString(); });
128
+
129
+ try {
130
+ await cleanup();
131
+ await waitForServer();
132
+
133
+ // 7. GET /api/ai-profiles
134
+ let r = await json('GET', '/api/ai-profiles');
135
+ assert(r.res.ok, 'GET ai-profiles should return 200');
136
+ assert(r.data.ok, 'response should be ok');
137
+ assert(r.data.config && r.data.config.schema === 'ai-profiles/v1', 'config should be returned');
138
+ assert(Array.isArray(r.data.adapters) && r.data.adapters.length >= 1, 'adapters should be listed');
139
+
140
+ // 8. PUT /api/ai-profiles with invalid profile id → 400
141
+ r = await json('PUT', '/api/ai-profiles', {
142
+ schema: 'ai-profiles/v1',
143
+ defaultProfileId: 'nope',
144
+ profiles: [{ id: 'nope', name: 'nope', implementation: 'nope' }],
145
+ });
146
+ assert(!r.res.ok && r.res.status === 400, 'invalid profile id should 400');
147
+ assert(Array.isArray(r.data.errors), 'should return errors list');
148
+
149
+ // 9. PUT /api/ai-profiles with valid config
150
+ r = await json('PUT', '/api/ai-profiles', {
151
+ schema: 'ai-profiles/v1',
152
+ defaultProfileId: 'mock-agent',
153
+ profiles: [
154
+ { id: 'mock-agent', name: 'Mock', enabled: true, implementation: 'mock-agent' },
155
+ { id: 'claude-code-agent', name: 'Claude Code', enabled: false, implementation: 'claude-code-agent' },
156
+ ],
157
+ });
158
+ assert(r.res.ok, 'valid profile config should be accepted');
159
+
160
+ // 10. Per-project ai profile selection
161
+ r = await json('PUT', '/api/projects', {
162
+ slug: TEMP_SLUG,
163
+ config: { displayName: 'TASK-005' },
164
+ });
165
+ assert(r.res.ok, 'upsert should succeed');
166
+ assert(r.data.repoStatus !== undefined, 'repoStatus should be returned');
167
+
168
+ r = await json('PUT', `/api/projects/${TEMP_SLUG}/ai-profile`, { aiProfileId: 'mock-agent' });
169
+ assert(r.res.ok, 'set ai profile should succeed');
170
+ assert(r.data.aiProfileId === 'mock-agent', 'aiProfileId should be set');
171
+
172
+ r = await json('PUT', `/api/projects/${TEMP_SLUG}/settings`, {
173
+ aiProfileId: 'mock-agent',
174
+ knowledgeLanguage: 'en-US',
175
+ });
176
+ assert(r.res.ok, 'set project settings should succeed');
177
+ assert(r.data.aiProfileId === 'mock-agent', 'settings should return aiProfileId');
178
+ assert(r.data.knowledgeLanguage === 'en-US', 'settings should return knowledgeLanguage');
179
+
180
+ r = await json('PUT', `/api/projects/${TEMP_SLUG}/settings`, { knowledgeLanguage: 'fr-FR' });
181
+ assert(!r.res.ok && r.res.status === 400, 'invalid knowledgeLanguage should 400');
182
+
183
+ r = await json('PUT', `/api/projects/${TEMP_SLUG}/ai-profile`, { aiProfileId: 'nope' });
184
+ assert(!r.res.ok && r.res.status === 400, 'unknown adapter should 400');
185
+
186
+ r = await json('GET', '/api/projects');
187
+ assert(r.data[TEMP_SLUG].aiProfileId === 'mock-agent', 'aiProfileId should be persisted');
188
+ assert(r.data[TEMP_SLUG].knowledgeLanguage === 'en-US', 'knowledgeLanguage should be persisted');
189
+
190
+ console.log('TASK-005 AI profile test passed');
191
+ } catch (e) {
192
+ console.error('TASK-005 AI profile test failed:', e.message);
193
+ if (serverOutput) console.error(serverOutput);
194
+ process.exitCode = 1;
195
+ } finally {
196
+ await cleanup().catch(() => {});
197
+ child.kill();
198
+ }
199
+ })();
@@ -0,0 +1,132 @@
1
+ // Baseline TASK-001 test: project schema normalization and API smoke checks.
2
+ // Run from repository root: node _site/_test/baseline-schema-test.js
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ const ROOT = path.resolve(__dirname, '..', '..');
8
+ const SERVER = path.join(ROOT, '_site', 'server.js');
9
+ const PROJECTS_JSON = path.join(ROOT, 'projects.json');
10
+ const PORT = process.env.KB_BASELINE_TEST_PORT || '7791';
11
+ const BASE_URL = `http://127.0.0.1:${PORT}`;
12
+ const TEMP_SLUG = 'task-001-temp';
13
+
14
+ function assert(condition, message) {
15
+ if (!condition) throw new Error(message);
16
+ }
17
+
18
+ async function waitForServer() {
19
+ const deadline = Date.now() + 15000;
20
+ let lastError;
21
+ while (Date.now() < deadline) {
22
+ try {
23
+ const res = await fetch(`${BASE_URL}/api/state`);
24
+ if (res.ok) return;
25
+ lastError = new Error(`HTTP ${res.status}`);
26
+ } catch (e) {
27
+ lastError = e;
28
+ }
29
+ await new Promise(resolve => setTimeout(resolve, 250));
30
+ }
31
+ throw lastError || new Error('server did not start');
32
+ }
33
+
34
+ async function json(method, url, body) {
35
+ const res = await fetch(`${BASE_URL}${url}`, {
36
+ method,
37
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
38
+ body: body ? JSON.stringify(body) : undefined,
39
+ });
40
+ const text = await res.text();
41
+ let data = {};
42
+ if (text) {
43
+ try { data = JSON.parse(text); } catch { data = { raw: text }; }
44
+ }
45
+ return { res, data };
46
+ }
47
+
48
+ async function removeTempProject() {
49
+ const current = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf8'));
50
+ if (current[TEMP_SLUG]) {
51
+ delete current[TEMP_SLUG];
52
+ const result = await json('PUT', '/api/projects', { projects: current });
53
+ assert(result.res.ok, 'failed to remove temp project');
54
+ }
55
+ }
56
+
57
+ (async () => {
58
+ JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf8'));
59
+
60
+ const child = spawn(process.execPath, [SERVER], {
61
+ cwd: ROOT,
62
+ env: { ...process.env, KB_SITE_PORT: PORT, KB_SITE_HOST: '127.0.0.1' },
63
+ stdio: ['ignore', 'pipe', 'pipe'],
64
+ windowsHide: true,
65
+ });
66
+
67
+ let serverOutput = '';
68
+ child.stdout.on('data', d => { serverOutput += d.toString(); });
69
+ child.stderr.on('data', d => { serverOutput += d.toString(); });
70
+
71
+ try {
72
+ await waitForServer();
73
+
74
+ const stateResult = await json('GET', '/api/state');
75
+ assert(stateResult.res.ok, '/api/state should return 200');
76
+ assert(stateResult.data.projectSchemaVersion === 'v1', 'projectSchemaVersion should be v1');
77
+ assert(stateResult.data.kbRoot.endsWith('project-knowledge-base'), 'kbRoot should point to project-knowledge-base');
78
+
79
+ const projects = stateResult.data.projects;
80
+ assert(projects && Object.keys(projects).length >= 1, 'state should include projects');
81
+ for (const [slug, cfg] of Object.entries(projects)) {
82
+ assert(Object.prototype.hasOwnProperty.call(cfg, 'enabled'), `${slug} missing enabled`);
83
+ assert(Object.prototype.hasOwnProperty.call(cfg, 'repoStatus'), `${slug} missing repoStatus`);
84
+ assert(Object.prototype.hasOwnProperty.call(cfg, 'headCommit'), `${slug} missing headCommit`);
85
+ assert(Object.prototype.hasOwnProperty.call(cfg, 'lastSeenCommit'), `${slug} missing lastSeenCommit`);
86
+ assert(Object.prototype.hasOwnProperty.call(cfg, 'lastAnalyzedCommit'), `${slug} missing lastAnalyzedCommit`);
87
+ assert(cfg.aiProfileId, `${slug} missing aiProfileId`);
88
+ assert(cfg.kbSchemaVersion === 'v1', `${slug} missing kbSchemaVersion v1`);
89
+ assert(cfg.goalStatus, `${slug} missing goalStatus`);
90
+ assert(!String(cfg.kbPath || '').includes('\\SanQian.Xu\\kb\\'), `${slug} uses legacy kbPath`);
91
+ }
92
+
93
+ await removeTempProject();
94
+ const upsertResult = await json('PUT', '/api/projects', {
95
+ slug: TEMP_SLUG,
96
+ config: {
97
+ displayName: 'TASK-001 Temp',
98
+ localPath: ROOT,
99
+ gitPath: ROOT,
100
+ kbPath: `D:\\SanQian.Xu\\kb\\projects\\${TEMP_SLUG}`,
101
+ },
102
+ });
103
+ assert(upsertResult.res.ok, 'temp project upsert should succeed');
104
+
105
+ const projectsResult = await json('GET', '/api/projects');
106
+ assert(projectsResult.res.ok, '/api/projects should return 200');
107
+ const temp = projectsResult.data[TEMP_SLUG];
108
+ assert(temp, 'temp project should exist');
109
+ assert(temp.kbPath.endsWith(`project-knowledge-base\\projects\\${TEMP_SLUG}`), 'legacy kbPath should be normalized');
110
+ assert(temp.enabled === true, 'temp project should default enabled=true');
111
+ // repoStatus is auto-validated on upsert; ROOT may or may not be a git repo.
112
+ // Accept any of the well-defined statuses that prove normalization ran.
113
+ assert(['ok', 'unknown', 'not-git', 'missing-path'].includes(temp.repoStatus),
114
+ `temp project repoStatus should be a known value, got ${temp.repoStatus}`);
115
+
116
+ const htmlResult = await fetch(`${BASE_URL}/`);
117
+ const html = await htmlResult.text();
118
+ assert(htmlResult.ok, 'site root should return 200');
119
+ assert(html.includes('id="app"'), 'site HTML should include Vue app root');
120
+ assert(html.includes('Projects'), 'site HTML should include Projects tab');
121
+
122
+ await removeTempProject();
123
+ console.log('TASK-001 baseline schema test passed');
124
+ } catch (e) {
125
+ console.error('TASK-001 baseline schema test failed:', e.message);
126
+ if (serverOutput) console.error(serverOutput);
127
+ process.exitCode = 1;
128
+ } finally {
129
+ await removeTempProject().catch(() => {});
130
+ child.kill();
131
+ }
132
+ })();