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
@@ -0,0 +1,133 @@
1
+ // _site/scripts/hook-trigger.js
2
+ //
3
+ // Lightweight, fire-and-forget trigger invoked by `<repo>/.git/hooks/post-commit`.
4
+ // The hook is a tiny shim that runs `node <path-to-this-file>` with the
5
+ // repo path as argv. This script then asks the KB server to run a `safe` job
6
+ // for that project (scan + analyze-commits, no apply), and silently exits 0
7
+ // even if the server is unreachable — the commit must never be blocked.
8
+ //
9
+ // Usage from a git hook:
10
+ // node "$(git rev-parse --show-toplevel)/.kb/hook-trigger.js" \
11
+ // --kb-root <path-to-KB-site-root> \
12
+ // --repo <absolute-repo-path>
13
+ //
14
+ // Behavior contract (NEVER violated):
15
+ // * Always exits 0. The user's commit is sacred.
16
+ // * HTTP timeout is 2 seconds total; if the server doesn't respond fast,
17
+ // the trigger is abandoned and the failure is appended to a log file.
18
+ // * If the server is reachable, the script returns immediately after
19
+ // dispatching the job — it does NOT wait for the job to finish.
20
+ // * Failures are appended to `<kbRoot>/.hook-trigger-errors.log`.
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const os = require('os');
25
+ const http = require('http');
26
+
27
+ const args = process.argv.slice(2);
28
+ function arg(name, fallback) {
29
+ const i = args.indexOf(name);
30
+ if (i < 0) return fallback;
31
+ return args[i + 1] || fallback;
32
+ }
33
+ const KB_ROOT = arg('--kb-root', '');
34
+ const REPO = arg('--repo', '');
35
+ const HOST = arg('--host', '127.0.0.1');
36
+ const PORT = parseInt(arg('--port', process.env.KB_SITE_PORT || '7777'), 10);
37
+
38
+ const HOOK_TIMEOUT_MS = 2000;
39
+ const LOG_FILENAME = '.hook-trigger-errors.log';
40
+
41
+ function logError(msg) {
42
+ if (!KB_ROOT) return; // Without kb-root we have nowhere to log
43
+ try {
44
+ const logPath = path.join(KB_ROOT, LOG_FILENAME);
45
+ const stamp = new Date().toISOString();
46
+ fs.appendFileSync(logPath, `[${stamp}] ${msg}\n`, 'utf-8');
47
+ } catch { /* swallow */ }
48
+ }
49
+
50
+ function postJson(p, body) {
51
+ return new Promise((resolve, reject) => {
52
+ const data = JSON.stringify(body);
53
+ const req = http.request({
54
+ host: HOST, port: PORT, method: 'POST', path: p,
55
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
56
+ timeout: HOOK_TIMEOUT_MS,
57
+ }, (res) => {
58
+ res.on('data', () => {});
59
+ res.on('end', () => resolve({ status: res.statusCode }));
60
+ });
61
+ req.on('error', reject);
62
+ req.on('timeout', () => req.destroy(new Error('timeout')));
63
+ req.write(data);
64
+ req.end();
65
+ });
66
+ }
67
+
68
+ function getJson(p) {
69
+ return new Promise((resolve, reject) => {
70
+ const req = http.request({
71
+ host: HOST, port: PORT, method: 'GET', path: p, timeout: HOOK_TIMEOUT_MS,
72
+ }, (res) => {
73
+ const chunks = [];
74
+ res.on('data', c => chunks.push(c));
75
+ res.on('end', () => {
76
+ const text = Buffer.concat(chunks).toString('utf-8');
77
+ let json = {};
78
+ try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
79
+ resolve({ status: res.statusCode, data: json });
80
+ });
81
+ });
82
+ req.on('error', reject);
83
+ req.on('timeout', () => req.destroy(new Error('timeout')));
84
+ req.end();
85
+ });
86
+ }
87
+
88
+ async function findSlugForRepo(repoAbs) {
89
+ // The KB server is authoritative on which project owns this repo path.
90
+ // /api/state.projects[*].gitPath is the canonical mapping.
91
+ const r = await getJson('/api/state');
92
+ if (r.status !== 200 || !r.data || !r.data.projects) return null;
93
+ const norm = p => path.resolve(p).toLowerCase().replace(/[\\\/]+$/, '');
94
+ const target = norm(repoAbs);
95
+ for (const [slug, cfg] of Object.entries(r.data.projects)) {
96
+ if (cfg && cfg.gitPath && norm(cfg.gitPath) === target) return slug;
97
+ if (cfg && cfg.localPath && norm(cfg.localPath) === target) return slug;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ (async () => {
103
+ if (!KB_ROOT || !REPO) {
104
+ // Without these we cannot do anything useful; just bail silently.
105
+ process.exit(0);
106
+ }
107
+
108
+ // 1. Resolve the project slug for this repo.
109
+ let slug;
110
+ try {
111
+ slug = await findSlugForRepo(REPO);
112
+ } catch (e) {
113
+ logError(`slug lookup failed for repo=${REPO}: ${e.message}`);
114
+ process.exit(0);
115
+ }
116
+ if (!slug) {
117
+ logError(`no KB project registered for repo=${REPO}`);
118
+ process.exit(0);
119
+ }
120
+
121
+ // 2. Dispatch a `safe` job for this single project. Fire-and-forget: we
122
+ // do not wait for the job to finish; the server will record the result.
123
+ try {
124
+ const r = await postJson('/api/jobs/run', { mode: 'safe', slug });
125
+ if (r.status !== 200) {
126
+ logError(`dispatch non-200 for slug=${slug} repo=${REPO}: HTTP ${r.status}`);
127
+ }
128
+ } catch (e) {
129
+ logError(`dispatch failed for slug=${slug} repo=${REPO}: ${e.message}`);
130
+ }
131
+
132
+ process.exit(0);
133
+ })();
@@ -0,0 +1,151 @@
1
+ // _site/scripts/safe-runner.js
2
+ //
3
+ // Standalone safe-mode runner. Invoked by the Windows scheduled task.
4
+ // Performs scan + analyze-commits for every enabled project, and explicitly
5
+ // never calls apply, so AI drafts cannot become trusted knowledge without a
6
+ // human at the Drafts tab.
7
+ //
8
+ // Usage:
9
+ // node _site/scripts/safe-runner.js [--port 7777] [--host 127.0.0.1] [--slug ALL]
10
+ //
11
+ // The script does NOT spawn the server; the server must already be running.
12
+ // If the server is unreachable, the script exits with a non-zero code so the
13
+ // scheduled task registers a clear failure.
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const http = require('http');
18
+
19
+ const args = process.argv.slice(2);
20
+ function arg(name, fallback) {
21
+ const i = args.indexOf(name);
22
+ if (i < 0) return fallback;
23
+ return args[i + 1] || fallback;
24
+ }
25
+ const HOST = arg('--host', '127.0.0.1');
26
+ const PORT = parseInt(arg('--port', process.env.KB_SITE_PORT || '7777'), 10);
27
+ const SLUG = arg('--slug', 'ALL');
28
+
29
+ function log(line) {
30
+ const stamp = new Date().toISOString();
31
+ process.stdout.write(`[${stamp}] ${line}\n`);
32
+ }
33
+
34
+ function postJson(path_, body) {
35
+ return new Promise((resolve, reject) => {
36
+ const data = JSON.stringify(body);
37
+ const req = http.request({
38
+ host: HOST, port: PORT, method: 'POST', path: path_,
39
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
40
+ timeout: 5000,
41
+ }, (res) => {
42
+ const chunks = [];
43
+ res.on('data', c => chunks.push(c));
44
+ res.on('end', () => {
45
+ const text = Buffer.concat(chunks).toString('utf-8');
46
+ let json = {};
47
+ try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
48
+ resolve({ status: res.statusCode, data: json });
49
+ });
50
+ });
51
+ req.on('error', reject);
52
+ req.on('timeout', () => req.destroy(new Error('request timeout')));
53
+ req.write(data);
54
+ req.end();
55
+ });
56
+ }
57
+
58
+ function getJson(path_) {
59
+ return new Promise((resolve, reject) => {
60
+ const req = http.request({
61
+ host: HOST, port: PORT, method: 'GET', path: path_, timeout: 5000,
62
+ }, (res) => {
63
+ const chunks = [];
64
+ res.on('data', c => chunks.push(c));
65
+ res.on('end', () => {
66
+ const text = Buffer.concat(chunks).toString('utf-8');
67
+ let json = {};
68
+ try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
69
+ resolve({ status: res.statusCode, data: json });
70
+ });
71
+ });
72
+ req.on('error', reject);
73
+ req.on('timeout', () => req.destroy(new Error('request timeout')));
74
+ req.end();
75
+ });
76
+ }
77
+
78
+ async function waitForServer(maxMs = 15000) {
79
+ const deadline = Date.now() + maxMs;
80
+ let lastError;
81
+ while (Date.now() < deadline) {
82
+ try {
83
+ const r = await getJson('/api/state');
84
+ if (r.status >= 200 && r.status < 500) return true;
85
+ lastError = new Error(`HTTP ${r.status}`);
86
+ } catch (e) { lastError = e; }
87
+ await new Promise(r => setTimeout(r, 500));
88
+ }
89
+ throw lastError || new Error('server not reachable');
90
+ }
91
+
92
+ async function pollJob(jobId, maxMs = 10 * 60 * 1000) {
93
+ const deadline = Date.now() + maxMs;
94
+ while (Date.now() < deadline) {
95
+ const r = await getJson(`/api/jobs/${jobId}`);
96
+ if (r.status === 404) {
97
+ // Job not in the running map and not in the persisted log yet
98
+ await new Promise(r => setTimeout(r, 1000));
99
+ continue;
100
+ }
101
+ if (r.status >= 200 && r.status < 300 && r.data && r.data.job) {
102
+ if (r.data.job.status === 'running') {
103
+ await new Promise(r => setTimeout(r, 1500));
104
+ continue;
105
+ }
106
+ return r.data.job;
107
+ }
108
+ throw new Error(`unexpected response: HTTP ${r.status} ${JSON.stringify(r.data).slice(0, 200)}`);
109
+ }
110
+ throw new Error('job did not complete within timeout');
111
+ }
112
+
113
+ (async () => {
114
+ log(`safe-runner starting: host=${HOST} port=${PORT} slug=${SLUG}`);
115
+
116
+ try {
117
+ await waitForServer();
118
+ } catch (e) {
119
+ log(`ERROR: server not reachable at http://${HOST}:${PORT}/ — ${e.message}`);
120
+ log(`(the safe-runner requires the KB site server to be running; start it with "node _site/server.js" first.)`);
121
+ process.exit(2);
122
+ }
123
+
124
+ log(`dispatching safe run (slug=${SLUG})`);
125
+ const start = await postJson('/api/jobs/run', { mode: 'safe', slug: SLUG });
126
+ if (start.status !== 200 || !start.data.ok) {
127
+ log(`ERROR: failed to dispatch job: HTTP ${start.status} ${JSON.stringify(start.data)}`);
128
+ process.exit(3);
129
+ }
130
+ const jobId = start.data.jobId;
131
+ log(`dispatched jobId=${jobId}`);
132
+
133
+ let job;
134
+ try {
135
+ job = await pollJob(jobId);
136
+ } catch (e) {
137
+ log(`ERROR: ${e.message}`);
138
+ process.exit(4);
139
+ }
140
+
141
+ log(`job ${jobId} finished: status=${job.status} exitCode=${job.exitCode}`);
142
+ if (job.summary) {
143
+ log(`summary: ${JSON.stringify(job.summary)}`);
144
+ }
145
+ if (job.output) {
146
+ const tail = job.output.split(/\r?\n/).filter(Boolean).slice(-20).join('\n');
147
+ if (tail) log(`tail of output:\n${tail}`);
148
+ }
149
+
150
+ process.exit(job.exitCode || 0);
151
+ })();