@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.
- package/.agents/plugins/marketplace.json +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/README.md +4 -0
- package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
- package/_modules/knowledge-engine/src/coordinator.js +7 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/server.js +1 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -35
- package/_site/_test/candidate-product-assets-test.js +13 -13
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/claude-executable-discovery-test.js +28 -28
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +134 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +4 -4
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +10 -10
- package/_site/_test/conversation-origin-test.js +15 -15
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/integration-adapters-test.js +67 -67
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +109 -109
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -33
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -63
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -42
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-import-ui-test.js +104 -104
- package/_site/_test/product-progress-status-test.js +12 -1
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/registry-events-test.js +38 -38
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-scope-root-binding-test.js +67 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +6 -6
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +35 -35
- package/_site/_test/workbench-context-receipts-test.js +22 -22
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/_site/_test/zcode-capture-integration-test.js +108 -108
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +3 -3
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +89 -89
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +12 -12
- package/_site/lib/commit-reconciler.js +38 -38
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +17 -17
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -93
- package/_site/lib/engine-knowledge-client.js +72 -72
- package/_site/lib/folder-picker-output.js +3 -3
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/github-team-store.js +3 -3
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +15 -15
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/live-gateway.js +78 -78
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -874
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +54 -54
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +14 -14
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/registry-events.js +16 -16
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +5 -5
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -89
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -83
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +8 -8
- package/bin/project-knowledge.js +170 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +205 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +42 -42
- package/docs/leader-glm-workbench-playbook.md +122 -122
- package/docs/project-registry-schema.md +11 -11
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -25
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +53 -33
- package/package.json +3 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/change.md +30 -30
- package/templates/project-readme.md +18 -18
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +118 -0
- package/ui/product.css +8 -0
- package/ui/product.js +146 -4
- 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 };
|