@sanqianx/project-knowledge 4.6.1 → 4.8.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 (198) hide show
  1. package/.agents/plugins/marketplace.json +17 -17
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +817 -780
  4. package/README.md +48 -36
  5. package/_modules/knowledge-engine/README.md +88 -0
  6. package/_modules/knowledge-engine/package.json +38 -0
  7. package/_modules/knowledge-engine/src/analysis-contract.js +25 -0
  8. package/_modules/knowledge-engine/src/analysis-worker.js +54 -0
  9. package/_modules/knowledge-engine/src/bin.js +52 -0
  10. package/_modules/knowledge-engine/src/bridge-client.js +24 -0
  11. package/_modules/knowledge-engine/src/coordinator.js +255 -0
  12. package/_modules/knowledge-engine/src/headless-analyzer.js +75 -0
  13. package/_modules/knowledge-engine/src/memories.js +85 -0
  14. package/_modules/knowledge-engine/src/migration.js +88 -0
  15. package/_modules/knowledge-engine/src/retrieval.js +116 -0
  16. package/_modules/knowledge-engine/src/runtime/README.md +7 -0
  17. package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
  18. package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
  19. package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
  20. package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
  21. package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
  22. package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
  23. package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
  24. package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
  25. package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
  26. package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
  27. package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
  28. package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
  29. package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
  30. package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
  31. package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
  32. package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
  33. package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
  34. package/_modules/knowledge-engine/src/server.js +155 -0
  35. package/_modules/knowledge-engine/src/store.js +239 -0
  36. package/_modules/knowledge-engine/ui/app.css +172 -0
  37. package/_modules/knowledge-engine/ui/app.js +297 -0
  38. package/_modules/knowledge-engine/ui/index.html +95 -0
  39. package/_site/README.md +30 -30
  40. package/_site/_test/ai-profile-resolver-test.js +137 -137
  41. package/_site/_test/automation-queue-test.js +14 -14
  42. package/_site/_test/automation-ui-test.js +40 -40
  43. package/_site/_test/background-task-registry-test.js +43 -43
  44. package/_site/_test/baseline-schema-test.js +91 -91
  45. package/_site/_test/bridge-adapter-test.js +1 -1
  46. package/_site/_test/bridge-consumer-service-test.js +1 -1
  47. package/_site/_test/chat-claudecodeui-match-test.js +92 -92
  48. package/_site/_test/claude-executable-discovery-test.js +62 -62
  49. package/_site/_test/claude-workbench-test.js +170 -170
  50. package/_site/_test/client-capture-restart-e2e-test.js +4 -4
  51. package/_site/_test/codex-conversation-projection-test.js +2 -1
  52. package/_site/_test/commit-boundary-freeze-test.js +1 -1
  53. package/_site/_test/commit-conversation-binding-test.js +95 -95
  54. package/_site/_test/commit-evidence-test.js +110 -110
  55. package/_site/_test/conversation-api-test.js +109 -109
  56. package/_site/_test/conversation-store-test.js +136 -136
  57. package/_site/_test/cross-repo-isolation-e2e-test.js +2 -2
  58. package/_site/_test/data-dir-migration-test.js +57 -57
  59. package/_site/_test/desktop-browser-compat-test.js +28 -28
  60. package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
  61. package/_site/_test/explicit-commit-processor-test.js +37 -37
  62. package/_site/_test/fixtures/make-git-repos.js +12 -1
  63. package/_site/_test/folder-picker-output-test.js +19 -19
  64. package/_site/_test/full-integration-e2e-test.js +165 -165
  65. package/_site/_test/git-validation-test.js +2 -2
  66. package/_site/_test/hook-runtime-endpoint-test.js +88 -88
  67. package/_site/_test/hook-status-repair-api-test.js +152 -152
  68. package/_site/_test/hook-trigger-test.js +85 -85
  69. package/_site/_test/import-preflight-api-test.js +234 -233
  70. package/_site/_test/index-writer-concurrency-test.js +135 -135
  71. package/_site/_test/integration-adapters-test.js +171 -171
  72. package/_site/_test/integration-capture-setup-test.js +1 -1
  73. package/_site/_test/integration-surface-coverage-test.js +164 -164
  74. package/_site/_test/knowledge-language-control-test.js +170 -170
  75. package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
  76. package/_site/_test/knowledge-query-test.js +54 -54
  77. package/_site/_test/knowledge-retrieval-service-test.js +77 -77
  78. package/_site/_test/knowledge-storage-startup-test.js +52 -52
  79. package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
  80. package/_site/_test/legacy-forward-compat-test.js +257 -257
  81. package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -363
  82. package/_site/_test/logging-api-test.js +89 -89
  83. package/_site/_test/logging-sse-no-gap-test.js +88 -88
  84. package/_site/_test/logging-ui-test.js +106 -104
  85. package/_site/_test/markdown-delta-overlay-test.js +85 -85
  86. package/_site/_test/markdown-maintenance-api-test.js +75 -75
  87. package/_site/_test/mcp-server-test.js +149 -149
  88. package/_site/_test/module-artifact-integrity-test.js +27 -0
  89. package/_site/_test/module-boundary-test.js +30 -0
  90. package/_site/_test/module-bridge-eventbridge-test.js +7 -0
  91. package/_site/_test/module-model-configuration-test.js +43 -0
  92. package/_site/_test/module-process-lifecycle-test.js +24 -0
  93. package/_site/_test/module-stream-proxy-test.js +34 -0
  94. package/_site/_test/non-release-ci-test.js +34 -34
  95. package/_site/_test/offline-boundary-isolation-test.js +32 -32
  96. package/_site/_test/p0-data-migration-characterization-test.js +37 -37
  97. package/_site/_test/p0-e2e-gate-test.js +347 -347
  98. package/_site/_test/packaged-ui-smoke-test.js +19 -10
  99. package/_site/_test/path-consistency-test.js +151 -151
  100. package/_site/_test/pending-sweep-test.js +9 -9
  101. package/_site/_test/post-commit-automation-test.js +87 -87
  102. package/_site/_test/product-diagnostics-ui-test.js +75 -0
  103. package/_site/_test/product-import-ui-test.js +92 -0
  104. package/_site/_test/project-delete-recovery-test.js +64 -64
  105. package/_site/_test/project-goal-editor-test.js +144 -144
  106. package/_site/_test/project-layout-v2-migration-test.js +127 -127
  107. package/_site/_test/project-lifecycle-transaction-test.js +106 -106
  108. package/_site/_test/prompt-settings-test.js +115 -115
  109. package/_site/_test/protected-architecture-gate-test.js +122 -122
  110. package/_site/_test/refactor-characterization-test.js +36 -36
  111. package/_site/_test/release-version-sync-test.js +1 -1
  112. package/_site/_test/requirement-recorder-test.js +173 -173
  113. package/_site/_test/run-all-tests.js +159 -156
  114. package/_site/_test/server-runtime-migration-safety-test.js +27 -27
  115. package/_site/_test/shared-contracts-test.js +58 -58
  116. package/_site/_test/startup-analysis-disabled-test.js +10 -10
  117. package/_site/_test/storage-foundation-test.js +78 -78
  118. package/_site/_test/structured-logger-test.js +101 -101
  119. package/_site/_test/tracking-start-test.js +108 -108
  120. package/_site/_test/v4122-upgrade-data-contract-test.js +56 -56
  121. package/_site/_test/workbench-permission-test.js +117 -117
  122. package/_site/_test/workspace-ui-contract-test.js +28 -43
  123. package/_site/lib/ai-profile-resolver.js +78 -78
  124. package/_site/lib/ai-workspace.js +101 -101
  125. package/_site/lib/automation-config.js +140 -140
  126. package/_site/lib/bridge-adapter.js +5 -1
  127. package/_site/lib/bridge-consumer-service.js +402 -402
  128. package/_site/lib/claude-cli-runner.js +1510 -1510
  129. package/_site/lib/commit-conversation-binder.js +234 -234
  130. package/_site/lib/commit-processing-ledger.js +27 -27
  131. package/_site/lib/commit-prompt.js +202 -202
  132. package/_site/lib/commit-reconciler.js +406 -406
  133. package/_site/lib/contracts.js +284 -279
  134. package/_site/lib/conversation-query-service.js +144 -144
  135. package/_site/lib/conversation-store.js +385 -385
  136. package/_site/lib/data-dir.js +74 -74
  137. package/_site/lib/data-state-classifier.js +40 -40
  138. package/_site/lib/engine-cutover.js +97 -0
  139. package/_site/lib/evidence-bundle.js +228 -228
  140. package/_site/lib/folder-picker-output.js +21 -21
  141. package/_site/lib/git-runner.js +62 -62
  142. package/_site/lib/github-team-store.js +1172 -1172
  143. package/_site/lib/hook-manager.js +203 -202
  144. package/_site/lib/index-service.js +173 -173
  145. package/_site/lib/integration-installer.js +907 -907
  146. package/_site/lib/kb-framework.js +176 -176
  147. package/_site/lib/kb-validator.js +124 -124
  148. package/_site/lib/knowledge-promotion.js +435 -435
  149. package/_site/lib/knowledge-retrieval-service.js +401 -401
  150. package/_site/lib/knowledge-tool-runtime.js +359 -359
  151. package/_site/lib/legacy-data-manifest.js +26 -26
  152. package/_site/lib/llm-client.js +161 -161
  153. package/_site/lib/markdown-knowledge-indexer.js +230 -230
  154. package/_site/lib/migration-service.js +442 -442
  155. package/_site/lib/module-bridge.js +198 -33
  156. package/_site/lib/post-commit-automation.js +87 -87
  157. package/_site/lib/project-lifecycle-service.js +519 -497
  158. package/_site/lib/project-store.js +268 -262
  159. package/_site/lib/requirement-recorder.js +278 -278
  160. package/_site/lib/runtime-endpoint.js +155 -155
  161. package/_site/lib/scanner.js +298 -298
  162. package/_site/lib/server-app.js +1590 -1520
  163. package/_site/lib/settings-store.js +133 -113
  164. package/_site/lib/storage-layout.js +162 -162
  165. package/_site/lib/structured-logger.js +574 -574
  166. package/_site/scripts/folder-picker.ps1 +156 -156
  167. package/_site/scripts/hook-trigger.js +157 -156
  168. package/_site/scripts/install-module-candidates.js +69 -0
  169. package/_site/scripts/pack-module-candidates.js +72 -0
  170. package/_site/scripts/sync-release-version.js +2 -2
  171. package/_site/scripts/vendor-modules.js +47 -0
  172. package/_site/scripts/verify-module-runtime.js +45 -0
  173. package/_site/scripts/verify-product-runtime.js +34 -0
  174. package/bin/project-knowledge-mcp.js +194 -194
  175. package/docs/README.zh-CN.md +34 -25
  176. package/docs/project-registry-schema.md +22 -22
  177. package/docs/testing-strategy.md +33 -33
  178. package/module-runtime-manifest.json +220 -0
  179. package/package.json +13 -10
  180. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  181. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  182. package/plugins/project-knowledge/.mcp.json +1 -1
  183. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  184. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  185. package/templates/project-readme.md +34 -34
  186. package/ui/favicon.svg +38 -38
  187. package/ui/index.html +51 -149
  188. package/ui/product.css +9 -0
  189. package/ui/product.js +386 -0
  190. package/vendor-manifest.json +46 -0
  191. package/_site/_test/import-ui-flow-test.js +0 -171
  192. package/_site/_test/project-control-panel-task14-test.js +0 -63
  193. package/_site/_test/task15-20-ui-flow-test.js +0 -148
  194. package/_site/_test/ui-i18n-toggle-test.js +0 -114
  195. package/_site/_test/ui-smoke-test.js +0 -73
  196. package/ui/app.css +0 -58
  197. package/ui/app.js +0 -607
  198. package/ui/i18n.js +0 -146
@@ -1,173 +1,173 @@
1
- // Run: node _site/_test/requirement-recorder-test.js
2
-
3
- const assert = require('assert');
4
- const fs = require('fs');
5
- const os = require('os');
6
- const path = require('path');
7
- const { spawnSync } = require('child_process');
8
- const { FIELD_LIMITS, SCHEMAS, DomainError } = require('../lib/contracts');
9
- const { StorageLayout } = require('../lib/storage-layout');
10
- const { ProjectRegistryStore } = require('../lib/project-registry-store');
11
- const { ProjectStore } = require('../lib/project-store');
12
- const { RequirementRecorder } = require('../lib/requirement-recorder');
13
- const { ConversationStore } = require('../lib/conversation-store');
14
- const { createRequirementMetadataAdapter } = require('../lib/requirement-adapters');
15
- const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
16
- const claudeRunner = require('../lib/claude-cli-runner');
17
-
18
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), `kb-requirement-recorder-${process.pid}-`));
19
- const dataDir = path.join(temp, 'data');
20
-
21
- function git(repo, args) {
22
- const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true });
23
- if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
24
- return String(result.stdout || '').trim();
25
- }
26
-
27
- function createRepo(name) {
28
- const repo = path.join(temp, name);
29
- fs.mkdirSync(repo, { recursive: true });
30
- git(repo, ['init']);
31
- git(repo, ['config', 'user.email', 'requirements@example.test']);
32
- git(repo, ['config', 'user.name', 'Requirement Test']);
33
- fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`, 'utf8');
34
- git(repo, ['add', 'README.md']);
35
- git(repo, ['commit', '-m', 'initial']);
36
- return repo;
37
- }
38
-
39
- async function addProject(registry, projects, projectId, repoPath) {
40
- await projects.create(projectId, {
41
- displayName: projectId,
42
- storageName: projectId,
43
- repoPath,
44
- knowledgePath: path.join(temp, 'knowledge', projectId),
45
- }, { trackingStartCommit: git(repoPath, ['rev-parse', 'HEAD']) });
46
- await registry.add(projectId, { displayName: projectId });
47
- }
48
-
49
- (async () => {
50
- const layout = new StorageLayout({ dataDir });
51
- const registry = new ProjectRegistryStore({ layout });
52
- const projects = new ProjectStore({ layout });
53
- await registry.initialize();
54
- const repoA = createRepo('repo-a');
55
- const repoB = createRepo('repo-b');
56
- await addProject(registry, projects, 'project-a', repoA);
57
- await addProject(registry, projects, 'project-b', repoB);
58
-
59
- const recorder = new RequirementRecorder({ layout, registryStore: registry, projectStore: projects });
60
- const conversations = new ConversationStore({ layout, projectStore: projects });
61
- assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-a')), false, 'requirements should be lazy-created');
62
- assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'unused project should not have an empty requirements file');
63
-
64
- const clients = ['claude', 'codex', 'opencode'];
65
- const records = [];
66
- for (const client of clients) {
67
- const adapter = createRequirementMetadataAdapter(recorder, client);
68
- records.push(await adapter({
69
- projectId: 'project-a',
70
- repoPath: repoA,
71
- sessionId: `session-${client}`,
72
- conversationId: `conversation-${client}`,
73
- text: `Implement API key validation for ${client}.`,
74
- apiKey: 'must-not-be-copied-from-metadata',
75
- }));
76
- }
77
- const schemaKeys = Object.keys(records[0]).sort();
78
- for (const record of records) {
79
- assert.strictEqual(record.schema, SCHEMAS.requirement);
80
- assert.deepStrictEqual(Object.keys(record).sort(), schemaKeys, 'all clients should produce the same requirement schema');
81
- assert.strictEqual(record.projectId, 'project-a');
82
- assert(record.requirementHash.startsWith('sha256:'), 'record should include a body hash');
83
- assert(record.headAtRecord && record.branch, 'trusted Git metadata should be captured when available');
84
- }
85
- const serialized = fs.readFileSync(layout.getProjectConversationEventsPath('project-a'), 'utf8');
86
- assert(!serialized.includes('must-not-be-copied-from-metadata'), 'credential-like adapter metadata must not be persisted');
87
- assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-a')), false, 'new explicit adapters must not create a second requirement store');
88
- assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'recording project A must not touch project B');
89
- assert.strictEqual(projects.readState('project-a').lastAnalyzedCommit, null, 'recording must not dispatch or advance analysis');
90
- assert.strictEqual(fs.existsSync(path.join(temp, 'knowledge', 'project-a')), false, 'recording must not create knowledge content');
91
-
92
- const resolvedByRoot = await recorder.recordRequirement({
93
- repoPath: path.join(repoA, 'README.md'),
94
- client: 'codex',
95
- sessionId: 'root-resolved',
96
- text: 'Resolve only from the current Git root.',
97
- });
98
- assert.strictEqual(resolvedByRoot.projectId, 'project-a');
99
- await assert.rejects(
100
- recorder.recordRequirement({ projectId: 'project-a', repoPath: repoB, client: 'codex', sessionId: 'wrong-root', text: 'Wrong repository.' }),
101
- error => error.code === 'PROJECT_NOT_FOUND',
102
- );
103
-
104
- const concurrent = await Promise.all(Array.from({ length: 32 }, (_, index) => recorder.recordRequirement({
105
- projectId: 'project-a',
106
- client: 'codex',
107
- sessionId: 'concurrent-session',
108
- text: `Concurrent requirement ${index}`,
109
- })));
110
- assert.strictEqual(new Set(concurrent.map(item => item.id)).size, 32, 'concurrent records should have unique ids');
111
- assert.strictEqual(conversations.readEvents('project-a').length, 36, 'every concurrent append should remain parseable in ConversationStore');
112
-
113
- await assert.rejects(
114
- recorder.recordRequirement({ projectId: 'project-a', client: 'codex', sessionId: 'oversized', text: 'x'.repeat(FIELD_LIMITS.requirement + 1) }),
115
- error => error.code === 'INVALID_ARGUMENT' && /exceeds/.test(error.message),
116
- );
117
-
118
- // T14 / I-02: the Workbench itself never appends Development Conversation
119
- // events; explicit record_requirement keeps working for Workbench sessions.
120
- const chat = claudeRunner.startChatSession({
121
- slug: 'project-a', projectPath: repoA, kbPath: path.join(temp, 'knowledge', 'project-a'),
122
- aiProfile: { id: 'test-profile', implementation: 'claude-code-agent', mainModel: 'test-model' },
123
- });
124
- // T15: every internal SDK session carries the Bridge capture-disable markers.
125
- const chatEnv = claudeRunner.getSession(chat.sessionId).claudeEnv || {};
126
- assert.strictEqual(chatEnv.AI_CODING_EVENT_BRIDGE_CAPTURE, '0', 'internal SDK sessions disable Bridge capture');
127
- assert.strictEqual(chatEnv.AI_CODING_EVENT_ORIGIN, 'project-knowledge-internal', 'internal SDK sessions are marked as Project-Knowledge internal');
128
- const beforeWorkbench = conversations.readEvents('project-a').length;
129
- const explicitRecord = await recorder.recordRequirement({
130
- projectId: 'project-a', repoPath: repoA, client: 'claude', sessionId: chat.sessionId, text: 'Explicit MCP requirement for a Workbench session.',
131
- });
132
- const explicitUser = conversations.readEvents('project-a').find(event => event.legacyRequirementId === explicitRecord.id);
133
- assert(explicitUser, 'explicit record_requirement still persists for Workbench sessions');
134
- for (const listener of claudeRunner.getSession(chat.sessionId).listeners) listener({ type: 'claude/result', result: 'Workbench reply must NOT be captured.', isError: false });
135
- await new Promise(resolve => setTimeout(resolve, 100));
136
- const afterWorkbench = conversations.readEvents('project-a');
137
- assert.strictEqual(afterWorkbench.length, beforeWorkbench + 1, 'only the explicit requirement was appended');
138
- assert(!afterWorkbench.some(event => String(event.content || '').includes('Workbench reply must NOT be captured.')), 'no embedded assistant capture exists');
139
- assert(!afterWorkbench.some(event => event.rawEventType === 'embedded-claude-result'), 'the embedded capture path is gone');
140
- claudeRunner.deleteSession(chat.sessionId);
141
-
142
- const failingRecorder = new RequirementRecorder({
143
- layout,
144
- projectStore: projects,
145
- conversationStore: { appendEvent: async () => { throw new Error('disk full'); } },
146
- resolver: { resolve: () => ({ projectId: 'project-a', config: { repoPath: repoA } }) },
147
- gitReader: { currentContext: () => ({ branch: null, headAtRecord: null }) },
148
- });
149
- await assert.rejects(
150
- failingRecorder.recordRequirement({ projectId: 'project-a', client: 'codex', sessionId: 'failure', text: 'Durability failure.', operationId: 'op-test-failure' }),
151
- error => error.operationId === 'op-test-failure' && error.retryable === true,
152
- );
153
-
154
- const runtime = new KnowledgeToolRuntime({ dataDir, cwd: repoA });
155
- const toolResult = await runtime.recordRequirement({
156
- projectId: 'project-b',
157
- repoPath: repoB,
158
- client: 'opencode',
159
- sessionId: 'mcp-session',
160
- text: 'MCP metadata write only.',
161
- });
162
- assert(toolResult.requirementId && !Object.hasOwn(toolResult, 'requirement'), 'MCP result should return an id without echoing private text');
163
- assert.strictEqual(conversations.readEvents('project-b').length, 1, 'MCP runtime should append to the shared ConversationStore');
164
- assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'MCP must not create legacy requirements.jsonl');
165
- await runtime.close();
166
-
167
- console.log('requirement-recorder-test PASS');
168
- })().catch(error => {
169
- console.error(error.stack || error.message);
170
- process.exitCode = 1;
171
- }).finally(() => {
172
- fs.rmSync(temp, { recursive: true, force: true });
173
- });
1
+ // Run: node _site/_test/requirement-recorder-test.js
2
+
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+ const { FIELD_LIMITS, SCHEMAS, DomainError } = require('../lib/contracts');
9
+ const { StorageLayout } = require('../lib/storage-layout');
10
+ const { ProjectRegistryStore } = require('../lib/project-registry-store');
11
+ const { ProjectStore } = require('../lib/project-store');
12
+ const { RequirementRecorder } = require('../lib/requirement-recorder');
13
+ const { ConversationStore } = require('../lib/conversation-store');
14
+ const { createRequirementMetadataAdapter } = require('../lib/requirement-adapters');
15
+ const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
16
+ const claudeRunner = require('../lib/claude-cli-runner');
17
+
18
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), `kb-requirement-recorder-${process.pid}-`));
19
+ const dataDir = path.join(temp, 'data');
20
+
21
+ function git(repo, args) {
22
+ const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true });
23
+ if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
24
+ return String(result.stdout || '').trim();
25
+ }
26
+
27
+ function createRepo(name) {
28
+ const repo = path.join(temp, name);
29
+ fs.mkdirSync(repo, { recursive: true });
30
+ git(repo, ['init']);
31
+ git(repo, ['config', 'user.email', 'requirements@example.test']);
32
+ git(repo, ['config', 'user.name', 'Requirement Test']);
33
+ fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`, 'utf8');
34
+ git(repo, ['add', 'README.md']);
35
+ git(repo, ['commit', '-m', 'initial']);
36
+ return repo;
37
+ }
38
+
39
+ async function addProject(registry, projects, projectId, repoPath) {
40
+ await projects.create(projectId, {
41
+ displayName: projectId,
42
+ storageName: projectId,
43
+ repoPath,
44
+ knowledgePath: path.join(temp, 'knowledge', projectId),
45
+ }, { trackingStartCommit: git(repoPath, ['rev-parse', 'HEAD']) });
46
+ await registry.add(projectId, { displayName: projectId });
47
+ }
48
+
49
+ (async () => {
50
+ const layout = new StorageLayout({ dataDir });
51
+ const registry = new ProjectRegistryStore({ layout });
52
+ const projects = new ProjectStore({ layout });
53
+ await registry.initialize();
54
+ const repoA = createRepo('repo-a');
55
+ const repoB = createRepo('repo-b');
56
+ await addProject(registry, projects, 'project-a', repoA);
57
+ await addProject(registry, projects, 'project-b', repoB);
58
+
59
+ const recorder = new RequirementRecorder({ layout, registryStore: registry, projectStore: projects });
60
+ const conversations = new ConversationStore({ layout, projectStore: projects });
61
+ assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-a')), false, 'requirements should be lazy-created');
62
+ assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'unused project should not have an empty requirements file');
63
+
64
+ const clients = ['claude', 'codex', 'opencode'];
65
+ const records = [];
66
+ for (const client of clients) {
67
+ const adapter = createRequirementMetadataAdapter(recorder, client);
68
+ records.push(await adapter({
69
+ projectId: 'project-a',
70
+ repoPath: repoA,
71
+ sessionId: `session-${client}`,
72
+ conversationId: `conversation-${client}`,
73
+ text: `Implement API key validation for ${client}.`,
74
+ apiKey: 'must-not-be-copied-from-metadata',
75
+ }));
76
+ }
77
+ const schemaKeys = Object.keys(records[0]).sort();
78
+ for (const record of records) {
79
+ assert.strictEqual(record.schema, SCHEMAS.requirement);
80
+ assert.deepStrictEqual(Object.keys(record).sort(), schemaKeys, 'all clients should produce the same requirement schema');
81
+ assert.strictEqual(record.projectId, 'project-a');
82
+ assert(record.requirementHash.startsWith('sha256:'), 'record should include a body hash');
83
+ assert(record.headAtRecord && record.branch, 'trusted Git metadata should be captured when available');
84
+ }
85
+ const serialized = fs.readFileSync(layout.getProjectConversationEventsPath('project-a'), 'utf8');
86
+ assert(!serialized.includes('must-not-be-copied-from-metadata'), 'credential-like adapter metadata must not be persisted');
87
+ assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-a')), false, 'new explicit adapters must not create a second requirement store');
88
+ assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'recording project A must not touch project B');
89
+ assert.strictEqual(projects.readState('project-a').lastAnalyzedCommit, null, 'recording must not dispatch or advance analysis');
90
+ assert.strictEqual(fs.existsSync(path.join(temp, 'knowledge', 'project-a')), false, 'recording must not create knowledge content');
91
+
92
+ const resolvedByRoot = await recorder.recordRequirement({
93
+ repoPath: path.join(repoA, 'README.md'),
94
+ client: 'codex',
95
+ sessionId: 'root-resolved',
96
+ text: 'Resolve only from the current Git root.',
97
+ });
98
+ assert.strictEqual(resolvedByRoot.projectId, 'project-a');
99
+ await assert.rejects(
100
+ recorder.recordRequirement({ projectId: 'project-a', repoPath: repoB, client: 'codex', sessionId: 'wrong-root', text: 'Wrong repository.' }),
101
+ error => error.code === 'PROJECT_NOT_FOUND',
102
+ );
103
+
104
+ const concurrent = await Promise.all(Array.from({ length: 32 }, (_, index) => recorder.recordRequirement({
105
+ projectId: 'project-a',
106
+ client: 'codex',
107
+ sessionId: 'concurrent-session',
108
+ text: `Concurrent requirement ${index}`,
109
+ })));
110
+ assert.strictEqual(new Set(concurrent.map(item => item.id)).size, 32, 'concurrent records should have unique ids');
111
+ assert.strictEqual(conversations.readEvents('project-a').length, 36, 'every concurrent append should remain parseable in ConversationStore');
112
+
113
+ await assert.rejects(
114
+ recorder.recordRequirement({ projectId: 'project-a', client: 'codex', sessionId: 'oversized', text: 'x'.repeat(FIELD_LIMITS.requirement + 1) }),
115
+ error => error.code === 'INVALID_ARGUMENT' && /exceeds/.test(error.message),
116
+ );
117
+
118
+ // T14 / I-02: the Workbench itself never appends Development Conversation
119
+ // events; explicit record_requirement keeps working for Workbench sessions.
120
+ const chat = claudeRunner.startChatSession({
121
+ slug: 'project-a', projectPath: repoA, kbPath: path.join(temp, 'knowledge', 'project-a'),
122
+ aiProfile: { id: 'test-profile', implementation: 'claude-code-agent', mainModel: 'test-model' },
123
+ });
124
+ // T15: every internal SDK session carries the Bridge capture-disable markers.
125
+ const chatEnv = claudeRunner.getSession(chat.sessionId).claudeEnv || {};
126
+ assert.strictEqual(chatEnv.AI_CODING_EVENT_BRIDGE_CAPTURE, '0', 'internal SDK sessions disable Bridge capture');
127
+ assert.strictEqual(chatEnv.AI_CODING_EVENT_ORIGIN, 'project-knowledge-internal', 'internal SDK sessions are marked as Project-Knowledge internal');
128
+ const beforeWorkbench = conversations.readEvents('project-a').length;
129
+ const explicitRecord = await recorder.recordRequirement({
130
+ projectId: 'project-a', repoPath: repoA, client: 'claude', sessionId: chat.sessionId, text: 'Explicit MCP requirement for a Workbench session.',
131
+ });
132
+ const explicitUser = conversations.readEvents('project-a').find(event => event.legacyRequirementId === explicitRecord.id);
133
+ assert(explicitUser, 'explicit record_requirement still persists for Workbench sessions');
134
+ for (const listener of claudeRunner.getSession(chat.sessionId).listeners) listener({ type: 'claude/result', result: 'Workbench reply must NOT be captured.', isError: false });
135
+ await new Promise(resolve => setTimeout(resolve, 100));
136
+ const afterWorkbench = conversations.readEvents('project-a');
137
+ assert.strictEqual(afterWorkbench.length, beforeWorkbench + 1, 'only the explicit requirement was appended');
138
+ assert(!afterWorkbench.some(event => String(event.content || '').includes('Workbench reply must NOT be captured.')), 'no embedded assistant capture exists');
139
+ assert(!afterWorkbench.some(event => event.rawEventType === 'embedded-claude-result'), 'the embedded capture path is gone');
140
+ claudeRunner.deleteSession(chat.sessionId);
141
+
142
+ const failingRecorder = new RequirementRecorder({
143
+ layout,
144
+ projectStore: projects,
145
+ conversationStore: { appendEvent: async () => { throw new Error('disk full'); } },
146
+ resolver: { resolve: () => ({ projectId: 'project-a', config: { repoPath: repoA } }) },
147
+ gitReader: { currentContext: () => ({ branch: null, headAtRecord: null }) },
148
+ });
149
+ await assert.rejects(
150
+ failingRecorder.recordRequirement({ projectId: 'project-a', client: 'codex', sessionId: 'failure', text: 'Durability failure.', operationId: 'op-test-failure' }),
151
+ error => error.operationId === 'op-test-failure' && error.retryable === true,
152
+ );
153
+
154
+ const runtime = new KnowledgeToolRuntime({ dataDir, cwd: repoA });
155
+ const toolResult = await runtime.recordRequirement({
156
+ projectId: 'project-b',
157
+ repoPath: repoB,
158
+ client: 'opencode',
159
+ sessionId: 'mcp-session',
160
+ text: 'MCP metadata write only.',
161
+ });
162
+ assert(toolResult.requirementId && !Object.hasOwn(toolResult, 'requirement'), 'MCP result should return an id without echoing private text');
163
+ assert.strictEqual(conversations.readEvents('project-b').length, 1, 'MCP runtime should append to the shared ConversationStore');
164
+ assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false, 'MCP must not create legacy requirements.jsonl');
165
+ await runtime.close();
166
+
167
+ console.log('requirement-recorder-test PASS');
168
+ })().catch(error => {
169
+ console.error(error.stack || error.message);
170
+ process.exitCode = 1;
171
+ }).finally(() => {
172
+ fs.rmSync(temp, { recursive: true, force: true });
173
+ });