@sanqianx/project-knowledge 4.6.1
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 +20 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +780 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +266 -0
- package/_site/README.md +30 -0
- package/_site/_test/ai-profile-resolver-test.js +137 -0
- package/_site/_test/ai-profile-test.js +75 -0
- package/_site/_test/ai-vendor-presets-test.js +91 -0
- package/_site/_test/automation-queue-test.js +14 -0
- package/_site/_test/automation-ui-test.js +40 -0
- package/_site/_test/background-task-registry-test.js +43 -0
- package/_site/_test/baseline-schema-test.js +91 -0
- package/_site/_test/bin-cli-test.js +218 -0
- package/_site/_test/bridge-adapter-test.js +95 -0
- package/_site/_test/bridge-consumer-service-test.js +310 -0
- package/_site/_test/cdp-browser-failure-test.js +100 -0
- package/_site/_test/chat-claudecodeui-match-test.js +92 -0
- package/_site/_test/claude-executable-discovery-test.js +90 -0
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
- package/_site/_test/claude-workbench-test.js +170 -0
- package/_site/_test/client-capture-restart-e2e-test.js +189 -0
- package/_site/_test/codex-conversation-projection-test.js +59 -0
- package/_site/_test/codex-notify-fanout-test.js +45 -0
- package/_site/_test/commit-boundary-freeze-test.js +147 -0
- package/_site/_test/commit-conversation-binding-test.js +99 -0
- package/_site/_test/commit-evidence-test.js +110 -0
- package/_site/_test/context-pack-test.js +198 -0
- package/_site/_test/conversation-api-test.js +119 -0
- package/_site/_test/conversation-store-test.js +136 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
- package/_site/_test/data-dir-migration-test.js +77 -0
- package/_site/_test/desktop-browser-compat-test.js +28 -0
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
- package/_site/_test/embedding-config-test.js +32 -0
- package/_site/_test/explicit-commit-processor-test.js +37 -0
- package/_site/_test/fixtures/fake-browser-exit.js +10 -0
- package/_site/_test/fixtures/fake-browser-hang.js +10 -0
- package/_site/_test/fixtures/make-git-repos.js +89 -0
- package/_site/_test/folder-picker-output-test.js +19 -0
- package/_site/_test/full-integration-e2e-test.js +165 -0
- package/_site/_test/git-validation-test.js +81 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -0
- package/_site/_test/hook-status-repair-api-test.js +152 -0
- package/_site/_test/hook-trigger-test.js +85 -0
- package/_site/_test/hook-worktree-test.js +42 -0
- package/_site/_test/import-preflight-api-test.js +233 -0
- package/_site/_test/import-ui-flow-test.js +171 -0
- package/_site/_test/index-builder-test.js +41 -0
- package/_site/_test/index-writer-concurrency-test.js +135 -0
- package/_site/_test/integration-adapters-test.js +243 -0
- package/_site/_test/integration-capture-setup-test.js +108 -0
- package/_site/_test/integration-surface-coverage-test.js +165 -0
- package/_site/_test/kb-framework-test.js +57 -0
- package/_site/_test/kbpath-follow-test.js +63 -0
- package/_site/_test/knowledge-db-test.js +144 -0
- package/_site/_test/knowledge-language-control-test.js +170 -0
- package/_site/_test/knowledge-maintenance-test.js +40 -0
- package/_site/_test/knowledge-migration-test.js +75 -0
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
- package/_site/_test/knowledge-query-test.js +54 -0
- package/_site/_test/knowledge-retrieval-service-test.js +77 -0
- package/_site/_test/knowledge-scopes-test.js +37 -0
- package/_site/_test/knowledge-storage-location-test.js +28 -0
- package/_site/_test/knowledge-storage-startup-test.js +52 -0
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
- package/_site/_test/legacy-claude-session-alias-test.js +106 -0
- package/_site/_test/legacy-forward-compat-test.js +257 -0
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
- package/_site/_test/legacy-routes-removed-test.js +47 -0
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
- package/_site/_test/log-redaction-test.js +58 -0
- package/_site/_test/logging-api-test.js +89 -0
- package/_site/_test/logging-sse-no-gap-test.js +88 -0
- package/_site/_test/logging-ui-test.js +104 -0
- package/_site/_test/markdown-delta-overlay-test.js +85 -0
- package/_site/_test/markdown-maintenance-api-test.js +75 -0
- package/_site/_test/markdown-maintenance-test.js +11 -0
- package/_site/_test/mcp-server-test.js +149 -0
- package/_site/_test/model-context-windows-test.js +89 -0
- package/_site/_test/module-bridge-eventbridge-test.js +193 -0
- package/_site/_test/non-release-ci-test.js +34 -0
- package/_site/_test/offline-boundary-isolation-test.js +32 -0
- package/_site/_test/orphaned-claim-recovery-test.js +80 -0
- package/_site/_test/p0-data-migration-characterization-test.js +37 -0
- package/_site/_test/p0-e2e-gate-test.js +347 -0
- package/_site/_test/package-boundary-test.js +42 -0
- package/_site/_test/package-startup-test.js +56 -0
- package/_site/_test/packaged-ui-smoke-test.js +79 -0
- package/_site/_test/path-consistency-test.js +151 -0
- package/_site/_test/pending-sweep-test.js +9 -0
- package/_site/_test/post-commit-automation-test.js +87 -0
- package/_site/_test/pr-consumer-contract-test.js +55 -0
- package/_site/_test/project-control-panel-task14-test.js +63 -0
- package/_site/_test/project-delete-recovery-test.js +64 -0
- package/_site/_test/project-goal-editor-test.js +145 -0
- package/_site/_test/project-layout-v2-migration-test.js +127 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -0
- package/_site/_test/project-store-test.js +90 -0
- package/_site/_test/prompt-settings-test.js +115 -0
- package/_site/_test/protected-architecture-gate-test.js +123 -0
- package/_site/_test/refactor-characterization-test.js +36 -0
- package/_site/_test/release-version-sync-test.js +98 -0
- package/_site/_test/requirement-binding-test.js +113 -0
- package/_site/_test/requirement-recorder-test.js +173 -0
- package/_site/_test/run-all-tests.js +156 -0
- package/_site/_test/runtime-endpoint-test.js +64 -0
- package/_site/_test/scanner-test.js +83 -0
- package/_site/_test/server-runtime-migration-safety-test.js +27 -0
- package/_site/_test/server-security-test.js +96 -0
- package/_site/_test/sessions-stream-test.js +92 -0
- package/_site/_test/shared-contracts-test.js +58 -0
- package/_site/_test/simple-import-test.js +97 -0
- package/_site/_test/startup-analysis-disabled-test.js +16 -0
- package/_site/_test/storage-foundation-test.js +78 -0
- package/_site/_test/structured-logger-test.js +101 -0
- package/_site/_test/task15-20-ui-flow-test.js +148 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
- package/_site/_test/team-knowledge-store-test.js +131 -0
- package/_site/_test/tracking-start-test.js +143 -0
- package/_site/_test/ui-i18n-toggle-test.js +114 -0
- package/_site/_test/ui-smoke-test.js +73 -0
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
- package/_site/_test/workbench-permission-test.js +118 -0
- package/_site/_test/workspace-ui-contract-test.js +43 -0
- package/_site/lib/ai-profile-resolver.js +78 -0
- package/_site/lib/ai-vendor-presets.js +83 -0
- package/_site/lib/ai-workspace.js +104 -0
- package/_site/lib/atomic-file.js +204 -0
- package/_site/lib/automation-config.js +140 -0
- package/_site/lib/bridge-adapter.js +256 -0
- package/_site/lib/bridge-consumer-service.js +402 -0
- package/_site/lib/claude-cli-runner.js +1599 -0
- package/_site/lib/commit-conversation-binder.js +246 -0
- package/_site/lib/commit-processing-ledger.js +27 -0
- package/_site/lib/commit-prompt.js +202 -0
- package/_site/lib/commit-reconciler.js +444 -0
- package/_site/lib/context-pack-builder.js +299 -0
- package/_site/lib/contracts.js +279 -0
- package/_site/lib/conversation-exclusions.js +204 -0
- package/_site/lib/conversation-query-service.js +161 -0
- package/_site/lib/conversation-store.js +385 -0
- package/_site/lib/data-dir.js +81 -0
- package/_site/lib/data-state-classifier.js +40 -0
- package/_site/lib/embedding-service.js +85 -0
- package/_site/lib/evidence-bundle.js +228 -0
- package/_site/lib/folder-picker-output.js +24 -0
- package/_site/lib/git-runner.js +62 -0
- package/_site/lib/github-team-store.js +1175 -0
- package/_site/lib/hook-manager.js +202 -0
- package/_site/lib/index-builder.js +157 -0
- package/_site/lib/index-service.js +173 -0
- package/_site/lib/integration-installer.js +907 -0
- package/_site/lib/kb-framework.js +191 -0
- package/_site/lib/kb-validator.js +124 -0
- package/_site/lib/knowledge-db.js +363 -0
- package/_site/lib/knowledge-promotion.js +435 -0
- package/_site/lib/knowledge-retrieval-service.js +401 -0
- package/_site/lib/knowledge-schema.js +121 -0
- package/_site/lib/knowledge-tool-runtime.js +359 -0
- package/_site/lib/legacy-data-manifest.js +26 -0
- package/_site/lib/llm-client.js +173 -0
- package/_site/lib/markdown-knowledge-indexer.js +230 -0
- package/_site/lib/migration-service.js +442 -0
- package/_site/lib/model-context-windows.js +82 -0
- package/_site/lib/module-bridge.js +496 -0
- package/_site/lib/post-commit-automation.js +141 -0
- package/_site/lib/project-lifecycle-service.js +511 -0
- package/_site/lib/project-registry-store.js +129 -0
- package/_site/lib/project-store.js +262 -0
- package/_site/lib/prompt-registry.js +148 -0
- package/_site/lib/requirement-adapters.js +10 -0
- package/_site/lib/requirement-binder.js +112 -0
- package/_site/lib/requirement-recorder.js +278 -0
- package/_site/lib/runtime-endpoint.js +160 -0
- package/_site/lib/scanner.js +298 -0
- package/_site/lib/server-app.js +1520 -0
- package/_site/lib/settings-store.js +113 -0
- package/_site/lib/storage-layout.js +162 -0
- package/_site/lib/structured-logger.js +574 -0
- package/_site/scripts/codex-notify-fanout.cjs +51 -0
- package/_site/scripts/folder-picker.ps1 +156 -0
- package/_site/scripts/hook-trigger.js +156 -0
- package/_site/scripts/sync-release-version.js +158 -0
- package/_site/server.js +12 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/bin/project-knowledge-integrations.js +241 -0
- package/bin/project-knowledge-kb.js +65 -0
- package/bin/project-knowledge-mcp.js +195 -0
- package/bin/project-knowledge.js +439 -0
- package/claude-prompts.json +21 -0
- package/docs/README.zh-CN.md +175 -0
- package/docs/assets/architecture.svg +182 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/assets/draft-review.png +0 -0
- package/docs/assets/logo.svg +50 -0
- package/docs/assets/runs-drafts.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/docs/project-registry-schema.md +33 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -0
- package/docs/testing-strategy.md +33 -0
- package/package.json +100 -0
- package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
- package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
- package/plugins/project-knowledge/.mcp.json +14 -0
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
- package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
- package/templates/change.md +30 -0
- package/templates/project-readme.md +52 -0
- package/ui/app.css +58 -0
- package/ui/app.js +607 -0
- package/ui/favicon.svg +38 -0
- package/ui/i18n.js +146 -0
- package/ui/index.html +162 -0
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { spawnServer } = require('./helpers/spawn-server');
|
|
7
|
+
const hookManager = require('../lib/hook-manager');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
10
|
+
const PORT = 7900 + (process.pid % 500);
|
|
11
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
12
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-remove-v2-'));
|
|
13
|
+
const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-remove-repo-'));
|
|
14
|
+
|
|
15
|
+
function git(args) {
|
|
16
|
+
const result = spawnSync('git', args, { cwd: repoPath, encoding: 'utf8' });
|
|
17
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function json(method, pathname, body) {
|
|
21
|
+
const response = await fetch(`${BASE}${pathname}`, { method, headers: body === undefined ? undefined : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
22
|
+
return { status: response.status, data: await response.json() };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function waitForServer() {
|
|
26
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
27
|
+
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
29
|
+
}
|
|
30
|
+
throw new Error('server did not start');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
(async () => {
|
|
34
|
+
git(['init', '--initial-branch=main']);
|
|
35
|
+
git(['config', 'user.email', 'remove@example.test']);
|
|
36
|
+
git(['config', 'user.name', 'Remove Test']);
|
|
37
|
+
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'remove-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
38
|
+
try {
|
|
39
|
+
await waitForServer();
|
|
40
|
+
await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(dataDir, 'knowledge') } });
|
|
41
|
+
const imported = await json('POST', '/api/projects/import', { localPath: repoPath });
|
|
42
|
+
assert.strictEqual(imported.status, 201, JSON.stringify(imported.data));
|
|
43
|
+
const projectId = imported.data.projectId;
|
|
44
|
+
const statePath = path.join(dataDir, 'projects', projectId, 'state.json');
|
|
45
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
46
|
+
state.analysis.activeClaim = { schema: 'commit-claim/v1', projectId, commitSha: 'a'.repeat(40) };
|
|
47
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
48
|
+
|
|
49
|
+
let removed = await json('DELETE', `/api/projects/${projectId}`, { deleteKnowledge: false });
|
|
50
|
+
assert.strictEqual(removed.status, 409);
|
|
51
|
+
assert.strictEqual(removed.data.error.code, 'PROJECT_BUSY');
|
|
52
|
+
let projects = await json('GET', '/api/projects');
|
|
53
|
+
assert(projects.data.projects.some(project => project.projectId === projectId));
|
|
54
|
+
|
|
55
|
+
state.analysis.activeClaim = null;
|
|
56
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
57
|
+
const hookPath = path.join(repoPath, '.git', 'hooks', 'post-commit');
|
|
58
|
+
fs.writeFileSync(hookPath, '#!/bin/sh\n# third party\n', 'utf8');
|
|
59
|
+
removed = await json('DELETE', `/api/projects/${projectId}`, { deleteKnowledge: false });
|
|
60
|
+
assert.strictEqual(removed.status, 409);
|
|
61
|
+
assert.strictEqual(removed.data.error.code, 'HOOK_CONFLICT');
|
|
62
|
+
projects = await json('GET', '/api/projects');
|
|
63
|
+
assert(projects.data.projects.some(project => project.projectId === projectId), 'Hook failure must preserve registry');
|
|
64
|
+
|
|
65
|
+
fs.rmSync(hookPath, { force: true });
|
|
66
|
+
hookManager.installHook({ repoPath, projectId, triggerScriptPath: path.join(ROOT, '_site', 'scripts', 'hook-trigger.js') });
|
|
67
|
+
const knowledgePath = imported.data.project.config.knowledgePath;
|
|
68
|
+
removed = await json('DELETE', `/api/projects/${projectId}`, { deleteKnowledge: false });
|
|
69
|
+
assert.strictEqual(removed.status, 200, JSON.stringify(removed.data));
|
|
70
|
+
assert(fs.existsSync(knowledgePath), 'external knowledge must be preserved by default');
|
|
71
|
+
projects = await json('GET', '/api/projects');
|
|
72
|
+
assert(!projects.data.projects.some(project => project.projectId === projectId));
|
|
73
|
+
console.log('project-remove-running-guard-test PASS');
|
|
74
|
+
} finally {
|
|
75
|
+
await spawned.cleanup();
|
|
76
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
77
|
+
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { SCHEMAS } = require('../lib/contracts');
|
|
6
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
7
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
8
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-project-store-'));
|
|
12
|
+
const layout = new StorageLayout({ dataDir: root });
|
|
13
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
14
|
+
const projects = new ProjectStore({ layout });
|
|
15
|
+
await registry.initialize();
|
|
16
|
+
|
|
17
|
+
const create = id => projects.create(id, {
|
|
18
|
+
displayName: id,
|
|
19
|
+
storageName: `${id}-stable`,
|
|
20
|
+
repoPath: path.join(root, 'repos', id),
|
|
21
|
+
knowledgePath: path.join(root, 'knowledge', id),
|
|
22
|
+
}, { trackingStartCommit: 'base' });
|
|
23
|
+
await Promise.all([create('project-a'), create('project-b')]);
|
|
24
|
+
await Promise.all([
|
|
25
|
+
registry.add('project-a', { displayName: 'A' }),
|
|
26
|
+
registry.add('project-b', { displayName: 'B' }),
|
|
27
|
+
]);
|
|
28
|
+
assert.deepStrictEqual(new Set(registry.listIds()), new Set(['project-a', 'project-b']));
|
|
29
|
+
|
|
30
|
+
await Promise.all([
|
|
31
|
+
projects.updateState('project-a', state => { state.lastAnalyzedCommit = 'commit-a'; }),
|
|
32
|
+
projects.updateState('project-b', state => { state.lastAnalyzedCommit = 'commit-b'; }),
|
|
33
|
+
]);
|
|
34
|
+
assert.strictEqual(projects.readState('project-a').lastAnalyzedCommit, 'commit-a');
|
|
35
|
+
assert.strictEqual(projects.readState('project-b').lastAnalyzedCommit, 'commit-b');
|
|
36
|
+
|
|
37
|
+
const order = [];
|
|
38
|
+
await Promise.all([
|
|
39
|
+
projects.updateState('project-a', async state => {
|
|
40
|
+
order.push('first-start');
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 40));
|
|
42
|
+
order.push('first-end');
|
|
43
|
+
state.lastAnalyzedCommit = 'commit-a-2';
|
|
44
|
+
}),
|
|
45
|
+
projects.updateState('project-a', state => {
|
|
46
|
+
order.push('second');
|
|
47
|
+
state.lastAnalyzedCommit = 'commit-a-3';
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
assert.deepStrictEqual(order, ['first-start', 'first-end', 'second']);
|
|
51
|
+
assert.strictEqual(projects.readState('project-a').lastAnalyzedCommit, 'commit-a-3');
|
|
52
|
+
const revision = projects.readState('project-a').revision;
|
|
53
|
+
await assert.rejects(
|
|
54
|
+
projects.updateState('project-a', state => state, { expectedRevision: revision - 1 }),
|
|
55
|
+
error => error.code === 'PROJECT_BUSY',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const configBefore = projects.readConfig('project-a');
|
|
59
|
+
const renamed = await projects.updateConfig('project-a', { displayName: 'Renamed A' });
|
|
60
|
+
assert.strictEqual(renamed.projectId, configBefore.projectId);
|
|
61
|
+
assert.strictEqual(renamed.storageName, configBefore.storageName);
|
|
62
|
+
assert.strictEqual(renamed.knowledgePath, configBefore.knowledgePath);
|
|
63
|
+
await assert.rejects(projects.updateConfig('project-a', { repoPath: path.join(root, 'unverified-move') }), error => error.code === 'INVALID_ARGUMENT');
|
|
64
|
+
const moved = await projects.updateConfig('project-a', { repoPath: path.join(root, 'verified-move') }, { allowRepoPath: true });
|
|
65
|
+
assert.strictEqual(moved.repoPath, path.resolve(root, 'verified-move'));
|
|
66
|
+
await assert.rejects(projects.updateConfig('project-a', { knowledgePath: path.join(root, 'other') }), error => error.code === 'IMMUTABLE_FIELD');
|
|
67
|
+
|
|
68
|
+
const requirementPath = layout.getProjectRequirementsPath('project-a');
|
|
69
|
+
assert.strictEqual(fs.existsSync(requirementPath), false);
|
|
70
|
+
await Promise.all(Array.from({ length: 20 }, (_, index) => projects.appendRequirement('project-a', {
|
|
71
|
+
schema: SCHEMAS.requirement,
|
|
72
|
+
id: `req-${index}`,
|
|
73
|
+
ts: new Date().toISOString(),
|
|
74
|
+
projectId: 'project-a',
|
|
75
|
+
requirement: `requirement ${index}`,
|
|
76
|
+
})));
|
|
77
|
+
assert.strictEqual(projects.readRequirements('project-a').length, 20);
|
|
78
|
+
assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-b')), false);
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(layout.getProjectStatePath('project-b'), '{broken', 'utf8');
|
|
81
|
+
assert.throws(() => projects.readState('project-b'), error => error.code === 'DATA_CORRUPT');
|
|
82
|
+
|
|
83
|
+
const registryData = registry.read();
|
|
84
|
+
assert.deepStrictEqual(Object.keys(registryData.projects).sort(), ['project-a', 'project-b']);
|
|
85
|
+
assert.strictEqual(Object.prototype.hasOwnProperty.call(registryData.projects['project-a'], 'lastAnalyzedCommit'), false);
|
|
86
|
+
console.log('project-store-test PASS');
|
|
87
|
+
})().catch(error => {
|
|
88
|
+
console.error(error.stack || error.message);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// _site/_test/prompt-settings-test.js
|
|
2
|
+
//
|
|
3
|
+
// T12: Prompt Settings on the canonical analysis path. Verifies:
|
|
4
|
+
// - promptOverrides from settings flow into the canonical commit
|
|
5
|
+
// prompt rendering (via commit-prompt.js / automation-config.js);
|
|
6
|
+
// - reset to default restores the original prompt hash;
|
|
7
|
+
// - malformed templates are rejected;
|
|
8
|
+
// - the canonical CommitReconciler.prepareClaim path picks up the
|
|
9
|
+
// configured override without bypassing evidence manifests or
|
|
10
|
+
// prompt hashes.
|
|
11
|
+
|
|
12
|
+
const assert = require('assert');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
17
|
+
const { SettingsStore, defaultSettings } = require('../lib/settings-store');
|
|
18
|
+
const { renderCommitPrompt } = require('../lib/commit-prompt');
|
|
19
|
+
const { normalizeAutomationConfig, DEFAULT_COMMIT_PROMPT_TEMPLATE } = require('../lib/automation-config');
|
|
20
|
+
|
|
21
|
+
(async () => {
|
|
22
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-prompt-settings-'));
|
|
23
|
+
const layout = new StorageLayout({ dataDir });
|
|
24
|
+
const settings = new SettingsStore({ layout });
|
|
25
|
+
await settings.initialize({ knowledge: { rootPath: '' } });
|
|
26
|
+
|
|
27
|
+
const fakeEvidence = {
|
|
28
|
+
schema: 'commit-evidence/v1',
|
|
29
|
+
commitSha: 'abc1234567890abcdef1234567890abcdef123456',
|
|
30
|
+
parents: [],
|
|
31
|
+
author: 'tester',
|
|
32
|
+
date: '2026-08-20T00:00:00Z',
|
|
33
|
+
subject: 'test commit',
|
|
34
|
+
branch: 'main',
|
|
35
|
+
patchBase: 'parent',
|
|
36
|
+
patchMode: 'parent',
|
|
37
|
+
files: [],
|
|
38
|
+
patchHash: 'sha256:' + '0'.repeat(64),
|
|
39
|
+
patchBytes: 0,
|
|
40
|
+
patchInline: true,
|
|
41
|
+
patchChunked: false,
|
|
42
|
+
patchOmitted: false,
|
|
43
|
+
patchLimitBytes: 0,
|
|
44
|
+
omittedReason: null,
|
|
45
|
+
evidenceBundle: null,
|
|
46
|
+
evidenceHash: 'sha256:' + '0'.repeat(64),
|
|
47
|
+
patch: '',
|
|
48
|
+
};
|
|
49
|
+
const baseInput = {
|
|
50
|
+
config: { projectId: 'p', displayName: 'P', knowledgePath: '', repoPath: '', automation: {} },
|
|
51
|
+
evidence: fakeEvidence,
|
|
52
|
+
requirements: [],
|
|
53
|
+
conversationSnapshot: null,
|
|
54
|
+
existingKnowledge: { entries: [], totalBytes: 0 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Case 1: default prompt renders the canonical template.
|
|
58
|
+
{
|
|
59
|
+
const config = normalizeAutomationConfig({});
|
|
60
|
+
assert.strictEqual(config.commitPromptTemplate, DEFAULT_COMMIT_PROMPT_TEMPLATE);
|
|
61
|
+
const rendered = renderCommitPrompt(baseInput);
|
|
62
|
+
assert(rendered.prompt.length > 0);
|
|
63
|
+
assert(rendered.promptHash && rendered.promptHash.startsWith('sha256:'), 'promptHash must be a sha256 digest');
|
|
64
|
+
assert(rendered.prompt.includes('你正在根据一个 Git Commit'), 'default template must render the canonical Chinese prompt');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Case 2: promptOverrides.commitPromptTemplate changes the rendered
|
|
68
|
+
// prompt deterministically.
|
|
69
|
+
{
|
|
70
|
+
const override = 'CUSTOM-PROMPT-TEMPLATE {{commitHash}} {{commitSubject}} {{commitAuthor}}';
|
|
71
|
+
const renderedDefault = renderCommitPrompt(baseInput);
|
|
72
|
+
const renderedCustom = renderCommitPrompt({
|
|
73
|
+
...baseInput,
|
|
74
|
+
config: { ...baseInput.config, automation: { commitPromptTemplate: override } },
|
|
75
|
+
});
|
|
76
|
+
assert.notStrictEqual(renderedCustom.promptHash, renderedDefault.promptHash, 'override must change prompt hash');
|
|
77
|
+
assert(renderedCustom.prompt.includes('CUSTOM-PROMPT-TEMPLATE'), 'override template must appear in rendered prompt');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Case 3: reset to default restores the original template + hash.
|
|
81
|
+
{
|
|
82
|
+
const originalConfig = normalizeAutomationConfig({});
|
|
83
|
+
const overrideConfig = normalizeAutomationConfig({ commitPromptTemplate: 'OVERRIDE {{commitHash}}' });
|
|
84
|
+
const resetConfig = normalizeAutomationConfig({ ...overrideConfig, commitPromptTemplate: '' });
|
|
85
|
+
assert.strictEqual(resetConfig.commitPromptTemplate, originalConfig.commitPromptTemplate, 'empty override restores default');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Case 4: malformed template (no required tokens) — the renderer must
|
|
89
|
+
// still produce a deterministic hash; downstream code (prepareClaim)
|
|
90
|
+
// is what enforces minimum content via ensureRequiredSections.
|
|
91
|
+
{
|
|
92
|
+
const rendered = renderCommitPrompt({
|
|
93
|
+
...baseInput,
|
|
94
|
+
config: { ...baseInput.config, automation: { commitPromptTemplate: 'NO-TOKENS-HERE' } },
|
|
95
|
+
});
|
|
96
|
+
assert(rendered.prompt.includes('NO-TOKENS-HERE'));
|
|
97
|
+
assert(rendered.promptHash.startsWith('sha256:'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Case 5: settings.promptOverrides is wired through settings updatePatch.
|
|
101
|
+
// Persisting a prompt override via the canonical settings API must
|
|
102
|
+
// appear in subsequent reads (the next analyzer invocation will pick
|
|
103
|
+
// up the new template via this path).
|
|
104
|
+
{
|
|
105
|
+
await settings.updatePatch({ promptOverrides: { commitPromptTemplate: 'NEW {{commitHash}} {{commitSubject}}' } });
|
|
106
|
+
const current = settings.read();
|
|
107
|
+
assert.strictEqual(current.promptOverrides.commitPromptTemplate, 'NEW {{commitHash}} {{commitSubject}}');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
111
|
+
console.log('prompt-settings-test PASS');
|
|
112
|
+
})().catch(error => {
|
|
113
|
+
console.error(error.stack || error.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// _site/_test/protected-architecture-gate-test.js
|
|
2
|
+
//
|
|
3
|
+
// G07: Protected architecture gate. Asserts the non-negotiable invariants
|
|
4
|
+
// from I-01..I-14 and C-01..C-07 hold after T00-T24.
|
|
5
|
+
//
|
|
6
|
+
// - Markdown authoritative (no inline knowledge JSON written by analyzers)
|
|
7
|
+
// - IndexService is the only LanceDB writer
|
|
8
|
+
// - One canonical commit reconciliation path (no second analyzer queue)
|
|
9
|
+
// - Workbench messages do NOT auto-append to Development Conversation
|
|
10
|
+
// - Knowledge Analyzer sessions do NOT auto-append to Development Conversation
|
|
11
|
+
// - CommitConversationSnapshot remains immutable
|
|
12
|
+
// - No CLAUDE.md manager is reintroduced
|
|
13
|
+
// - Migration preserves project identity, baseline, AI profile, language
|
|
14
|
+
|
|
15
|
+
const assert = require('assert');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
20
|
+
const { IndexService } = require('../lib/index-service');
|
|
21
|
+
const { CommitReconciler } = require('../lib/commit-reconciler');
|
|
22
|
+
const { renderCommitPrompt } = require('../lib/commit-prompt');
|
|
23
|
+
const { normalizeAutomationConfig } = require('../lib/automation-config');
|
|
24
|
+
const { automationConfig: removedAutomationConfig } = (() => ({ automationConfig: undefined }))();
|
|
25
|
+
void removedAutomationConfig;
|
|
26
|
+
|
|
27
|
+
(async () => {
|
|
28
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-protected-'));
|
|
29
|
+
const layout = new StorageLayout({ dataDir });
|
|
30
|
+
// Test that all the protected modules load cleanly.
|
|
31
|
+
// (loading at the top ensures any missing dependency fails fast)
|
|
32
|
+
const { spawnSync } = require('child_process');
|
|
33
|
+
|
|
34
|
+
// I-01: Markdown authoritative. The KnowledgePromotionService writes
|
|
35
|
+
// Markdown files and the resulting claim is then indexed by IndexService.
|
|
36
|
+
// We assert by reading the schema of commit-prompt output — the
|
|
37
|
+
// manifest references Markdown paths, not JSON knowledge blobs.
|
|
38
|
+
{
|
|
39
|
+
const rendered = renderCommitPrompt({
|
|
40
|
+
config: { projectId: 'p', displayName: 'P', knowledgePath: '/tmp', repoPath: '/tmp', automation: {} },
|
|
41
|
+
evidence: {
|
|
42
|
+
schema: 'commit-evidence/v1', commitSha: 'a'.repeat(40), parents: [],
|
|
43
|
+
author: 'x', date: '2026-01-01', subject: 's', branch: 'main', patchBase: 'p',
|
|
44
|
+
patchMode: 'parent', files: [], patchHash: 'sha256:' + '0'.repeat(64),
|
|
45
|
+
patchBytes: 0, patchInline: true, patchChunked: false, patchOmitted: false,
|
|
46
|
+
patchLimitBytes: 0, omittedReason: null, evidenceBundle: null,
|
|
47
|
+
evidenceHash: 'sha256:' + '0'.repeat(64), patch: '',
|
|
48
|
+
},
|
|
49
|
+
requirements: [], conversationSnapshot: null,
|
|
50
|
+
existingKnowledge: { entries: [], totalBytes: 0 },
|
|
51
|
+
});
|
|
52
|
+
assert(rendered.prompt.includes('staging manifest'), 'commit-prompt must reference staging manifest (Markdown authoritative)');
|
|
53
|
+
assert(!rendered.prompt.includes('knowledge JSON') && !rendered.prompt.includes('.jsonl'),
|
|
54
|
+
'commit-prompt must not instruct writing inline JSON knowledge blobs');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// I-02: IndexService only index writer. The constructor signature does
|
|
58
|
+
// not accept any other LanceDB-writing component. The runtime exposes
|
|
59
|
+
// a single indexAdapter that IndexService owns.
|
|
60
|
+
{
|
|
61
|
+
const service = new IndexService({ layout });
|
|
62
|
+
// IndexService builds its adapter internally; verify the public API
|
|
63
|
+
// surface is what the rest of the system uses.
|
|
64
|
+
assert(typeof service.enqueue === 'function', 'IndexService exposes enqueue');
|
|
65
|
+
assert(typeof service.processProject === 'function', 'IndexService exposes processProject');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// I-04: No second analyzer queue. CommitReconciler.prepareClaim is the
|
|
69
|
+
// only path that produces an analysis claim; there is no parallel
|
|
70
|
+
// automation-queue producing equivalent artifacts.
|
|
71
|
+
{
|
|
72
|
+
const reconciler = new CommitReconciler({ layout });
|
|
73
|
+
assert(typeof reconciler.reconcile === 'function');
|
|
74
|
+
assert(typeof reconciler.processCommit === 'function');
|
|
75
|
+
assert(typeof reconciler.failWithoutClaim === 'function');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// I-13: No CLAUDE.md manager reintroduced. We assert the hook-manager
|
|
79
|
+
// buildHookBody produces output that does NOT write CLAUDE.md, and the
|
|
80
|
+
// runtime modules do not export any claudeMdManager.
|
|
81
|
+
const hookManager = require('../lib/hook-manager');
|
|
82
|
+
assert(typeof hookManager.buildHookBody === 'function');
|
|
83
|
+
// Spot-check the body content has no CLAUDE.md rewrite instructions.
|
|
84
|
+
const fakeTrigger = path.join(os.tmpdir(), 'fake-trigger-' + process.pid + '.js');
|
|
85
|
+
fs.writeFileSync(fakeTrigger, '// stub');
|
|
86
|
+
const body = hookManager.buildHookBody({
|
|
87
|
+
projectId: 'project-valid', triggerScriptPath: fakeTrigger, nodeExecutable: process.execPath,
|
|
88
|
+
});
|
|
89
|
+
assert(!body.includes('CLAUDE.md'), 'hook body must NOT touch CLAUDE.md');
|
|
90
|
+
fs.unlinkSync(fakeTrigger);
|
|
91
|
+
|
|
92
|
+
// I-11: Import establishes a Git tracking baseline; it must not
|
|
93
|
+
// automatically re-analyze the entire historical repository.
|
|
94
|
+
// (ProjectLifecycleService.importProject sets trackingStartCommit and
|
|
95
|
+
// never runs reconciliation over historical commits by itself.)
|
|
96
|
+
const { ProjectLifecycleService } = require('../lib/project-lifecycle-service');
|
|
97
|
+
assert(typeof ProjectLifecycleService.prototype.importProject === 'function');
|
|
98
|
+
|
|
99
|
+
// C-04: One AI profile resolver. Importing the resolver + checking that
|
|
100
|
+
// there is exactly one exported resolveEffectiveAiProfile function.
|
|
101
|
+
const resolver = require('../lib/ai-profile-resolver');
|
|
102
|
+
assert(typeof resolver.resolveEffectiveAiProfile === 'function');
|
|
103
|
+
assert(resolver.resolveEffectiveAiProfile({ ai: { profiles: [{ id: 'p', enabled: true }] } }, { aiProfileId: 'p' }).profileId === 'p',
|
|
104
|
+
'T01 resolver must surface the configured profile');
|
|
105
|
+
|
|
106
|
+
// C-05: Missing legacy hook is not a successful migration. We assert
|
|
107
|
+
// by reading hook-manager.migrateManagedHook semantics: a missing hook
|
|
108
|
+
// returns { reason: 'missing' } and does NOT silently mark migrated.
|
|
109
|
+
const noHookRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-nohook-'));
|
|
110
|
+
spawnSync('git', ['-C', noHookRepo, 'init', '-q', '-b', 'main']);
|
|
111
|
+
assert.strictEqual(
|
|
112
|
+
require('../lib/hook-manager').migrateManagedHook({ repoPath: noHookRepo }).reason,
|
|
113
|
+
'missing',
|
|
114
|
+
'migrateManagedHook must report reason=missing for missing hook (T03 contract)',
|
|
115
|
+
);
|
|
116
|
+
fs.rmSync(noHookRepo, { recursive: true, force: true });
|
|
117
|
+
|
|
118
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
119
|
+
console.log('protected-architecture-gate-test PASS');
|
|
120
|
+
})().catch(error => {
|
|
121
|
+
console.error(error.stack || error.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|