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.
- package/CHANGELOG.md +34 -0
- package/INDEX.md +53 -0
- package/README.md +79 -0
- package/_site/README.md +63 -0
- package/_site/_test/ai-profile-test.js +199 -0
- package/_site/_test/baseline-schema-test.js +132 -0
- package/_site/_test/commit-analysis-test.js +184 -0
- package/_site/_test/context-pack-test.js +199 -0
- package/_site/_test/draft-apply-test.js +363 -0
- package/_site/_test/git-validation-test.js +171 -0
- package/_site/_test/hook-trigger-test.js +257 -0
- package/_site/_test/initial-analysis-test.js +228 -0
- package/_site/_test/job-orchestrator-test.js +297 -0
- package/_site/_test/kb-v2-templates-test.js +189 -0
- package/_site/_test/pr-consumer-contract-test.js +236 -0
- package/_site/_test/run-all-tests.js +135 -0
- package/_site/_test/scanner-test.js +206 -0
- package/_site/_test/ui-smoke-test.js +237 -0
- package/_site/_test/ui-test.js +237 -0
- package/_site/index.html +1166 -0
- package/_site/lib/ai-adapter.js +287 -0
- package/_site/lib/analysis-orchestrator.js +433 -0
- package/_site/lib/context-pack-builder.js +290 -0
- package/_site/lib/draft-apply.js +219 -0
- package/_site/lib/git-runner.js +26 -0
- package/_site/lib/hook-manager.js +148 -0
- package/_site/lib/job-orchestrator.js +231 -0
- package/_site/lib/kb-validator.js +224 -0
- package/_site/lib/llm-client.js +126 -0
- package/_site/lib/scanner.js +94 -0
- package/_site/scripts/hook-trigger.js +133 -0
- package/_site/scripts/safe-runner.js +151 -0
- package/_site/server.js +1058 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/ai-profiles.json +18 -0
- package/docs/ai-knowledge-base-system-design.md +395 -0
- package/docs/pr-consumer-contract.md +198 -0
- package/docs/project-goal.md +72 -0
- package/docs/project-registry-schema.md +46 -0
- package/docs/testing-strategy.md +169 -0
- package/iterations.json +23 -0
- package/package.json +47 -0
- package/scripts/gen-commit-doc.ps1 +178 -0
- package/scripts/gen-commit-doc.sh +197 -0
- package/scripts/list-features.ps1 +41 -0
- package/scripts/register-scheduled-task.bat +5 -0
- package/templates/change.md +59 -0
- package/templates/commit-feature.md +56 -0
- package/templates/feature.md +44 -0
- package/templates/framework.md +80 -0
- package/templates/index-header.md +3 -0
- package/templates/kb-manifest.json +38 -0
- package/templates/module.md +58 -0
- package/templates/project-analysis.md +48 -0
- package/templates/project-goal.md +55 -0
- package/templates/project-readme.md +60 -0
- package/templates/quality-review-rules.md +37 -0
- package/templates/update-entry.md +7 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// AI analyzer adapter interface (TASK-005)
|
|
2
|
+
// Each adapter implements the same shape so the orchestrator can swap implementations.
|
|
3
|
+
//
|
|
4
|
+
// analyzeInitialProject({ project, contextPack }) -> { goalDraft, analysisDraft, modules, features }
|
|
5
|
+
// analyzeCommitBatch({ project, commits, contextPack }) -> { changes: [...] }
|
|
6
|
+
// validateOutput(output) -> { valid: boolean, errors: string[] }
|
|
7
|
+
//
|
|
8
|
+
// All adapters MUST return objects that pass validateOutput. The orchestrator never
|
|
9
|
+
// writes drafts to disk unless the output validates.
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
function isString(v) { return typeof v === 'string'; }
|
|
15
|
+
function isNonEmptyString(v) { return isString(v) && v.length > 0; }
|
|
16
|
+
function isStringArray(v) { return Array.isArray(v) && v.every(isString); }
|
|
17
|
+
function isObject(v) { return v && typeof v === 'object' && !Array.isArray(v); }
|
|
18
|
+
|
|
19
|
+
function normalizeKnowledgeLanguage(value) {
|
|
20
|
+
return value === 'en-US' ? 'en-US' : 'zh-CN';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function languageName(value) {
|
|
24
|
+
return normalizeKnowledgeLanguage(value) === 'en-US' ? 'English' : 'Simplified Chinese';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initialProjectSchema = {
|
|
28
|
+
goalDraft: (v) => isObject(v) && isNonEmptyString(v.oneSentenceGoal || v.one_sentence_goal) && isStringArray(v.targetUsers || v.target_users),
|
|
29
|
+
analysisDraft: (v) => isObject(v) && isNonEmptyString(v.purpose) && isStringArray(v.capabilities),
|
|
30
|
+
modules: (v) => Array.isArray(v) && v.every(m => isObject(m) && isNonEmptyString(m.slug) && isNonEmptyString(m.role)),
|
|
31
|
+
features: (v) => Array.isArray(v) && v.every(f => isObject(f) && isNonEmptyString(f.slug) && isNonEmptyString(f.summary)),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const changeEntrySchema = {
|
|
35
|
+
commit: isNonEmptyString,
|
|
36
|
+
classification: (v) => ['new-feature', 'existing-feature-update', 'bug-fix', 'refactor', 'infrastructure', 'test-only', 'docs-only'].includes(v),
|
|
37
|
+
goalImpact: isNonEmptyString,
|
|
38
|
+
evidence: isStringArray,
|
|
39
|
+
proposedOps: (v) => Array.isArray(v) && v.every(op => isObject(op) && isNonEmptyString(op.op) && isNonEmptyString(op.path)),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function validateInitialOutput(output) {
|
|
43
|
+
const errors = [];
|
|
44
|
+
if (!isObject(output)) { errors.push('output is not an object'); return { valid: false, errors }; }
|
|
45
|
+
for (const [key, check] of Object.entries(initialProjectSchema)) {
|
|
46
|
+
if (!(key in output)) { errors.push(`missing field: ${key}`); continue; }
|
|
47
|
+
if (!check(output[key])) errors.push(`invalid field: ${key}`);
|
|
48
|
+
}
|
|
49
|
+
return { valid: errors.length === 0, errors };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateCommitBatchOutput(output) {
|
|
53
|
+
const errors = [];
|
|
54
|
+
if (!isObject(output)) { errors.push('output is not an object'); return { valid: false, errors }; }
|
|
55
|
+
if (!Array.isArray(output.changes)) { errors.push('missing or invalid: changes'); return { valid: false, errors }; }
|
|
56
|
+
for (let i = 0; i < output.changes.length; i++) {
|
|
57
|
+
const c = output.changes[i];
|
|
58
|
+
if (!isObject(c)) { errors.push(`change[${i}] is not an object`); continue; }
|
|
59
|
+
for (const [key, check] of Object.entries(changeEntrySchema)) {
|
|
60
|
+
if (!(key in c)) { errors.push(`change[${i}] missing: ${key}`); continue; }
|
|
61
|
+
if (!check(c[key])) errors.push(`change[${i}].${key} is invalid`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { valid: errors.length === 0, errors };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- mock-agent: deterministic output for tests ----
|
|
68
|
+
const mockAgent = {
|
|
69
|
+
id: 'mock-agent',
|
|
70
|
+
name: 'Mock Agent (deterministic)',
|
|
71
|
+
async analyzeInitialProject({ project, contextPack }) {
|
|
72
|
+
const slug = (project && project.slug) || 'unknown';
|
|
73
|
+
const zh = normalizeKnowledgeLanguage(project && project.knowledgeLanguage) === 'zh-CN';
|
|
74
|
+
return {
|
|
75
|
+
goalDraft: {
|
|
76
|
+
oneSentenceGoal: zh ? `[mock] ${slug} 提供自动化项目监督。` : `[mock] ${slug} provides automated project supervision.`,
|
|
77
|
+
targetUsers: zh ? ['开发者', '技术负责人'] : ['developer', 'tech-lead'],
|
|
78
|
+
coreScenarios: zh ? ['导入 Git 项目', '扫描提交', '审核草稿'] : ['import-git-project', 'scan-commits', 'review-drafts'],
|
|
79
|
+
successCriteria: zh ? ['草稿有证据支撑', '不会静默修改项目目标'] : ['drafts are evidence-backed', 'no silent goal edits'],
|
|
80
|
+
nonGoals: zh ? ['替代人工审核'] : ['replace human reviewers'],
|
|
81
|
+
priorityPrinciples: zh ? ['项目目标优先', '必须有证据'] : ['goal first', 'evidence required'],
|
|
82
|
+
prReviewPrinciples: zh ? ['引用证据', '不能自动覆盖 project-goal.md'] : ['cite evidence', 'never overwrite project-goal.md automatically'],
|
|
83
|
+
},
|
|
84
|
+
analysisDraft: {
|
|
85
|
+
purpose: zh ? `[mock] 根据上下文推断 ${slug} 的项目用途。` : `[mock] inferred purpose of ${slug} from context pack.`,
|
|
86
|
+
capabilities: zh ? ['Git 检查', '提交扫描', '草稿生成'] : ['git inspection', 'scanner', 'drafts'],
|
|
87
|
+
architecture: zh ? '分层结构:api -> scanner -> context -> adapter -> drafts' : 'layered: api -> scanner -> context -> adapter -> drafts',
|
|
88
|
+
modules: [
|
|
89
|
+
{ slug: 'api', role: zh ? 'HTTP 接口层' : 'HTTP surface' },
|
|
90
|
+
{ slug: 'scanner', role: zh ? 'Git 状态读取器' : 'git state reader' },
|
|
91
|
+
],
|
|
92
|
+
dataFlow: zh ? 'HTTP -> api -> scanner -> context pack -> adapter -> drafts' : 'HTTP -> api -> scanner -> context pack -> adapter -> drafts',
|
|
93
|
+
goalSupport: zh ? '支撑 goalDraft.coreScenarios 中的核心场景' : 'supports each scenario in goalDraft.coreScenarios',
|
|
94
|
+
gaps: [],
|
|
95
|
+
evidence: (contextPack && contextPack.entries) ? contextPack.entries.slice(0, 3).map(e => e.path) : [],
|
|
96
|
+
},
|
|
97
|
+
modules: [
|
|
98
|
+
{ slug: 'api', role: zh ? 'HTTP 接口层' : 'HTTP surface' },
|
|
99
|
+
{ slug: 'scanner', role: zh ? 'Git 状态读取器' : 'git state reader' },
|
|
100
|
+
],
|
|
101
|
+
features: [
|
|
102
|
+
{ slug: 'git-validation', summary: zh ? '检查 Git 路径并报告仓库状态' : 'inspect a git path and report status' },
|
|
103
|
+
{ slug: 'scanner', summary: zh ? '列出自上次应用以来待分析的提交' : 'enumerate pending commits since last apply' },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
async analyzeCommitBatch({ project, commits, contextPack }) {
|
|
108
|
+
const zh = normalizeKnowledgeLanguage(project && project.knowledgeLanguage) === 'zh-CN';
|
|
109
|
+
const changes = (commits || []).map(c => {
|
|
110
|
+
const subject = (c.subject || '').toLowerCase();
|
|
111
|
+
let classification = 'infrastructure';
|
|
112
|
+
if (/^feat[:\s]/.test(subject)) classification = 'new-feature';
|
|
113
|
+
else if (/^fix[:\s]/.test(subject)) classification = 'bug-fix';
|
|
114
|
+
else if (/^refactor[:\s]/.test(subject)) classification = 'refactor';
|
|
115
|
+
else if (/^test[:\s]/.test(subject)) classification = 'test-only';
|
|
116
|
+
else if (/^docs[:\s]/.test(subject)) classification = 'docs-only';
|
|
117
|
+
const slug = subject.replace(/^[^:]+:\s*/, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'change';
|
|
118
|
+
return {
|
|
119
|
+
commit: c.hash,
|
|
120
|
+
classification,
|
|
121
|
+
goalImpact: zh ? `[mock] ${classification} 对 ${slug} 的影响` : `[mock] ${classification} for ${slug}`,
|
|
122
|
+
evidence: contextPack && contextPack.entries ? contextPack.entries.slice(0, 2).map(e => e.path) : [],
|
|
123
|
+
proposedOps: classification === 'new-feature'
|
|
124
|
+
? [{ op: 'create-file', path: `features/${slug}.md`, fromTemplate: 'feature.md' }]
|
|
125
|
+
: [{ op: 'append-section', path: `changes/${c.short || (c.hash || '').slice(0,7)}.md`, fromTemplate: 'change.md' }],
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
return { changes };
|
|
129
|
+
},
|
|
130
|
+
validateOutput(output) {
|
|
131
|
+
if (output && Array.isArray(output.changes)) return validateCommitBatchOutput(output);
|
|
132
|
+
return validateInitialOutput(output);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ---- claude-code-agent: real LLM via Anthropic-compatible API ----
|
|
137
|
+
//
|
|
138
|
+
// This adapter calls an Anthropic Messages API (real Anthropic or any
|
|
139
|
+
// compatible proxy such as api.minimaxi.com/anthropic). The model returns
|
|
140
|
+
// JSON that the KB orchestrator validates against the existing schemas.
|
|
141
|
+
//
|
|
142
|
+
// The adapter is intentionally minimal: it does ONE call per phase
|
|
143
|
+
// (initial or commits) and asks the model to return a strict JSON object
|
|
144
|
+
// matching the schema. Validation happens in `validateOutput`; if the
|
|
145
|
+
// model misbehaves, the run is marked failed and the user can re-trigger.
|
|
146
|
+
|
|
147
|
+
const { completeJson } = require('./llm-client');
|
|
148
|
+
|
|
149
|
+
const INITIAL_SYSTEM = `You are a senior software architect analyzing a project's codebase to seed a knowledge base. You MUST respond with a single JSON object that matches the schema in the user message. Do not include commentary, prose, or markdown fences. Do not invent file paths. The project goal and analysis will be reviewed by a human before becoming trusted knowledge; aim for evidence-backed, concise claims.`;
|
|
150
|
+
|
|
151
|
+
const COMMIT_SYSTEM = `You are a senior software architect reviewing a batch of git commits. For each commit, decide what change should be recorded in the project's knowledge base. Respond with a single JSON object matching the schema in the user message. Do not invent file paths. Cite the provided evidence (context pack entries) whenever possible. Do not propose operations that would write outside features/ or changes/ or modules/ or project-goal.md or project-analysis.md.`;
|
|
152
|
+
|
|
153
|
+
function buildInitialUser({ project, contextPack }) {
|
|
154
|
+
const outputLanguage = languageName(project && project.knowledgeLanguage);
|
|
155
|
+
const ctx = (contextPack && contextPack.entries || []).slice(0, 50).map(e => `- ${e.path}: ${(e.summary || '').slice(0, 200)}`).join('\n');
|
|
156
|
+
return `Project slug: ${project.slug}
|
|
157
|
+
Display name: ${project.displayName || project.slug}
|
|
158
|
+
Primary language: ${project.primaryLanguage || 'unknown'}
|
|
159
|
+
Tags: ${(project.tags || []).join(', ')}
|
|
160
|
+
Knowledge output language: ${normalizeKnowledgeLanguage(project.knowledgeLanguage)} (${outputLanguage})
|
|
161
|
+
|
|
162
|
+
Context pack (top entries, latest first):
|
|
163
|
+
${ctx || '(no context pack available)'}
|
|
164
|
+
|
|
165
|
+
Return a JSON object with these fields:
|
|
166
|
+
{
|
|
167
|
+
"goalDraft": {
|
|
168
|
+
"oneSentenceGoal": string,
|
|
169
|
+
"targetUsers": [string],
|
|
170
|
+
"coreScenarios": [string],
|
|
171
|
+
"successCriteria": [string],
|
|
172
|
+
"nonGoals": [string],
|
|
173
|
+
"priorityPrinciples": [string]
|
|
174
|
+
},
|
|
175
|
+
"analysisDraft": {
|
|
176
|
+
"purpose": string,
|
|
177
|
+
"capabilities": [string],
|
|
178
|
+
"architecture": string,
|
|
179
|
+
"modules": [{ "slug": string, "role": string }],
|
|
180
|
+
"dataFlow": string,
|
|
181
|
+
"goalSupport": string,
|
|
182
|
+
"gaps": [string],
|
|
183
|
+
"evidence": [string]
|
|
184
|
+
},
|
|
185
|
+
"modules": [{ "slug": string, "role": string }],
|
|
186
|
+
"features": [{ "slug": string, "summary": string }]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Constraints:
|
|
190
|
+
- All human-readable natural language string values MUST be written in ${outputLanguage}. Keep JSON keys and file paths exactly as specified.
|
|
191
|
+
- All slugs MUST be kebab-case (a-z, 0-9, hyphen).
|
|
192
|
+
- The first two fields (goalDraft, analysisDraft) MUST be present.
|
|
193
|
+
- Do not include any text outside the JSON object.`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildCommitUser({ project, commits, contextPack }) {
|
|
197
|
+
const outputLanguage = languageName(project && project.knowledgeLanguage);
|
|
198
|
+
const ctx = (contextPack && contextPack.entries || []).slice(0, 30).map(e => `- ${e.path}: ${(e.summary || '').slice(0, 200)}`).join('\n');
|
|
199
|
+
const list = (commits || []).map(c => `- full=${c.hash} | short=${c.short || c.hash.slice(0,7)} | ${c.date || ''} | ${c.author || ''} | ${c.subject || ''}`).join('\n');
|
|
200
|
+
return `Project slug: ${project.slug}
|
|
201
|
+
Knowledge output language: ${normalizeKnowledgeLanguage(project.knowledgeLanguage)} (${outputLanguage})
|
|
202
|
+
|
|
203
|
+
Pending commits (chronological order, oldest first). For each line, the "full=" value is the COMPLETE 40-character commit hash; you MUST copy that exact value into the JSON "commit" field:
|
|
204
|
+
${list || '(no commits)'}
|
|
205
|
+
|
|
206
|
+
Context pack excerpts:
|
|
207
|
+
${ctx || '(no context pack available)'}
|
|
208
|
+
|
|
209
|
+
Return a JSON object of the form:
|
|
210
|
+
{
|
|
211
|
+
"changes": [
|
|
212
|
+
{
|
|
213
|
+
"commit": string, // FULL 40-char commit hash copied from the input
|
|
214
|
+
"classification": "new-feature" | "existing-feature-update" | "bug-fix" | "refactor" | "infrastructure" | "test-only" | "docs-only",
|
|
215
|
+
"goalImpact": string, // one short sentence
|
|
216
|
+
"evidence": [string], // paths cited from the context pack
|
|
217
|
+
"proposedOps": [{ "op": "create-file" | "append-section" | "update-section", "path": string, "fromTemplate"?: string }]
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Constraints:
|
|
223
|
+
- All human-readable natural language string values MUST be written in ${outputLanguage}. Keep JSON keys, enum values, file paths, and commit hashes exactly as specified.
|
|
224
|
+
- One entry per commit in the input list, in the same order.
|
|
225
|
+
- "commit" MUST be the full 40-character hash from the input (the value after "full="), NOT the short hash.
|
|
226
|
+
- proposedOps[].path MUST be a relative path under features/, changes/, modules/, or project-goal.md (the latter only if the change is to the goal).
|
|
227
|
+
- Do not include any text outside the JSON object.`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const claudeCodeAgent = {
|
|
231
|
+
id: 'claude-code-agent',
|
|
232
|
+
name: 'Claude Code Agent (Anthropic-compatible)',
|
|
233
|
+
description: 'Real LLM via ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN. Used by post-commit hook when projects.json sets aiProfileId=claude-code-agent.',
|
|
234
|
+
async analyzeInitialProject({ project, contextPack }) {
|
|
235
|
+
const user = buildInitialUser({ project, contextPack });
|
|
236
|
+
const r = await completeJson({ system: INITIAL_SYSTEM, user, maxTokens: 3000 });
|
|
237
|
+
if (!r.parsed) {
|
|
238
|
+
const e = new Error(`LLM returned no parseable JSON (${r.parseError && r.parseError.message || 'no output'})`);
|
|
239
|
+
e.llmText = r.text;
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
return r.parsed;
|
|
243
|
+
},
|
|
244
|
+
async analyzeCommitBatch({ project, commits, contextPack }) {
|
|
245
|
+
const user = buildCommitUser({ project, commits, contextPack });
|
|
246
|
+
const r = await completeJson({ system: COMMIT_SYSTEM, user, maxTokens: 4096 });
|
|
247
|
+
if (!r.parsed) {
|
|
248
|
+
console.error(`[claude-code-agent] LLM did not return valid JSON. text head: ${r.text.slice(0, 500)} ... text tail: ${r.text.slice(-500)}`);
|
|
249
|
+
const e = new Error(`LLM returned no parseable JSON (${r.parseError && r.parseError.message || 'no output'})`);
|
|
250
|
+
e.llmText = r.text;
|
|
251
|
+
throw e;
|
|
252
|
+
}
|
|
253
|
+
// Sanity log: how many changes did the LLM return, and what do the commits look like?
|
|
254
|
+
const changes = (r.parsed && r.parsed.changes) || [];
|
|
255
|
+
console.log(`[claude-code-agent] parsed ${changes.length} changes; first change: ${JSON.stringify(changes[0]).slice(0, 300)}`);
|
|
256
|
+
return r.parsed;
|
|
257
|
+
},
|
|
258
|
+
validateOutput(output) {
|
|
259
|
+
if (output && Array.isArray(output.changes)) return validateCommitBatchOutput(output);
|
|
260
|
+
return validateInitialOutput(output);
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const ADAPTERS = {
|
|
265
|
+
'mock-agent': mockAgent,
|
|
266
|
+
'claude-code-agent': claudeCodeAgent,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function getAdapter(id) {
|
|
270
|
+
return ADAPTERS[id] || null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function listAdapters() {
|
|
274
|
+
return Object.values(ADAPTERS).map(a => ({
|
|
275
|
+
id: a.id,
|
|
276
|
+
name: a.name,
|
|
277
|
+
description: a.description || null,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
ADAPTERS,
|
|
283
|
+
getAdapter,
|
|
284
|
+
listAdapters,
|
|
285
|
+
validateInitialOutput,
|
|
286
|
+
validateCommitBatchOutput,
|
|
287
|
+
};
|