@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,127 +1,127 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { StorageLayout } = require('../lib/storage-layout');
6
- const { MigrationService } = require('../lib/migration-service');
7
-
8
- function fixture() {
9
- const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-migration-'));
10
- const oldRoot = path.join(dataDir, 'old-knowledge');
11
- const differentRoot = path.join(dataDir, 'different-current-root');
12
- fs.mkdirSync(oldRoot, { recursive: true });
13
- const exactKbPath = path.join(oldRoot, 'legacy-a-fixed');
14
- fs.mkdirSync(exactKbPath, { recursive: true });
15
- fs.writeFileSync(path.join(exactKbPath, 'README.md'), '# existing knowledge\n', 'utf8');
16
- fs.writeFileSync(path.join(dataDir, 'projects.json'), JSON.stringify({
17
- legacyA: {
18
- displayName: 'Legacy A',
19
- localPath: path.join(dataDir, 'repo-a'),
20
- gitPath: path.join(dataDir, 'repo-a'),
21
- kbPath: exactKbPath,
22
- trackingStartCommit: 'track-a',
23
- lastAnalyzedCommit: 'last-a',
24
- enabled: true,
25
- aiProfileId: 'primary',
26
- },
27
- legacyB: {
28
- displayName: 'Legacy B',
29
- localPath: path.join(dataDir, 'repo-b'),
30
- trackingStartCommit: 'track-b',
31
- enabled: true,
32
- },
33
- }, null, 2));
34
- fs.writeFileSync(path.join(dataDir, 'knowledge-store.json'), JSON.stringify({ rootPath: differentRoot, configured: true }));
35
- fs.writeFileSync(path.join(dataDir, 'ai-profiles.json'), JSON.stringify({ schema: 'ai-profiles/v1', profiles: [{ id: 'primary', apiKey: 'exact-secret-value' }] }));
36
- fs.writeFileSync(path.join(dataDir, 'embedding-config.json'), JSON.stringify({ modelId: 'model-a' }));
37
- fs.writeFileSync(path.join(dataDir, 'logging.json'), JSON.stringify({ levels: ['debug', 'info', 'error'], retentionDays: 0, maxTotalSizeMB: 512 }));
38
- fs.writeFileSync(path.join(dataDir, 'claude-prompts.json'), JSON.stringify({ hookPromptTemplate: 'legacy override' }));
39
- fs.writeFileSync(path.join(dataDir, '.hook-trigger-errors.log'), '2026-01-01 old hook error\n');
40
- return { dataDir, exactKbPath, differentRoot };
41
- }
42
-
43
- (async () => {
44
- {
45
- const { dataDir, exactKbPath, differentRoot } = fixture();
46
- const layout = new StorageLayout({ dataDir });
47
- const migration = new MigrationService({ layout, legacyDataDir: dataDir });
48
- const result = await migration.migrateIfNeeded({ migrationRunId: 'layout-v2-success' });
49
- assert.strictEqual(result.ok, true);
50
- assert.strictEqual(result.completed, true);
51
- assert(fs.existsSync(layout.getMigrationCompletionPath()));
52
- const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
53
- assert.strictEqual(registry.schema, 'project-registry/v2');
54
- assert.strictEqual(registry.projectOrder.length, 2);
55
- const idA = result.projectMap.legacyA;
56
- const idB = result.projectMap.legacyB;
57
- const configA = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idA), 'utf8'));
58
- const configB = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idB), 'utf8'));
59
- const stateA = JSON.parse(fs.readFileSync(layout.getProjectStatePath(idA), 'utf8'));
60
- assert.strictEqual(configA.knowledgePath, exactKbPath, 'old kbPath must remain exact');
61
- assert.strictEqual(configB.knowledgePath, path.join(differentRoot, 'legacyB'));
62
- assert.strictEqual(stateA.trackingStartCommit, 'track-a');
63
- assert.strictEqual(stateA.lastAnalyzedCommit, 'last-a');
64
- const settings = JSON.parse(fs.readFileSync(layout.getSettingsPath(), 'utf8'));
65
- assert.strictEqual(settings.ai.profiles[0].apiKey, 'exact-secret-value');
66
- assert.strictEqual(Object.prototype.hasOwnProperty.call(settings.logging, 'retentionDays'), false);
67
- assert.strictEqual(settings.legacyExtensions.logging.retentionDays, 0, 'legacy logging settings remain preserved as read-only migration evidence');
68
- assert(fs.existsSync(path.join(result.recoveryDir, 'backup', 'projects.json')));
69
- assert(fs.existsSync(path.join(dataDir, '.hook-trigger-errors.log')), 'legacy hook log must remain');
70
- const second = await migration.migrateIfNeeded();
71
- assert.strictEqual(second.completed, true);
72
- assert.strictEqual(second.migrated, false);
73
- }
74
-
75
- for (const stage of ['backup', 'staging', 'validation', 'activation', 'open-verification']) {
76
- const { dataDir } = fixture();
77
- const originalProjects = fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8');
78
- const layout = new StorageLayout({ dataDir });
79
- const migration = new MigrationService({ layout, legacyDataDir: dataDir });
80
- const result = await migration.migrateIfNeeded({ migrationRunId: `fault-${stage}`, faultAt: stage });
81
- assert.strictEqual(result.ok, false, `${stage} should fail`);
82
- assert.strictEqual(result.useLegacy, true);
83
- assert.strictEqual(fs.existsSync(layout.getMigrationCompletionPath()), false, `${stage} must not write completion`);
84
- assert.strictEqual(fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8'), originalProjects, `${stage} must preserve/restore legacy registry`);
85
- const journal = JSON.parse(fs.readFileSync(path.join(result.recoveryDir, 'journal.json'), 'utf8'));
86
- assert.strictEqual(journal.phase, 'failed');
87
- assert.strictEqual(journal.rollback.ok, true, `${stage} rollback must complete`);
88
- assert.strictEqual(fs.existsSync(layout.getSettingsPath()), false, `${stage} must remove newly activated settings`);
89
- assert.strictEqual(fs.existsSync(path.join(dataDir, 'projects')), false, `${stage} must remove newly activated project metadata`);
90
- if (['activation', 'open-verification'].includes(stage)) {
91
- const retry = await migration.migrateIfNeeded({ migrationRunId: `retry-${stage}` });
92
- assert.strictEqual(retry.ok, true, `${stage} must be safely retryable after rollback`);
93
- assert.strictEqual(retry.completed, true);
94
- }
95
- }
96
-
97
- {
98
- const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-fresh-'));
99
- const layout = new StorageLayout({ dataDir });
100
- const result = await new MigrationService({ layout }).migrateIfNeeded();
101
- assert.strictEqual(result.reason, 'fresh-install');
102
- assert.strictEqual(fs.existsSync(layout.getRecoveryPath()), false, 'fresh install must not create optional recovery dir');
103
- }
104
-
105
- {
106
- // Regression: legacy files carrying a UTF-8 BOM (common from PowerShell
107
- // tooling) must migrate cleanly instead of failing as corrupt sources.
108
- const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-bom-'));
109
- const layout = new StorageLayout({ dataDir });
110
- const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(JSON.stringify({
111
- legacyA: { displayName: 'BOM A', localPath: path.join(dataDir, 'repo-a'), trackingStartCommit: 'track-a', enabled: true }
112
- }), 'utf8')]);
113
- fs.writeFileSync(path.join(dataDir, 'projects.json'), bom);
114
- fs.writeFileSync(path.join(dataDir, 'team-git-providers.json'), Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('{}', 'utf8')]));
115
- const result = await new MigrationService({ layout, legacyDataDir: dataDir }).migrateIfNeeded();
116
- assert.strictEqual(result.ok, true, 'BOM-prefixed legacy sources must migrate');
117
- assert.strictEqual(result.migrated, true);
118
- const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
119
- assert.strictEqual(registry.schema, 'project-registry/v2');
120
- assert.strictEqual(Object.keys(registry.projects).length, 1);
121
- }
122
-
123
- console.log('project-layout-v2-migration-test PASS');
124
- })().catch(error => {
125
- console.error(error.stack || error.message);
126
- process.exitCode = 1;
127
- });
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { StorageLayout } = require('../lib/storage-layout');
6
+ const { MigrationService } = require('../lib/migration-service');
7
+
8
+ function fixture() {
9
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-migration-'));
10
+ const oldRoot = path.join(dataDir, 'old-knowledge');
11
+ const differentRoot = path.join(dataDir, 'different-current-root');
12
+ fs.mkdirSync(oldRoot, { recursive: true });
13
+ const exactKbPath = path.join(oldRoot, 'legacy-a-fixed');
14
+ fs.mkdirSync(exactKbPath, { recursive: true });
15
+ fs.writeFileSync(path.join(exactKbPath, 'README.md'), '# existing knowledge\n', 'utf8');
16
+ fs.writeFileSync(path.join(dataDir, 'projects.json'), JSON.stringify({
17
+ legacyA: {
18
+ displayName: 'Legacy A',
19
+ localPath: path.join(dataDir, 'repo-a'),
20
+ gitPath: path.join(dataDir, 'repo-a'),
21
+ kbPath: exactKbPath,
22
+ trackingStartCommit: 'track-a',
23
+ lastAnalyzedCommit: 'last-a',
24
+ enabled: true,
25
+ aiProfileId: 'primary',
26
+ },
27
+ legacyB: {
28
+ displayName: 'Legacy B',
29
+ localPath: path.join(dataDir, 'repo-b'),
30
+ trackingStartCommit: 'track-b',
31
+ enabled: true,
32
+ },
33
+ }, null, 2));
34
+ fs.writeFileSync(path.join(dataDir, 'knowledge-store.json'), JSON.stringify({ rootPath: differentRoot, configured: true }));
35
+ fs.writeFileSync(path.join(dataDir, 'ai-profiles.json'), JSON.stringify({ schema: 'ai-profiles/v1', profiles: [{ id: 'primary', apiKey: 'exact-secret-value' }] }));
36
+ fs.writeFileSync(path.join(dataDir, 'embedding-config.json'), JSON.stringify({ modelId: 'model-a' }));
37
+ fs.writeFileSync(path.join(dataDir, 'logging.json'), JSON.stringify({ levels: ['debug', 'info', 'error'], retentionDays: 0, maxTotalSizeMB: 512 }));
38
+ fs.writeFileSync(path.join(dataDir, 'claude-prompts.json'), JSON.stringify({ hookPromptTemplate: 'legacy override' }));
39
+ fs.writeFileSync(path.join(dataDir, '.hook-trigger-errors.log'), '2026-01-01 old hook error\n');
40
+ return { dataDir, exactKbPath, differentRoot };
41
+ }
42
+
43
+ (async () => {
44
+ {
45
+ const { dataDir, exactKbPath, differentRoot } = fixture();
46
+ const layout = new StorageLayout({ dataDir });
47
+ const migration = new MigrationService({ layout, legacyDataDir: dataDir });
48
+ const result = await migration.migrateIfNeeded({ migrationRunId: 'layout-v2-success' });
49
+ assert.strictEqual(result.ok, true);
50
+ assert.strictEqual(result.completed, true);
51
+ assert(fs.existsSync(layout.getMigrationCompletionPath()));
52
+ const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
53
+ assert.strictEqual(registry.schema, 'project-registry/v2');
54
+ assert.strictEqual(registry.projectOrder.length, 2);
55
+ const idA = result.projectMap.legacyA;
56
+ const idB = result.projectMap.legacyB;
57
+ const configA = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idA), 'utf8'));
58
+ const configB = JSON.parse(fs.readFileSync(layout.getProjectConfigPath(idB), 'utf8'));
59
+ const stateA = JSON.parse(fs.readFileSync(layout.getProjectStatePath(idA), 'utf8'));
60
+ assert.strictEqual(configA.knowledgePath, exactKbPath, 'old kbPath must remain exact');
61
+ assert.strictEqual(configB.knowledgePath, path.join(differentRoot, 'legacyB'));
62
+ assert.strictEqual(stateA.trackingStartCommit, 'track-a');
63
+ assert.strictEqual(stateA.lastAnalyzedCommit, 'last-a');
64
+ const settings = JSON.parse(fs.readFileSync(layout.getSettingsPath(), 'utf8'));
65
+ assert.strictEqual(settings.ai.profiles[0].apiKey, 'exact-secret-value');
66
+ assert.strictEqual(Object.prototype.hasOwnProperty.call(settings.logging, 'retentionDays'), false);
67
+ assert.strictEqual(settings.legacyExtensions.logging.retentionDays, 0, 'legacy logging settings remain preserved as read-only migration evidence');
68
+ assert(fs.existsSync(path.join(result.recoveryDir, 'backup', 'projects.json')));
69
+ assert(fs.existsSync(path.join(dataDir, '.hook-trigger-errors.log')), 'legacy hook log must remain');
70
+ const second = await migration.migrateIfNeeded();
71
+ assert.strictEqual(second.completed, true);
72
+ assert.strictEqual(second.migrated, false);
73
+ }
74
+
75
+ for (const stage of ['backup', 'staging', 'validation', 'activation', 'open-verification']) {
76
+ const { dataDir } = fixture();
77
+ const originalProjects = fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8');
78
+ const layout = new StorageLayout({ dataDir });
79
+ const migration = new MigrationService({ layout, legacyDataDir: dataDir });
80
+ const result = await migration.migrateIfNeeded({ migrationRunId: `fault-${stage}`, faultAt: stage });
81
+ assert.strictEqual(result.ok, false, `${stage} should fail`);
82
+ assert.strictEqual(result.useLegacy, true);
83
+ assert.strictEqual(fs.existsSync(layout.getMigrationCompletionPath()), false, `${stage} must not write completion`);
84
+ assert.strictEqual(fs.readFileSync(path.join(dataDir, 'projects.json'), 'utf8'), originalProjects, `${stage} must preserve/restore legacy registry`);
85
+ const journal = JSON.parse(fs.readFileSync(path.join(result.recoveryDir, 'journal.json'), 'utf8'));
86
+ assert.strictEqual(journal.phase, 'failed');
87
+ assert.strictEqual(journal.rollback.ok, true, `${stage} rollback must complete`);
88
+ assert.strictEqual(fs.existsSync(layout.getSettingsPath()), false, `${stage} must remove newly activated settings`);
89
+ assert.strictEqual(fs.existsSync(path.join(dataDir, 'projects')), false, `${stage} must remove newly activated project metadata`);
90
+ if (['activation', 'open-verification'].includes(stage)) {
91
+ const retry = await migration.migrateIfNeeded({ migrationRunId: `retry-${stage}` });
92
+ assert.strictEqual(retry.ok, true, `${stage} must be safely retryable after rollback`);
93
+ assert.strictEqual(retry.completed, true);
94
+ }
95
+ }
96
+
97
+ {
98
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-fresh-'));
99
+ const layout = new StorageLayout({ dataDir });
100
+ const result = await new MigrationService({ layout }).migrateIfNeeded();
101
+ assert.strictEqual(result.reason, 'fresh-install');
102
+ assert.strictEqual(fs.existsSync(layout.getRecoveryPath()), false, 'fresh install must not create optional recovery dir');
103
+ }
104
+
105
+ {
106
+ // Regression: legacy files carrying a UTF-8 BOM (common from PowerShell
107
+ // tooling) must migrate cleanly instead of failing as corrupt sources.
108
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-layout-v2-bom-'));
109
+ const layout = new StorageLayout({ dataDir });
110
+ const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(JSON.stringify({
111
+ legacyA: { displayName: 'BOM A', localPath: path.join(dataDir, 'repo-a'), trackingStartCommit: 'track-a', enabled: true }
112
+ }), 'utf8')]);
113
+ fs.writeFileSync(path.join(dataDir, 'projects.json'), bom);
114
+ fs.writeFileSync(path.join(dataDir, 'team-git-providers.json'), Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('{}', 'utf8')]));
115
+ const result = await new MigrationService({ layout, legacyDataDir: dataDir }).migrateIfNeeded();
116
+ assert.strictEqual(result.ok, true, 'BOM-prefixed legacy sources must migrate');
117
+ assert.strictEqual(result.migrated, true);
118
+ const registry = JSON.parse(fs.readFileSync(layout.getProjectRegistryPath(), 'utf8'));
119
+ assert.strictEqual(registry.schema, 'project-registry/v2');
120
+ assert.strictEqual(Object.keys(registry.projects).length, 1);
121
+ }
122
+
123
+ console.log('project-layout-v2-migration-test PASS');
124
+ })().catch(error => {
125
+ console.error(error.stack || error.message);
126
+ process.exitCode = 1;
127
+ });
@@ -1,106 +1,106 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
6
- const { StorageLayout } = require('../lib/storage-layout');
7
- const { SettingsStore } = require('../lib/settings-store');
8
- const { ProjectRegistryStore } = require('../lib/project-registry-store');
9
- const { ProjectStore } = require('../lib/project-store');
10
- const { ProjectLifecycleService } = require('../lib/project-lifecycle-service');
11
- const realHookManager = require('../lib/hook-manager');
12
-
13
- const ROOT = path.resolve(__dirname, '..', '..');
14
- const TRIGGER = path.join(ROOT, '_site', 'scripts', 'hook-trigger.js');
15
-
16
- function git(repo, args) {
17
- const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
18
- if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
19
- return String(result.stdout || '').trim();
20
- }
21
-
22
- function createRepo(root, name, withCommit = true) {
23
- const repo = path.join(root, name);
24
- fs.mkdirSync(repo, { recursive: true });
25
- git(repo, ['init', '--initial-branch=main']);
26
- git(repo, ['config', 'user.email', 'life@example.invalid']);
27
- git(repo, ['config', 'user.name', 'Lifecycle Test']);
28
- if (withCommit) {
29
- fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`);
30
- git(repo, ['add', 'README.md']);
31
- git(repo, ['commit', '-m', 'initial']);
32
- }
33
- return repo;
34
- }
35
-
36
- (async () => {
37
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-lifecycle-'));
38
- const dataDir = path.join(root, 'data');
39
- const knowledgeRoot = path.join(root, 'knowledge');
40
- const layout = new StorageLayout({ dataDir });
41
- const settings = new SettingsStore({ layout });
42
- const registry = new ProjectRegistryStore({ layout });
43
- const projects = new ProjectStore({ layout });
44
- await settings.initialize({ knowledge: { rootPath: knowledgeRoot } });
45
- await registry.initialize();
46
- const lifecycle = new ProjectLifecycleService({
47
- layout,
48
- settingsStore: settings,
49
- registryStore: registry,
50
- projectStore: projects,
51
- triggerScriptPath: TRIGGER,
52
- bridgeAdapter: { async getHighWatermark() { return { status: 'captured', cursor: 73 }; } },
53
- });
54
-
55
- const repo = createRepo(root, 'repo-a');
56
- const claude = path.join(repo, 'CLAUDE.md');
57
- fs.writeFileSync(claude, '# user only\n');
58
- const imported = await lifecycle.importProject({ localPath: repo, projectId: 'project-life-a', storageName: 'repo-a-stable' });
59
- assert.strictEqual(imported.ok, true);
60
- assert.strictEqual(imported.config.aiProfileId, null, 'import must not require an AI profile');
61
- assert.strictEqual(imported.state.trackingStartCommit, git(repo, ['rev-parse', 'HEAD']));
62
- assert.strictEqual(imported.state.conversationBaselineCursor, 73, 'import must freeze the current Bridge cursor as its conversation baseline');
63
- assert.deepStrictEqual(fs.readdirSync(imported.config.knowledgePath), [], 'import must not create TODO knowledge files');
64
- assert.strictEqual(fs.readFileSync(claude, 'utf8'), '# user only\n');
65
- assert.strictEqual(realHookManager.readHookStatus({ repoPath: repo, projectId: imported.projectId }).installed, true);
66
- await assert.rejects(lifecycle.importProject({ localPath: repo }), /already imported/);
67
-
68
- const conflictRepo = createRepo(root, 'repo-conflict');
69
- const conflictPath = path.join(knowledgeRoot, 'conflict-stable');
70
- fs.mkdirSync(conflictPath, { recursive: true });
71
- fs.writeFileSync(path.join(conflictPath, 'user.md'), 'do not overwrite');
72
- await assert.rejects(lifecycle.importProject({ localPath: conflictRepo, projectId: 'project-conflict', storageName: 'conflict-stable' }), error => error.code === 'MIGRATION_TARGET_CONFLICT');
73
- assert(!registry.listIds().includes('project-conflict'));
74
- assert.strictEqual(fs.readFileSync(path.join(conflictPath, 'user.md'), 'utf8'), 'do not overwrite');
75
-
76
- const failingRepo = createRepo(root, 'repo-failing');
77
- const failingHooks = { ...realHookManager, installHook() { throw new Error('injected Hook failure'); } };
78
- const failingLifecycle = new ProjectLifecycleService({ layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER, hookManager: failingHooks });
79
- await assert.rejects(failingLifecycle.importProject({ localPath: failingRepo, projectId: 'project-failing', storageName: 'failing-stable' }));
80
- assert(!registry.listIds().includes('project-failing'));
81
- assert(!fs.existsSync(layout.getProjectMetadataDir('project-failing')));
82
- assert(!fs.existsSync(path.join(knowledgeRoot, 'failing-stable')));
83
-
84
- const emptyRepo = createRepo(root, 'repo-empty', false);
85
- const empty = await lifecycle.importProject({ localPath: emptyRepo, projectId: 'project-empty', storageName: 'empty-stable' });
86
- assert.strictEqual(empty.state.trackingMode, 'empty-repo');
87
- assert.strictEqual(empty.state.trackingStartCommit, null);
88
-
89
- const failingDelete = new ProjectLifecycleService({
90
- layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER,
91
- hookManager: { ...realHookManager, uninstallHook() { throw new Error('injected uninstall failure'); } },
92
- });
93
- await assert.rejects(failingDelete.deleteProject(imported.projectId));
94
- assert(registry.listIds().includes(imported.projectId), 'Hook uninstall failure must preserve registration');
95
-
96
- const knowledgePath = imported.config.knowledgePath;
97
- const deleted = await lifecycle.deleteProject(imported.projectId);
98
- assert.strictEqual(deleted.removedKnowledge, false);
99
- assert(fs.existsSync(knowledgePath), 'default delete must preserve external knowledge directory');
100
- assert(!registry.listIds().includes(imported.projectId));
101
-
102
- console.log('project-lifecycle-transaction-test PASS');
103
- })().catch(error => {
104
- console.error(error.stack || error.message);
105
- process.exitCode = 1;
106
- });
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const { StorageLayout } = require('../lib/storage-layout');
7
+ const { SettingsStore } = require('../lib/settings-store');
8
+ const { ProjectRegistryStore } = require('../lib/project-registry-store');
9
+ const { ProjectStore } = require('../lib/project-store');
10
+ const { ProjectLifecycleService } = require('../lib/project-lifecycle-service');
11
+ const realHookManager = require('../lib/hook-manager');
12
+
13
+ const ROOT = path.resolve(__dirname, '..', '..');
14
+ const TRIGGER = path.join(ROOT, '_site', 'scripts', 'hook-trigger.js');
15
+
16
+ function git(repo, args) {
17
+ const result = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
18
+ if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed`);
19
+ return String(result.stdout || '').trim();
20
+ }
21
+
22
+ function createRepo(root, name, withCommit = true) {
23
+ const repo = path.join(root, name);
24
+ fs.mkdirSync(repo, { recursive: true });
25
+ git(repo, ['init', '--initial-branch=main']);
26
+ git(repo, ['config', 'user.email', 'life@example.invalid']);
27
+ git(repo, ['config', 'user.name', 'Lifecycle Test']);
28
+ if (withCommit) {
29
+ fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`);
30
+ git(repo, ['add', 'README.md']);
31
+ git(repo, ['commit', '-m', 'initial']);
32
+ }
33
+ return repo;
34
+ }
35
+
36
+ (async () => {
37
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-lifecycle-'));
38
+ const dataDir = path.join(root, 'data');
39
+ const knowledgeRoot = path.join(root, 'knowledge');
40
+ const layout = new StorageLayout({ dataDir });
41
+ const settings = new SettingsStore({ layout });
42
+ const registry = new ProjectRegistryStore({ layout });
43
+ const projects = new ProjectStore({ layout });
44
+ await settings.initialize({ knowledge: { rootPath: knowledgeRoot } });
45
+ await registry.initialize();
46
+ const lifecycle = new ProjectLifecycleService({
47
+ layout,
48
+ settingsStore: settings,
49
+ registryStore: registry,
50
+ projectStore: projects,
51
+ triggerScriptPath: TRIGGER,
52
+ bridgeAdapter: { async getHighWatermark() { return { status: 'captured', cursor: 73 }; } },
53
+ });
54
+
55
+ const repo = createRepo(root, 'repo-a');
56
+ const claude = path.join(repo, 'CLAUDE.md');
57
+ fs.writeFileSync(claude, '# user only\n');
58
+ const imported = await lifecycle.importProject({ localPath: repo, projectId: 'project-life-a', storageName: 'repo-a-stable' });
59
+ assert.strictEqual(imported.ok, true);
60
+ assert.strictEqual(imported.config.aiProfileId, null, 'import must not require an AI profile');
61
+ assert.strictEqual(imported.state.trackingStartCommit, git(repo, ['rev-parse', 'HEAD']));
62
+ assert.strictEqual(imported.state.conversationBaselineCursor, 73, 'import must freeze the current Bridge cursor as its conversation baseline');
63
+ assert.deepStrictEqual(fs.readdirSync(imported.config.knowledgePath), [], 'import must not create TODO knowledge files');
64
+ assert.strictEqual(fs.readFileSync(claude, 'utf8'), '# user only\n');
65
+ assert.strictEqual(realHookManager.readHookStatus({ repoPath: repo, projectId: imported.projectId }).installed, true);
66
+ await assert.rejects(lifecycle.importProject({ localPath: repo }), /already imported/);
67
+
68
+ const conflictRepo = createRepo(root, 'repo-conflict');
69
+ const conflictPath = path.join(knowledgeRoot, 'conflict-stable');
70
+ fs.mkdirSync(conflictPath, { recursive: true });
71
+ fs.writeFileSync(path.join(conflictPath, 'user.md'), 'do not overwrite');
72
+ await assert.rejects(lifecycle.importProject({ localPath: conflictRepo, projectId: 'project-conflict', storageName: 'conflict-stable' }), error => error.code === 'MIGRATION_TARGET_CONFLICT');
73
+ assert(!registry.listIds().includes('project-conflict'));
74
+ assert.strictEqual(fs.readFileSync(path.join(conflictPath, 'user.md'), 'utf8'), 'do not overwrite');
75
+
76
+ const failingRepo = createRepo(root, 'repo-failing');
77
+ const failingHooks = { ...realHookManager, installHook() { throw new Error('injected Hook failure'); } };
78
+ const failingLifecycle = new ProjectLifecycleService({ layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER, hookManager: failingHooks });
79
+ await assert.rejects(failingLifecycle.importProject({ localPath: failingRepo, projectId: 'project-failing', storageName: 'failing-stable' }));
80
+ assert(!registry.listIds().includes('project-failing'));
81
+ assert(!fs.existsSync(layout.getProjectMetadataDir('project-failing')));
82
+ assert(!fs.existsSync(path.join(knowledgeRoot, 'failing-stable')));
83
+
84
+ const emptyRepo = createRepo(root, 'repo-empty', false);
85
+ const empty = await lifecycle.importProject({ localPath: emptyRepo, projectId: 'project-empty', storageName: 'empty-stable' });
86
+ assert.strictEqual(empty.state.trackingMode, 'empty-repo');
87
+ assert.strictEqual(empty.state.trackingStartCommit, null);
88
+
89
+ const failingDelete = new ProjectLifecycleService({
90
+ layout, settingsStore: settings, registryStore: registry, projectStore: projects, triggerScriptPath: TRIGGER,
91
+ hookManager: { ...realHookManager, uninstallHook() { throw new Error('injected uninstall failure'); } },
92
+ });
93
+ await assert.rejects(failingDelete.deleteProject(imported.projectId));
94
+ assert(registry.listIds().includes(imported.projectId), 'Hook uninstall failure must preserve registration');
95
+
96
+ const knowledgePath = imported.config.knowledgePath;
97
+ const deleted = await lifecycle.deleteProject(imported.projectId);
98
+ assert.strictEqual(deleted.removedKnowledge, false);
99
+ assert(fs.existsSync(knowledgePath), 'default delete must preserve external knowledge directory');
100
+ assert(!registry.listIds().includes(imported.projectId));
101
+
102
+ console.log('project-lifecycle-transaction-test PASS');
103
+ })().catch(error => {
104
+ console.error(error.stack || error.message);
105
+ process.exitCode = 1;
106
+ });