@sanqianx/project-knowledge 5.0.19 → 5.0.20

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 (200) hide show
  1. package/.agents/plugins/marketplace.json +3 -3
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +5 -0
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +349 -349
  7. package/_modules/knowledge-engine/README.md +4 -0
  8. package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
  9. package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
  10. package/_modules/knowledge-engine/src/coordinator.js +7 -0
  11. package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
  12. package/_modules/knowledge-engine/src/server.js +1 -1
  13. package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
  14. package/_site/_test/ai-profile-test.js +75 -75
  15. package/_site/_test/ai-vendor-presets-test.js +91 -91
  16. package/_site/_test/automatic-context-test.js +70 -70
  17. package/_site/_test/bin-cli-test.js +230 -230
  18. package/_site/_test/bridge-adapter-test.js +95 -95
  19. package/_site/_test/bridge-consumer-service-test.js +310 -310
  20. package/_site/_test/candidate-bin-content-test.js +35 -35
  21. package/_site/_test/candidate-product-assets-test.js +13 -13
  22. package/_site/_test/candidate-release-contract-test.js +14 -14
  23. package/_site/_test/capture-hook-dedup-test.js +158 -158
  24. package/_site/_test/claude-executable-discovery-test.js +28 -28
  25. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
  26. package/_site/_test/cli-instance-management-test.js +62 -62
  27. package/_site/_test/client-capture-restart-e2e-test.js +189 -189
  28. package/_site/_test/codex-conversation-projection-test.js +60 -60
  29. package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
  30. package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
  31. package/_site/_test/codex-notify-fanout-test.js +134 -134
  32. package/_site/_test/codex-notify-upgrade-test.js +253 -253
  33. package/_site/_test/commit-boundary-freeze-test.js +147 -147
  34. package/_site/_test/commit-conversation-binding-test.js +4 -4
  35. package/_site/_test/configuration-generation-test.js +22 -22
  36. package/_site/_test/context-pack-test.js +200 -200
  37. package/_site/_test/conversation-api-test.js +10 -10
  38. package/_site/_test/conversation-origin-test.js +15 -15
  39. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
  40. package/_site/_test/embedding-config-test.js +32 -32
  41. package/_site/_test/fixtures/fake-browser-exit.js +10 -10
  42. package/_site/_test/fixtures/fake-browser-hang.js +10 -10
  43. package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
  44. package/_site/_test/fixtures/make-git-repos.js +92 -92
  45. package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
  46. package/_site/_test/fresh-hook-reimport-test.js +53 -53
  47. package/_site/_test/fresh-product-data-test.js +56 -56
  48. package/_site/_test/git-path-compatibility-test.js +80 -80
  49. package/_site/_test/git-validation-test.js +81 -81
  50. package/_site/_test/hook-worktree-test.js +42 -42
  51. package/_site/_test/index-builder-test.js +41 -41
  52. package/_site/_test/index-error-display-contract-test.js +10 -10
  53. package/_site/_test/integration-adapters-test.js +67 -67
  54. package/_site/_test/integration-capture-setup-test.js +118 -118
  55. package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
  56. package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
  57. package/_site/_test/integration-runtime-binding-test.js +473 -473
  58. package/_site/_test/integration-status-scan-test.js +29 -29
  59. package/_site/_test/kb-framework-test.js +57 -57
  60. package/_site/_test/kbpath-follow-test.js +63 -63
  61. package/_site/_test/knowledge-db-test.js +144 -144
  62. package/_site/_test/knowledge-engine-client-test.js +109 -109
  63. package/_site/_test/knowledge-maintenance-test.js +40 -40
  64. package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
  65. package/_site/_test/knowledge-scopes-test.js +37 -37
  66. package/_site/_test/knowledge-storage-location-test.js +28 -28
  67. package/_site/_test/launcher-readiness-test.js +80 -80
  68. package/_site/_test/legacy-claude-session-alias-test.js +106 -106
  69. package/_site/_test/legacy-routes-removed-test.js +47 -47
  70. package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
  71. package/_site/_test/live-gateway-test.js +48 -48
  72. package/_site/_test/local-marketplace-test.js +16 -16
  73. package/_site/_test/log-redaction-test.js +58 -58
  74. package/_site/_test/markdown-maintenance-test.js +11 -11
  75. package/_site/_test/model-context-windows-test.js +89 -89
  76. package/_site/_test/module-agent-start-proxy-test.js +65 -65
  77. package/_site/_test/module-artifact-integrity-test.js +27 -27
  78. package/_site/_test/module-boundary-test.js +30 -30
  79. package/_site/_test/module-bridge-eventbridge-test.js +202 -202
  80. package/_site/_test/module-dynamic-port-test.js +118 -118
  81. package/_site/_test/module-index-proxy-test.js +33 -33
  82. package/_site/_test/module-model-configuration-test.js +44 -44
  83. package/_site/_test/module-process-lifecycle-test.js +35 -35
  84. package/_site/_test/module-stream-proxy-test.js +34 -34
  85. package/_site/_test/module-terminal-restart-test.js +63 -63
  86. package/_site/_test/module-wiring-unbind-race-test.js +59 -59
  87. package/_site/_test/native-context-integrations-test.js +86 -86
  88. package/_site/_test/orphaned-claim-recovery-test.js +80 -80
  89. package/_site/_test/package-boundary-test.js +43 -42
  90. package/_site/_test/package-startup-test.js +56 -56
  91. package/_site/_test/packaged-ui-smoke-test.js +88 -88
  92. package/_site/_test/pr-consumer-contract-test.js +55 -55
  93. package/_site/_test/product-brand-contract-test.js +25 -25
  94. package/_site/_test/product-diagnostics-ui-test.js +93 -93
  95. package/_site/_test/product-import-ui-test.js +104 -104
  96. package/_site/_test/product-progress-status-test.js +12 -1
  97. package/_site/_test/project-remove-running-guard-test.js +79 -79
  98. package/_site/_test/project-store-test.js +90 -90
  99. package/_site/_test/registry-events-test.js +38 -38
  100. package/_site/_test/release-version-sync-test.js +115 -115
  101. package/_site/_test/requirement-binding-test.js +113 -113
  102. package/_site/_test/runtime-endpoint-test.js +64 -64
  103. package/_site/_test/scanner-test.js +83 -83
  104. package/_site/_test/server-security-test.js +96 -96
  105. package/_site/_test/sessions-stream-test.js +93 -93
  106. package/_site/_test/shared-scope-root-binding-test.js +67 -67
  107. package/_site/_test/simple-import-test.js +98 -98
  108. package/_site/_test/startup-analysis-disabled-test.js +6 -6
  109. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
  110. package/_site/_test/team-knowledge-store-test.js +132 -132
  111. package/_site/_test/test-server-cleanup-test.js +35 -35
  112. package/_site/_test/test-server-module-isolation-test.js +37 -37
  113. package/_site/_test/tracking-start-test.js +35 -35
  114. package/_site/_test/workbench-context-receipts-test.js +22 -22
  115. package/_site/_test/workspace-ui-contract-test.js +31 -31
  116. package/_site/_test/zcode-capture-integration-test.js +108 -108
  117. package/_site/lib/ai-vendor-presets.js +83 -83
  118. package/_site/lib/ai-workspace.js +3 -3
  119. package/_site/lib/atomic-file.js +204 -204
  120. package/_site/lib/bridge-adapter.js +260 -260
  121. package/_site/lib/capture-hook-config.js +108 -108
  122. package/_site/lib/claude-cli-runner.js +89 -89
  123. package/_site/lib/cli-instances.js +43 -43
  124. package/_site/lib/codex-context-status.js +17 -17
  125. package/_site/lib/codex-notify-config.js +193 -193
  126. package/_site/lib/commit-conversation-binder.js +12 -12
  127. package/_site/lib/commit-reconciler.js +38 -38
  128. package/_site/lib/context-pack-builder.js +299 -299
  129. package/_site/lib/conversation-exclusions.js +204 -204
  130. package/_site/lib/conversation-query-service.js +17 -17
  131. package/_site/lib/data-dir.js +13 -13
  132. package/_site/lib/embedding-service.js +85 -85
  133. package/_site/lib/engine-connection.js +93 -93
  134. package/_site/lib/engine-knowledge-client.js +72 -72
  135. package/_site/lib/folder-picker-output.js +3 -3
  136. package/_site/lib/git-path.js +14 -14
  137. package/_site/lib/github-team-store.js +3 -3
  138. package/_site/lib/index-builder.js +157 -157
  139. package/_site/lib/integration-status-scan.js +99 -99
  140. package/_site/lib/integration-status-worker.js +14 -14
  141. package/_site/lib/kb-framework.js +15 -15
  142. package/_site/lib/knowledge-db.js +363 -363
  143. package/_site/lib/knowledge-schema.js +121 -121
  144. package/_site/lib/live-gateway.js +78 -78
  145. package/_site/lib/llm-client.js +12 -12
  146. package/_site/lib/model-context-windows.js +82 -82
  147. package/_site/lib/module-bridge.js +874 -874
  148. package/_site/lib/module-port-resolver.js +57 -57
  149. package/_site/lib/post-commit-automation.js +54 -54
  150. package/_site/lib/product-data.js +35 -35
  151. package/_site/lib/project-lifecycle-service.js +14 -14
  152. package/_site/lib/project-registry-store.js +129 -129
  153. package/_site/lib/prompt-registry.js +148 -148
  154. package/_site/lib/registry-events.js +16 -16
  155. package/_site/lib/requirement-adapters.js +10 -10
  156. package/_site/lib/requirement-binder.js +112 -112
  157. package/_site/lib/runtime-control.js +89 -89
  158. package/_site/lib/runtime-endpoint.js +5 -5
  159. package/_site/lib/zcode-context-integration.js +60 -60
  160. package/_site/scripts/codex-notify-fanout.cjs +95 -95
  161. package/_site/scripts/install-module-candidates.js +69 -69
  162. package/_site/scripts/pack-module-candidates.js +89 -89
  163. package/_site/scripts/sync-product-content.js +28 -28
  164. package/_site/scripts/sync-release-version.js +164 -164
  165. package/_site/scripts/vendor-modules.js +49 -49
  166. package/_site/scripts/verify-module-runtime.js +50 -50
  167. package/_site/scripts/verify-product-runtime.js +36 -36
  168. package/_site/scripts/verify-retrieval-gold.js +83 -83
  169. package/_site/server.js +15 -15
  170. package/_site/start.bat +26 -26
  171. package/_site/stop.bat +11 -11
  172. package/bin/project-knowledge-integrations.js +254 -254
  173. package/bin/project-knowledge-kb.js +67 -67
  174. package/bin/project-knowledge-mcp.js +8 -8
  175. package/bin/project-knowledge.js +170 -170
  176. package/claude-prompts.json +21 -21
  177. package/docs/README.zh-CN.md +205 -205
  178. package/docs/assets/architecture.svg +181 -181
  179. package/docs/assets/logo.svg +49 -49
  180. package/docs/fresh-v5-start.md +42 -42
  181. package/docs/leader-glm-workbench-playbook.md +122 -122
  182. package/docs/project-registry-schema.md +11 -11
  183. package/docs/release-5.0.16-acceptance.md +176 -176
  184. package/docs/release-5.0.17-acceptance.md +117 -117
  185. package/docs/release-5.0.19-acceptance.md +39 -25
  186. package/docs/team-knowledge-mode-a-plan.md +73 -73
  187. package/module-runtime-manifest.json +53 -33
  188. package/package.json +3 -2
  189. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  190. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  191. package/plugins/project-knowledge/.mcp.json +1 -1
  192. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  193. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  194. package/templates/change.md +30 -30
  195. package/templates/project-readme.md +18 -18
  196. package/ui/index.html +1 -1
  197. package/ui/knowledge-panel.html +118 -0
  198. package/ui/product.css +8 -0
  199. package/ui/product.js +146 -4
  200. package/vendor-manifest.json +55 -54
@@ -1,260 +1,260 @@
1
- const path = require('path');
2
- const { SCHEMAS, createId } = require('./contracts');
3
- const AtomicFile = require('./atomic-file');
4
-
5
- function loadBridgeModule(explicitPath = '') {
6
- // The bridge engine lives inside the consolidated console package
7
- // (@sanqianx/ai-coding-event-bridge-console/src/bridge) — one package for
8
- // both the capture library and the 会话浏览 service.
9
- const candidates = [explicitPath, process.env.AI_CODING_EVENT_BRIDGE_MODULE, '@sanqianx/ai-coding-event-bridge-console/src/bridge'].filter(Boolean);
10
- for (const candidate of candidates) {
11
- try {
12
- const target = candidate.startsWith('.') || path.isAbsolute(candidate) ? path.resolve(candidate) : candidate;
13
- return { module: require(target), moduleId: candidate, error: null };
14
- } catch (error) {
15
- if (candidate !== candidates[candidates.length - 1]) continue;
16
- return { module: null, moduleId: candidate, error };
17
- }
18
- }
19
- return { module: null, moduleId: '', error: null };
20
- }
21
-
22
- function bridgeImplementation(loaded, options = {}) {
23
- if (!loaded) return null;
24
- if (typeof loaded.createBridge === 'function') return loaded.createBridge(options);
25
- if (loaded.default && typeof loaded.default.createBridge === 'function') return loaded.default.createBridge(options);
26
- return loaded.default || loaded;
27
- }
28
-
29
- // repo-identity/v1 objects are the only new automatic identity shape. Legacy
30
- // plain strings are tolerated for stored records; the old PK-only
31
- // { commonDir } object shape must never be sent to the Bridge again — it is
32
- // answered with a deterministic gap instead of a guess.
33
- function isLegacyStringIdentity(identity) {
34
- return typeof identity === 'string' && identity.length > 0;
35
- }
36
-
37
- // Local structural check: works even when the injected bridge module is a
38
- // narrow test fixture without the full export surface.
39
- function isV1Identity(identity) {
40
- return Boolean(
41
- identity &&
42
- typeof identity === 'object' &&
43
- identity.schema === 'repo-identity/v1' &&
44
- typeof identity.workspaceId === 'string' &&
45
- /^sha256:[0-9a-f]{64}$/.test(identity.workspaceId) &&
46
- typeof identity.workspaceRoot === 'string' &&
47
- identity.workspaceRoot.length > 0
48
- );
49
- }
50
-
51
- class BridgeAdapter {
52
- constructor(options = {}) {
53
- this.dataDir = path.resolve(options.dataDir || process.env.KB_DATA_DIR || '.');
54
- this.atomic = options.atomic || AtomicFile;
55
- const loaded = options.bridgeModule
56
- ? { module: options.bridgeModule, moduleId: 'injected', error: null }
57
- : loadBridgeModule(options.modulePath || '');
58
- this.moduleId = loaded.moduleId;
59
- this.loadError = loaded.error;
60
- this.module = loaded.module;
61
- // The Bridge journal is a global per-user spool (~/.ai-coding-event-bridge
62
- // by default) — never a directory inside PK's project data.
63
- const bridgeOptions = {};
64
- const homeDir = options.bridgeHomeDir || process.env.AI_CODING_EVENT_BRIDGE_HOME;
65
- if (homeDir) bridgeOptions.homeDir = homeDir;
66
- this.bridge = bridgeImplementation(loaded.module, bridgeOptions);
67
- }
68
-
69
- isAvailable() {
70
- return Boolean(this.bridge && typeof this.bridge.appendCommitBoundary === 'function');
71
- }
72
- hookBinding() { return { dataDir: this.dataDir, bridgeHomeDir: this.bridge?.homeDir || process.env.AI_CODING_EVENT_BRIDGE_HOME, bridgeModulePath: this.moduleId === 'injected' ? undefined : this.moduleId }; }
73
-
74
- // Canonical RepoIdentityV1 resolution delegated to the Bridge package so PK
75
- // never invents a second identity algorithm.
76
- async resolveRepoIdentity(cwd) {
77
- if (!this.module || typeof this.module.resolveRepoContext !== 'function') {
78
- return { status: 'unavailable', repoIdentity: null, reason: 'bridge-module-unavailable' };
79
- }
80
- try {
81
- const context = await this.module.resolveRepoContext(cwd);
82
- return { status: 'ok', repoIdentity: context.repoIdentity, projectPath: context.projectPath };
83
- } catch (error) {
84
- return { status: 'unavailable', repoIdentity: null, reason: 'repo-context-failed', errorCode: (error && error.code) || '' };
85
- }
86
- }
87
-
88
- async getHighWatermark(context = {}) {
89
- if (!this.bridge || typeof this.bridge.getHighWatermark !== 'function') {
90
- return { status: 'unavailable', cursor: null, reason: 'bridge-high-watermark-unavailable' };
91
- }
92
- try {
93
- const result = await this.bridge.getHighWatermark(context);
94
- const cursor = Number.isInteger(result) ? result : Number(result && (result.cursor ?? result.highWatermark));
95
- if (!Number.isInteger(cursor) || cursor < 0) return { status: 'unavailable', cursor: null, reason: 'bridge-high-watermark-invalid' };
96
- return { status: 'captured', cursor };
97
- } catch (error) {
98
- return { status: 'unavailable', cursor: null, reason: 'bridge-high-watermark-failed', errorCode: error && error.code || '' };
99
- }
100
- }
101
-
102
- async readEvents(options = {}) {
103
- if (!this.bridge || typeof this.bridge.readEvents !== 'function') {
104
- return { status: 'unavailable', events: null, reason: 'bridge-read-unavailable' };
105
- }
106
- try {
107
- const events = await this.bridge.readEvents(options);
108
- return { status: 'ok', events };
109
- } catch (error) {
110
- return { status: 'unavailable', events: null, reason: 'bridge-read-failed', errorCode: (error && error.code) || '' };
111
- }
112
- }
113
-
114
- async getHealth() {
115
- if (!this.bridge || typeof this.bridge.getHealth !== 'function') {
116
- return { status: 'unavailable', health: null, reason: 'bridge-health-unavailable' };
117
- }
118
- try {
119
- const health = await this.bridge.getHealth();
120
- return { status: 'ok', health };
121
- } catch (error) {
122
- return { status: 'unavailable', health: null, reason: 'bridge-health-failed', errorCode: (error && error.code) || '' };
123
- }
124
- }
125
-
126
- async registerConsumer(name, meta = {}) {
127
- if (!this.bridge || typeof this.bridge.registerConsumer !== 'function') {
128
- return { status: 'unavailable', reason: 'bridge-consumer-unavailable' };
129
- }
130
- try {
131
- const consumer = await this.bridge.registerConsumer(name, meta);
132
- return { status: 'ok', consumer };
133
- } catch (error) {
134
- return { status: 'unavailable', reason: 'bridge-consumer-failed', errorCode: (error && error.code) || '' };
135
- }
136
- }
137
-
138
- async getConsumer(name) {
139
- if (!this.bridge || typeof this.bridge.getConsumer !== 'function') {
140
- return { status: 'unavailable', consumer: null, reason: 'bridge-consumer-unavailable' };
141
- }
142
- try {
143
- const consumer = await this.bridge.getConsumer(name);
144
- return { status: 'ok', consumer };
145
- } catch (error) {
146
- return { status: 'unavailable', consumer: null, reason: 'bridge-consumer-failed', errorCode: (error && error.code) || '' };
147
- }
148
- }
149
-
150
- async listConsumers() {
151
- if (!this.bridge || typeof this.bridge.listConsumers !== 'function') {
152
- return { status: 'unavailable', consumers: null, reason: 'bridge-consumer-unavailable' };
153
- }
154
- try {
155
- const consumers = await this.bridge.listConsumers();
156
- return { status: 'ok', consumers };
157
- } catch (error) {
158
- return { status: 'unavailable', consumers: null, reason: 'bridge-consumer-failed', errorCode: (error && error.code) || '' };
159
- }
160
- }
161
-
162
- async ackConsumerCursor(name, sequence) {
163
- if (!this.bridge || typeof this.bridge.ackConsumerCursor !== 'function') {
164
- return { status: 'unavailable', reason: 'bridge-ack-unavailable' };
165
- }
166
- try {
167
- const result = await this.bridge.ackConsumerCursor(name, sequence);
168
- return { status: 'ok', ...result };
169
- } catch (error) {
170
- return { status: 'unavailable', reason: 'bridge-ack-failed', errorCode: (error && error.code) || '' };
171
- }
172
- }
173
-
174
- async compact(options = {}) {
175
- if (!this.bridge || typeof this.bridge.compact !== 'function') {
176
- return { status: 'unavailable', reason: 'bridge-compact-unavailable' };
177
- }
178
- try {
179
- const result = await this.bridge.compact(options);
180
- return { status: 'ok', ...result };
181
- } catch (error) {
182
- return { status: 'unavailable', reason: 'bridge-compact-failed', errorCode: (error && error.code) || '' };
183
- }
184
- }
185
-
186
- async recordGap(input, reason, error = null) {
187
- const gap = {
188
- schema: 'conversation-capture-gap/v1',
189
- gapId: createId('gap'),
190
- projectId: input.projectId,
191
- repoIdentity: input.repoIdentity || null,
192
- commitSha: input.commitSha || null,
193
- operationId: input.operationId || '',
194
- reason,
195
- errorCode: error && error.code || '',
196
- capturedAt: new Date().toISOString(),
197
- };
198
- const filePath = path.join(this.dataDir, 'runtime', 'conversation-capture-gaps.jsonl');
199
- try { await this.atomic.appendJsonlLocked(filePath, gap, { lockPath: `${filePath}.lock` }); }
200
- catch {
201
- // A gap that cannot be persisted remains visible through the Hook logger fallback.
202
- }
203
- return gap;
204
- }
205
-
206
- async appendCommitBoundary(input = {}) {
207
- if (!this.isAvailable()) {
208
- const gap = await this.recordGap(input, 'bridge-unavailable', this.loadError);
209
- return { status: 'unavailable', gap };
210
- }
211
- const identity = input.repoIdentity || null;
212
- if (identity === null) {
213
- const gap = await this.recordGap(input, 'repo-identity-missing');
214
- return { status: 'unavailable', gap };
215
- }
216
- const identityValid = isV1Identity(identity) || isLegacyStringIdentity(identity);
217
- if (!identityValid) {
218
- // Never send the old { commonDir } shape or malformed objects upstream;
219
- // record a deterministic gap instead of guessing a workspace.
220
- const gap = await this.recordGap(input, 'repo-identity-legacy-shape');
221
- return { status: 'unavailable', gap };
222
- }
223
- try {
224
- // This single Bridge API owns cursor read, sequence allocation, append and fsync.
225
- const result = await this.bridge.appendCommitBoundary({
226
- schema: SCHEMAS.gitCommitBoundary,
227
- projectId: input.projectId,
228
- repoIdentity: identity,
229
- commitSha: input.commitSha,
230
- parentShas: input.parentShas || [],
231
- branch: input.branch || null,
232
- committedAt: input.committedAt || new Date().toISOString(),
233
- operationId: input.operationId || '',
234
- });
235
- const cursor = Number(result && (result.bridgeCursorAtCommit ?? result.cursor ?? result.sequence));
236
- if (!Number.isInteger(cursor) || cursor < 0) throw Object.assign(new Error('Bridge returned an invalid commit boundary cursor.'), { code: 'BRIDGE_BOUNDARY_INVALID' });
237
- const boundary = {
238
- schema: SCHEMAS.gitCommitBoundary,
239
- projectId: input.projectId,
240
- repoIdentity: identity,
241
- commitSha: String(input.commitSha || '').toLowerCase(),
242
- parentShas: (input.parentShas || []).map(value => String(value).toLowerCase()),
243
- branch: input.branch || null,
244
- committedAt: result.committedAt || input.committedAt || new Date().toISOString(),
245
- bridgeCursorAtCommit: cursor,
246
- openTurnIdsAtCommit: Array.isArray(result.openTurnIdsAtCommit)
247
- ? result.openTurnIdsAtCommit.map(String)
248
- : Array.isArray(result.openTurnIds) ? result.openTurnIds.map(String) : [],
249
- operationId: input.operationId || '',
250
- journalSequence: Number.isInteger(result.sequence) ? result.sequence : cursor,
251
- };
252
- return { status: 'captured', boundary };
253
- } catch (error) {
254
- const gap = await this.recordGap(input, 'bridge-boundary-append-failed', error);
255
- return { status: 'unavailable', gap };
256
- }
257
- }
258
- }
259
-
260
- module.exports = { BridgeAdapter, loadBridgeModule };
1
+ const path = require('path');
2
+ const { SCHEMAS, createId } = require('./contracts');
3
+ const AtomicFile = require('./atomic-file');
4
+
5
+ function loadBridgeModule(explicitPath = '') {
6
+ // The bridge engine lives inside the consolidated console package
7
+ // (@sanqianx/ai-coding-event-bridge-console/src/bridge) — one package for
8
+ // both the capture library and the 会话浏览 service.
9
+ const candidates = [explicitPath, process.env.AI_CODING_EVENT_BRIDGE_MODULE, '@sanqianx/ai-coding-event-bridge-console/src/bridge'].filter(Boolean);
10
+ for (const candidate of candidates) {
11
+ try {
12
+ const target = candidate.startsWith('.') || path.isAbsolute(candidate) ? path.resolve(candidate) : candidate;
13
+ return { module: require(target), moduleId: candidate, error: null };
14
+ } catch (error) {
15
+ if (candidate !== candidates[candidates.length - 1]) continue;
16
+ return { module: null, moduleId: candidate, error };
17
+ }
18
+ }
19
+ return { module: null, moduleId: '', error: null };
20
+ }
21
+
22
+ function bridgeImplementation(loaded, options = {}) {
23
+ if (!loaded) return null;
24
+ if (typeof loaded.createBridge === 'function') return loaded.createBridge(options);
25
+ if (loaded.default && typeof loaded.default.createBridge === 'function') return loaded.default.createBridge(options);
26
+ return loaded.default || loaded;
27
+ }
28
+
29
+ // repo-identity/v1 objects are the only new automatic identity shape. Legacy
30
+ // plain strings are tolerated for stored records; the old PK-only
31
+ // { commonDir } object shape must never be sent to the Bridge again — it is
32
+ // answered with a deterministic gap instead of a guess.
33
+ function isLegacyStringIdentity(identity) {
34
+ return typeof identity === 'string' && identity.length > 0;
35
+ }
36
+
37
+ // Local structural check: works even when the injected bridge module is a
38
+ // narrow test fixture without the full export surface.
39
+ function isV1Identity(identity) {
40
+ return Boolean(
41
+ identity &&
42
+ typeof identity === 'object' &&
43
+ identity.schema === 'repo-identity/v1' &&
44
+ typeof identity.workspaceId === 'string' &&
45
+ /^sha256:[0-9a-f]{64}$/.test(identity.workspaceId) &&
46
+ typeof identity.workspaceRoot === 'string' &&
47
+ identity.workspaceRoot.length > 0
48
+ );
49
+ }
50
+
51
+ class BridgeAdapter {
52
+ constructor(options = {}) {
53
+ this.dataDir = path.resolve(options.dataDir || process.env.KB_DATA_DIR || '.');
54
+ this.atomic = options.atomic || AtomicFile;
55
+ const loaded = options.bridgeModule
56
+ ? { module: options.bridgeModule, moduleId: 'injected', error: null }
57
+ : loadBridgeModule(options.modulePath || '');
58
+ this.moduleId = loaded.moduleId;
59
+ this.loadError = loaded.error;
60
+ this.module = loaded.module;
61
+ // The Bridge journal is a global per-user spool (~/.ai-coding-event-bridge
62
+ // by default) — never a directory inside PK's project data.
63
+ const bridgeOptions = {};
64
+ const homeDir = options.bridgeHomeDir || process.env.AI_CODING_EVENT_BRIDGE_HOME;
65
+ if (homeDir) bridgeOptions.homeDir = homeDir;
66
+ this.bridge = bridgeImplementation(loaded.module, bridgeOptions);
67
+ }
68
+
69
+ isAvailable() {
70
+ return Boolean(this.bridge && typeof this.bridge.appendCommitBoundary === 'function');
71
+ }
72
+ hookBinding() { return { dataDir: this.dataDir, bridgeHomeDir: this.bridge?.homeDir || process.env.AI_CODING_EVENT_BRIDGE_HOME, bridgeModulePath: this.moduleId === 'injected' ? undefined : this.moduleId }; }
73
+
74
+ // Canonical RepoIdentityV1 resolution delegated to the Bridge package so PK
75
+ // never invents a second identity algorithm.
76
+ async resolveRepoIdentity(cwd) {
77
+ if (!this.module || typeof this.module.resolveRepoContext !== 'function') {
78
+ return { status: 'unavailable', repoIdentity: null, reason: 'bridge-module-unavailable' };
79
+ }
80
+ try {
81
+ const context = await this.module.resolveRepoContext(cwd);
82
+ return { status: 'ok', repoIdentity: context.repoIdentity, projectPath: context.projectPath };
83
+ } catch (error) {
84
+ return { status: 'unavailable', repoIdentity: null, reason: 'repo-context-failed', errorCode: (error && error.code) || '' };
85
+ }
86
+ }
87
+
88
+ async getHighWatermark(context = {}) {
89
+ if (!this.bridge || typeof this.bridge.getHighWatermark !== 'function') {
90
+ return { status: 'unavailable', cursor: null, reason: 'bridge-high-watermark-unavailable' };
91
+ }
92
+ try {
93
+ const result = await this.bridge.getHighWatermark(context);
94
+ const cursor = Number.isInteger(result) ? result : Number(result && (result.cursor ?? result.highWatermark));
95
+ if (!Number.isInteger(cursor) || cursor < 0) return { status: 'unavailable', cursor: null, reason: 'bridge-high-watermark-invalid' };
96
+ return { status: 'captured', cursor };
97
+ } catch (error) {
98
+ return { status: 'unavailable', cursor: null, reason: 'bridge-high-watermark-failed', errorCode: error && error.code || '' };
99
+ }
100
+ }
101
+
102
+ async readEvents(options = {}) {
103
+ if (!this.bridge || typeof this.bridge.readEvents !== 'function') {
104
+ return { status: 'unavailable', events: null, reason: 'bridge-read-unavailable' };
105
+ }
106
+ try {
107
+ const events = await this.bridge.readEvents(options);
108
+ return { status: 'ok', events };
109
+ } catch (error) {
110
+ return { status: 'unavailable', events: null, reason: 'bridge-read-failed', errorCode: (error && error.code) || '' };
111
+ }
112
+ }
113
+
114
+ async getHealth() {
115
+ if (!this.bridge || typeof this.bridge.getHealth !== 'function') {
116
+ return { status: 'unavailable', health: null, reason: 'bridge-health-unavailable' };
117
+ }
118
+ try {
119
+ const health = await this.bridge.getHealth();
120
+ return { status: 'ok', health };
121
+ } catch (error) {
122
+ return { status: 'unavailable', health: null, reason: 'bridge-health-failed', errorCode: (error && error.code) || '' };
123
+ }
124
+ }
125
+
126
+ async registerConsumer(name, meta = {}) {
127
+ if (!this.bridge || typeof this.bridge.registerConsumer !== 'function') {
128
+ return { status: 'unavailable', reason: 'bridge-consumer-unavailable' };
129
+ }
130
+ try {
131
+ const consumer = await this.bridge.registerConsumer(name, meta);
132
+ return { status: 'ok', consumer };
133
+ } catch (error) {
134
+ return { status: 'unavailable', reason: 'bridge-consumer-failed', errorCode: (error && error.code) || '' };
135
+ }
136
+ }
137
+
138
+ async getConsumer(name) {
139
+ if (!this.bridge || typeof this.bridge.getConsumer !== 'function') {
140
+ return { status: 'unavailable', consumer: null, reason: 'bridge-consumer-unavailable' };
141
+ }
142
+ try {
143
+ const consumer = await this.bridge.getConsumer(name);
144
+ return { status: 'ok', consumer };
145
+ } catch (error) {
146
+ return { status: 'unavailable', consumer: null, reason: 'bridge-consumer-failed', errorCode: (error && error.code) || '' };
147
+ }
148
+ }
149
+
150
+ async listConsumers() {
151
+ if (!this.bridge || typeof this.bridge.listConsumers !== 'function') {
152
+ return { status: 'unavailable', consumers: null, reason: 'bridge-consumer-unavailable' };
153
+ }
154
+ try {
155
+ const consumers = await this.bridge.listConsumers();
156
+ return { status: 'ok', consumers };
157
+ } catch (error) {
158
+ return { status: 'unavailable', consumers: null, reason: 'bridge-consumer-failed', errorCode: (error && error.code) || '' };
159
+ }
160
+ }
161
+
162
+ async ackConsumerCursor(name, sequence) {
163
+ if (!this.bridge || typeof this.bridge.ackConsumerCursor !== 'function') {
164
+ return { status: 'unavailable', reason: 'bridge-ack-unavailable' };
165
+ }
166
+ try {
167
+ const result = await this.bridge.ackConsumerCursor(name, sequence);
168
+ return { status: 'ok', ...result };
169
+ } catch (error) {
170
+ return { status: 'unavailable', reason: 'bridge-ack-failed', errorCode: (error && error.code) || '' };
171
+ }
172
+ }
173
+
174
+ async compact(options = {}) {
175
+ if (!this.bridge || typeof this.bridge.compact !== 'function') {
176
+ return { status: 'unavailable', reason: 'bridge-compact-unavailable' };
177
+ }
178
+ try {
179
+ const result = await this.bridge.compact(options);
180
+ return { status: 'ok', ...result };
181
+ } catch (error) {
182
+ return { status: 'unavailable', reason: 'bridge-compact-failed', errorCode: (error && error.code) || '' };
183
+ }
184
+ }
185
+
186
+ async recordGap(input, reason, error = null) {
187
+ const gap = {
188
+ schema: 'conversation-capture-gap/v1',
189
+ gapId: createId('gap'),
190
+ projectId: input.projectId,
191
+ repoIdentity: input.repoIdentity || null,
192
+ commitSha: input.commitSha || null,
193
+ operationId: input.operationId || '',
194
+ reason,
195
+ errorCode: error && error.code || '',
196
+ capturedAt: new Date().toISOString(),
197
+ };
198
+ const filePath = path.join(this.dataDir, 'runtime', 'conversation-capture-gaps.jsonl');
199
+ try { await this.atomic.appendJsonlLocked(filePath, gap, { lockPath: `${filePath}.lock` }); }
200
+ catch {
201
+ // A gap that cannot be persisted remains visible through the Hook logger fallback.
202
+ }
203
+ return gap;
204
+ }
205
+
206
+ async appendCommitBoundary(input = {}) {
207
+ if (!this.isAvailable()) {
208
+ const gap = await this.recordGap(input, 'bridge-unavailable', this.loadError);
209
+ return { status: 'unavailable', gap };
210
+ }
211
+ const identity = input.repoIdentity || null;
212
+ if (identity === null) {
213
+ const gap = await this.recordGap(input, 'repo-identity-missing');
214
+ return { status: 'unavailable', gap };
215
+ }
216
+ const identityValid = isV1Identity(identity) || isLegacyStringIdentity(identity);
217
+ if (!identityValid) {
218
+ // Never send the old { commonDir } shape or malformed objects upstream;
219
+ // record a deterministic gap instead of guessing a workspace.
220
+ const gap = await this.recordGap(input, 'repo-identity-legacy-shape');
221
+ return { status: 'unavailable', gap };
222
+ }
223
+ try {
224
+ // This single Bridge API owns cursor read, sequence allocation, append and fsync.
225
+ const result = await this.bridge.appendCommitBoundary({
226
+ schema: SCHEMAS.gitCommitBoundary,
227
+ projectId: input.projectId,
228
+ repoIdentity: identity,
229
+ commitSha: input.commitSha,
230
+ parentShas: input.parentShas || [],
231
+ branch: input.branch || null,
232
+ committedAt: input.committedAt || new Date().toISOString(),
233
+ operationId: input.operationId || '',
234
+ });
235
+ const cursor = Number(result && (result.bridgeCursorAtCommit ?? result.cursor ?? result.sequence));
236
+ if (!Number.isInteger(cursor) || cursor < 0) throw Object.assign(new Error('Bridge returned an invalid commit boundary cursor.'), { code: 'BRIDGE_BOUNDARY_INVALID' });
237
+ const boundary = {
238
+ schema: SCHEMAS.gitCommitBoundary,
239
+ projectId: input.projectId,
240
+ repoIdentity: identity,
241
+ commitSha: String(input.commitSha || '').toLowerCase(),
242
+ parentShas: (input.parentShas || []).map(value => String(value).toLowerCase()),
243
+ branch: input.branch || null,
244
+ committedAt: result.committedAt || input.committedAt || new Date().toISOString(),
245
+ bridgeCursorAtCommit: cursor,
246
+ openTurnIdsAtCommit: Array.isArray(result.openTurnIdsAtCommit)
247
+ ? result.openTurnIdsAtCommit.map(String)
248
+ : Array.isArray(result.openTurnIds) ? result.openTurnIds.map(String) : [],
249
+ operationId: input.operationId || '',
250
+ journalSequence: Number.isInteger(result.sequence) ? result.sequence : cursor,
251
+ };
252
+ return { status: 'captured', boundary };
253
+ } catch (error) {
254
+ const gap = await this.recordGap(input, 'bridge-boundary-append-failed', error);
255
+ return { status: 'unavailable', gap };
256
+ }
257
+ }
258
+ }
259
+
260
+ module.exports = { BridgeAdapter, loadBridgeModule };