@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.
- package/.agents/plugins/marketplace.json +17 -17
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +817 -780
- package/README.md +48 -36
- package/_modules/knowledge-engine/README.md +88 -0
- package/_modules/knowledge-engine/package.json +38 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +25 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +54 -0
- package/_modules/knowledge-engine/src/bin.js +52 -0
- package/_modules/knowledge-engine/src/bridge-client.js +24 -0
- package/_modules/knowledge-engine/src/coordinator.js +255 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +75 -0
- package/_modules/knowledge-engine/src/memories.js +85 -0
- package/_modules/knowledge-engine/src/migration.js +88 -0
- package/_modules/knowledge-engine/src/retrieval.js +116 -0
- package/_modules/knowledge-engine/src/runtime/README.md +7 -0
- package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
- package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
- package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
- package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
- package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
- package/_modules/knowledge-engine/src/server.js +155 -0
- package/_modules/knowledge-engine/src/store.js +239 -0
- package/_modules/knowledge-engine/ui/app.css +172 -0
- package/_modules/knowledge-engine/ui/app.js +297 -0
- package/_modules/knowledge-engine/ui/index.html +95 -0
- package/_site/README.md +30 -30
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- package/_site/_test/automation-queue-test.js +14 -14
- package/_site/_test/automation-ui-test.js +40 -40
- package/_site/_test/background-task-registry-test.js +43 -43
- package/_site/_test/baseline-schema-test.js +91 -91
- package/_site/_test/bridge-adapter-test.js +1 -1
- package/_site/_test/bridge-consumer-service-test.js +1 -1
- package/_site/_test/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +62 -62
- package/_site/_test/claude-workbench-test.js +170 -170
- package/_site/_test/client-capture-restart-e2e-test.js +4 -4
- package/_site/_test/codex-conversation-projection-test.js +2 -1
- package/_site/_test/commit-boundary-freeze-test.js +1 -1
- package/_site/_test/commit-conversation-binding-test.js +95 -95
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/conversation-api-test.js +109 -109
- package/_site/_test/conversation-store-test.js +136 -136
- package/_site/_test/cross-repo-isolation-e2e-test.js +2 -2
- package/_site/_test/data-dir-migration-test.js +57 -57
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/make-git-repos.js +12 -1
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/full-integration-e2e-test.js +165 -165
- package/_site/_test/git-validation-test.js +2 -2
- package/_site/_test/hook-runtime-endpoint-test.js +88 -88
- package/_site/_test/hook-status-repair-api-test.js +152 -152
- package/_site/_test/hook-trigger-test.js +85 -85
- package/_site/_test/import-preflight-api-test.js +234 -233
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +171 -171
- package/_site/_test/integration-capture-setup-test.js +1 -1
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/knowledge-language-control-test.js +170 -170
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
- package/_site/_test/knowledge-query-test.js +54 -54
- package/_site/_test/knowledge-retrieval-service-test.js +77 -77
- package/_site/_test/knowledge-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/legacy-forward-compat-test.js +257 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -363
- package/_site/_test/logging-api-test.js +89 -89
- package/_site/_test/logging-sse-no-gap-test.js +88 -88
- package/_site/_test/logging-ui-test.js +106 -104
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/module-artifact-integrity-test.js +27 -0
- package/_site/_test/module-boundary-test.js +30 -0
- package/_site/_test/module-bridge-eventbridge-test.js +7 -0
- package/_site/_test/module-model-configuration-test.js +43 -0
- package/_site/_test/module-process-lifecycle-test.js +24 -0
- package/_site/_test/module-stream-proxy-test.js +34 -0
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/p0-data-migration-characterization-test.js +37 -37
- package/_site/_test/p0-e2e-gate-test.js +347 -347
- package/_site/_test/packaged-ui-smoke-test.js +19 -10
- package/_site/_test/path-consistency-test.js +151 -151
- package/_site/_test/pending-sweep-test.js +9 -9
- package/_site/_test/post-commit-automation-test.js +87 -87
- package/_site/_test/product-diagnostics-ui-test.js +75 -0
- package/_site/_test/product-import-ui-test.js +92 -0
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-layout-v2-migration-test.js +127 -127
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- package/_site/_test/prompt-settings-test.js +115 -115
- package/_site/_test/protected-architecture-gate-test.js +122 -122
- package/_site/_test/refactor-characterization-test.js +36 -36
- package/_site/_test/release-version-sync-test.js +1 -1
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +159 -156
- package/_site/_test/server-runtime-migration-safety-test.js +27 -27
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/startup-analysis-disabled-test.js +10 -10
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/tracking-start-test.js +108 -108
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -56
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/workspace-ui-contract-test.js +28 -43
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-workspace.js +101 -101
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +5 -1
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/claude-cli-runner.js +1510 -1510
- package/_site/lib/commit-conversation-binder.js +234 -234
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +406 -406
- package/_site/lib/contracts.js +284 -279
- package/_site/lib/conversation-query-service.js +144 -144
- package/_site/lib/conversation-store.js +385 -385
- package/_site/lib/data-dir.js +74 -74
- package/_site/lib/data-state-classifier.js +40 -40
- package/_site/lib/engine-cutover.js +97 -0
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +21 -21
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1172 -1172
- package/_site/lib/hook-manager.js +203 -202
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-installer.js +907 -907
- package/_site/lib/kb-framework.js +176 -176
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-tool-runtime.js +359 -359
- package/_site/lib/legacy-data-manifest.js +26 -26
- package/_site/lib/llm-client.js +161 -161
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/migration-service.js +442 -442
- package/_site/lib/module-bridge.js +198 -33
- package/_site/lib/post-commit-automation.js +87 -87
- package/_site/lib/project-lifecycle-service.js +519 -497
- package/_site/lib/project-store.js +268 -262
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-endpoint.js +155 -155
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1590 -1520
- package/_site/lib/settings-store.js +133 -113
- package/_site/lib/storage-layout.js +162 -162
- package/_site/lib/structured-logger.js +574 -574
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +157 -156
- package/_site/scripts/install-module-candidates.js +69 -0
- package/_site/scripts/pack-module-candidates.js +72 -0
- package/_site/scripts/sync-release-version.js +2 -2
- package/_site/scripts/vendor-modules.js +47 -0
- package/_site/scripts/verify-module-runtime.js +45 -0
- package/_site/scripts/verify-product-runtime.js +34 -0
- package/bin/project-knowledge-mcp.js +194 -194
- package/docs/README.zh-CN.md +34 -25
- package/docs/project-registry-schema.md +22 -22
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +220 -0
- package/package.json +13 -10
- 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/project-readme.md +34 -34
- package/ui/favicon.svg +38 -38
- package/ui/index.html +51 -149
- package/ui/product.css +9 -0
- package/ui/product.js +386 -0
- package/vendor-manifest.json +46 -0
- package/_site/_test/import-ui-flow-test.js +0 -171
- package/_site/_test/project-control-panel-task14-test.js +0 -63
- package/_site/_test/task15-20-ui-flow-test.js +0 -148
- package/_site/_test/ui-i18n-toggle-test.js +0 -114
- package/_site/_test/ui-smoke-test.js +0 -73
- package/ui/app.css +0 -58
- package/ui/app.js +0 -607
- package/ui/i18n.js +0 -146
package/_site/lib/scanner.js
CHANGED
|
@@ -1,298 +1,298 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { TextDecoder } = require('util');
|
|
5
|
-
const { execGit } = require('./git-runner');
|
|
6
|
-
const { DomainError } = require('./contracts');
|
|
7
|
-
const { ExactPatchEvidenceBundle } = require('./evidence-bundle');
|
|
8
|
-
|
|
9
|
-
const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
10
|
-
const DEFAULT_MAX_PATCH_BYTES = 2 * 1024 * 1024;
|
|
11
|
-
|
|
12
|
-
function sha256(value) {
|
|
13
|
-
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function validateSha(value, name = 'commit') {
|
|
17
|
-
const sha = String(value || '').trim().toLowerCase();
|
|
18
|
-
if (!/^[0-9a-f]{40}$/.test(sha)) throw new DomainError('INVALID_ARGUMENT', `${name} must be a full Git commit SHA.`);
|
|
19
|
-
return sha;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function parseNameStatus(text) {
|
|
23
|
-
return String(text || '').split(/\r?\n/).filter(Boolean).map(line => {
|
|
24
|
-
const columns = line.split('\t');
|
|
25
|
-
const status = columns.shift() || '';
|
|
26
|
-
const renamed = /^[RC]/.test(status) && columns.length >= 2;
|
|
27
|
-
return {
|
|
28
|
-
status,
|
|
29
|
-
path: renamed ? columns[1] : columns[0] || '',
|
|
30
|
-
oldPath: renamed ? columns[0] : null,
|
|
31
|
-
binary: false,
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function applyNumstat(files, text) {
|
|
37
|
-
const byPath = new Map(files.map(file => [file.path, file]));
|
|
38
|
-
for (const line of String(text || '').split(/\r?\n/).filter(Boolean)) {
|
|
39
|
-
const [added, deleted, ...nameParts] = line.split('\t');
|
|
40
|
-
const name = nameParts[nameParts.length - 1] || '';
|
|
41
|
-
const file = byPath.get(name);
|
|
42
|
-
if (!file) continue;
|
|
43
|
-
file.binary = added === '-' && deleted === '-';
|
|
44
|
-
file.added = added === '-' ? null : Number(added);
|
|
45
|
-
file.deleted = deleted === '-' ? null : Number(deleted);
|
|
46
|
-
}
|
|
47
|
-
return files;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
class TrustedGitReader {
|
|
51
|
-
constructor(options = {}) {
|
|
52
|
-
this.execGit = options.execGit || execGit;
|
|
53
|
-
this.maxPatchBytes = Number(options.maxPatchBytes || DEFAULT_MAX_PATCH_BYTES);
|
|
54
|
-
this.evidenceBundle = options.evidenceBundle || new ExactPatchEvidenceBundle(options);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async run(repoPath, args, timeoutMs = 30_000) {
|
|
58
|
-
const result = await this.execGit(repoPath, args, timeoutMs);
|
|
59
|
-
if (!result || !result.ok) {
|
|
60
|
-
throw new DomainError('INVALID_ARGUMENT', 'Trusted Git read failed.', {
|
|
61
|
-
status: 409,
|
|
62
|
-
details: { args: args.slice(0, 4), code: result && result.code, error: String(result && (result.stderr || result.error) || '').slice(0, 1000) },
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return String(result.stdout || '');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async runBuffer(repoPath, args, timeoutMs = 30_000) {
|
|
69
|
-
const result = await this.execGit(repoPath, args, timeoutMs);
|
|
70
|
-
if (!result || !result.ok) {
|
|
71
|
-
throw new DomainError('INVALID_ARGUMENT', 'Trusted Git read failed.', {
|
|
72
|
-
status: 409,
|
|
73
|
-
details: { args: args.slice(0, 4), code: result && result.code, error: String(result && (result.stderr || result.error) || '').slice(0, 1000) },
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
return Buffer.isBuffer(result.stdoutBuffer) ? result.stdoutBuffer : Buffer.from(String(result.stdout || ''), 'utf8');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async isRepository(repoPath) {
|
|
80
|
-
if (!repoPath || !fs.existsSync(repoPath)) return false;
|
|
81
|
-
const result = await this.execGit(repoPath, ['rev-parse', '--is-inside-work-tree'], 10_000);
|
|
82
|
-
return Boolean(result && result.ok && String(result.stdout || '').trim() === 'true');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async head(repoPath) {
|
|
86
|
-
const result = await this.execGit(repoPath, ['rev-parse', '--verify', 'HEAD'], 10_000);
|
|
87
|
-
if (!result || !result.ok) return null;
|
|
88
|
-
const sha = String(result.stdout || '').trim().toLowerCase();
|
|
89
|
-
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async branch(repoPath) {
|
|
93
|
-
const result = await this.execGit(repoPath, ['symbolic-ref', '--quiet', '--short', 'HEAD'], 10_000);
|
|
94
|
-
return result && result.ok ? String(result.stdout || '').trim() : '';
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async isAncestor(repoPath, ancestor, commit) {
|
|
98
|
-
validateSha(ancestor, 'baseline');
|
|
99
|
-
validateSha(commit, 'HEAD');
|
|
100
|
-
const result = await this.execGit(repoPath, ['merge-base', '--is-ancestor', ancestor, commit], 10_000);
|
|
101
|
-
return Boolean(result && result.ok);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async listCommits(repoPath, baseline, head) {
|
|
105
|
-
validateSha(head, 'HEAD');
|
|
106
|
-
const revision = baseline ? `${validateSha(baseline, 'baseline')}..${head}` : head;
|
|
107
|
-
const output = await this.run(repoPath, ['rev-list', '--reverse', '--topo-order', revision]);
|
|
108
|
-
return output.split(/\r?\n/).map(value => value.trim().toLowerCase()).filter(value => /^[0-9a-f]{40}$/.test(value));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async collectEvidence(repoPath, commitSha, options = {}) {
|
|
112
|
-
const commit = validateSha(commitSha);
|
|
113
|
-
const raw = await this.run(repoPath, ['show', '-s', '--date=iso-strict', '--format=%H%x00%P%x00%an%x00%aI%x00%s', commit]);
|
|
114
|
-
const [resolved, parentsRaw, author, date, ...subjectParts] = raw.replace(/\r?\n$/, '').split('\u0000');
|
|
115
|
-
const parents = String(parentsRaw || '').split(/\s+/).filter(Boolean).map(parent => validateSha(parent, 'parent'));
|
|
116
|
-
const nameStatus = await this.run(repoPath, ['diff-tree', '--root', '--find-renames', '--name-status', '-r', '--no-commit-id', commit]);
|
|
117
|
-
const numstat = await this.run(repoPath, ['diff-tree', '--root', '--find-renames', '--numstat', '-r', '--no-commit-id', commit]);
|
|
118
|
-
const files = applyNumstat(parseNameStatus(nameStatus), numstat);
|
|
119
|
-
const base = parents.length ? parents[0] : EMPTY_TREE_SHA;
|
|
120
|
-
const patchBuffer = await this.runBuffer(repoPath, ['diff', '--no-ext-diff', '--binary', '--find-renames', '--unified=3', base, commit], options.patchTimeoutMs || 60_000);
|
|
121
|
-
const patchBytes = patchBuffer.length;
|
|
122
|
-
const maxPatchBytes = Number(options.maxPatchBytes || this.maxPatchBytes);
|
|
123
|
-
let patch = null;
|
|
124
|
-
try { patch = new TextDecoder('utf-8', { fatal: true }).decode(patchBuffer); } catch { patch = null; }
|
|
125
|
-
const patchChunked = patchBytes > maxPatchBytes || patch == null;
|
|
126
|
-
const branch = options.branch != null ? String(options.branch) : await this.branch(repoPath);
|
|
127
|
-
const metadata = {
|
|
128
|
-
commitSha: resolved || commit,
|
|
129
|
-
parents,
|
|
130
|
-
author: author || '',
|
|
131
|
-
date: date || '',
|
|
132
|
-
subject: subjectParts.join('\u0000').trim(),
|
|
133
|
-
branch,
|
|
134
|
-
patchBase: base,
|
|
135
|
-
patchMode: parents.length > 1 ? 'merge-first-parent' : parents.length === 0 ? 'root-empty-tree' : 'parent',
|
|
136
|
-
};
|
|
137
|
-
const patchHash = sha256(patchBuffer);
|
|
138
|
-
if (patchChunked && !options.evidenceRoot) {
|
|
139
|
-
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Large patch requires an exact evidence bundle root.', { status: 500 });
|
|
140
|
-
}
|
|
141
|
-
const evidenceBundle = options.evidenceRoot ? this.evidenceBundle.build({
|
|
142
|
-
evidenceRoot: options.evidenceRoot,
|
|
143
|
-
patch: patchBuffer,
|
|
144
|
-
patchHash,
|
|
145
|
-
commit: metadata,
|
|
146
|
-
files,
|
|
147
|
-
}) : null;
|
|
148
|
-
const evidenceHash = sha256(JSON.stringify({
|
|
149
|
-
metadata,
|
|
150
|
-
files,
|
|
151
|
-
patchHash,
|
|
152
|
-
patchBytes,
|
|
153
|
-
evidenceManifestHash: evidenceBundle && evidenceBundle.manifestHash || null,
|
|
154
|
-
}));
|
|
155
|
-
return {
|
|
156
|
-
schema: 'commit-evidence/v1',
|
|
157
|
-
...metadata,
|
|
158
|
-
files,
|
|
159
|
-
patch: patchChunked ? null : patch,
|
|
160
|
-
patchHash,
|
|
161
|
-
patchBytes,
|
|
162
|
-
patchInline: !patchChunked,
|
|
163
|
-
patchChunked,
|
|
164
|
-
patchOmitted: false,
|
|
165
|
-
patchLimitBytes: maxPatchBytes,
|
|
166
|
-
omittedReason: null,
|
|
167
|
-
evidenceBundle,
|
|
168
|
-
evidenceHash,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
verifyEvidence(evidence) {
|
|
173
|
-
if (!evidence || !evidence.patchHash || !Number.isInteger(evidence.patchBytes)) {
|
|
174
|
-
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Commit evidence is incomplete.', { status: 500 });
|
|
175
|
-
}
|
|
176
|
-
if (evidence.evidenceBundle) {
|
|
177
|
-
return this.evidenceBundle.verify(evidence.evidenceBundle.root, {
|
|
178
|
-
expectedPatchHash: evidence.patchHash,
|
|
179
|
-
expectedBytes: evidence.patchBytes,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
if (evidence.patchChunked) {
|
|
183
|
-
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Chunked patch has no frozen evidence bundle.', { status: 500 });
|
|
184
|
-
}
|
|
185
|
-
if (sha256(evidence.patch || '') !== evidence.patchHash) {
|
|
186
|
-
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Inline patch hash does not match.', { status: 500 });
|
|
187
|
-
}
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
class CommitScanner {
|
|
193
|
-
constructor(options = {}) {
|
|
194
|
-
this.git = options.gitReader || new TrustedGitReader(options);
|
|
195
|
-
this.batchSize = Math.max(1, Number(options.batchSize || 200));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async scan(config, state, options = {}) {
|
|
199
|
-
const repoPath = config && config.repoPath;
|
|
200
|
-
if (!repoPath || !await this.git.isRepository(repoPath)) {
|
|
201
|
-
return { status: 'not-git', head: null, baseline: null, commits: [], continuation: null, error: 'repoPath is not an available Git worktree' };
|
|
202
|
-
}
|
|
203
|
-
const head = await this.git.head(repoPath);
|
|
204
|
-
if (!head) {
|
|
205
|
-
return { status: 'empty-repo', head: null, baseline: null, commits: [], continuation: null, branch: await this.git.branch(repoPath) };
|
|
206
|
-
}
|
|
207
|
-
const branch = await this.git.branch(repoPath);
|
|
208
|
-
const last = state && state.lastAnalyzedCommit || null;
|
|
209
|
-
const tracking = state && state.trackingStartCommit || null;
|
|
210
|
-
const baseline = last || tracking || null;
|
|
211
|
-
if (!baseline && (!state || state.trackingMode !== 'empty-repo')) {
|
|
212
|
-
return { status: 'establish-tracking', head, baseline: null, commits: [], continuation: null, branch };
|
|
213
|
-
}
|
|
214
|
-
if (baseline && !await this.git.isAncestor(repoPath, baseline, head)) {
|
|
215
|
-
return { status: 'history-diverged', head, baseline, commits: [], continuation: null, branch, error: 'tracking baseline is not an ancestor of HEAD' };
|
|
216
|
-
}
|
|
217
|
-
const all = await this.git.listCommits(repoPath, baseline, head);
|
|
218
|
-
const batchSize = Math.max(1, Number(options.batchSize || this.batchSize));
|
|
219
|
-
const commits = all.slice(0, batchSize);
|
|
220
|
-
return {
|
|
221
|
-
status: 'ok',
|
|
222
|
-
head,
|
|
223
|
-
baseline,
|
|
224
|
-
branch,
|
|
225
|
-
commits,
|
|
226
|
-
pendingCount: all.length,
|
|
227
|
-
continuation: all.length > commits.length ? { remaining: all.length - commits.length, after: commits[commits.length - 1] } : null,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
collectEvidence(config, commitSha, options = {}) {
|
|
232
|
-
return this.git.collectEvidence(config.repoPath, commitSha, options);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
verifyEvidence(evidence) {
|
|
236
|
-
return this.git.verifyEvidence(evidence);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Transitional read-only adapter for callers migrated in T10.
|
|
241
|
-
async function scanProject(project, options = {}) {
|
|
242
|
-
const scanner = options.scanner || new CommitScanner(options);
|
|
243
|
-
const config = { repoPath: project && (project.repoPath || project.gitPath || project.localPath) };
|
|
244
|
-
const state = {
|
|
245
|
-
lastAnalyzedCommit: project && project.lastAnalyzedCommit || null,
|
|
246
|
-
trackingStartCommit: project && project.trackingStartCommit || null,
|
|
247
|
-
trackingMode: project && project.trackingMode || 'normal',
|
|
248
|
-
};
|
|
249
|
-
const scan = await scanner.scan(config, state, options);
|
|
250
|
-
const result = {
|
|
251
|
-
slug: project && (project.projectId || project.slug),
|
|
252
|
-
repoStatus: scan.status === 'empty-repo' ? 'empty' : scan.status === 'not-git' ? 'not-git' : scan.status === 'history-diverged' ? 'diverged' : 'ok',
|
|
253
|
-
headCommit: scan.head,
|
|
254
|
-
lastAnalyzedCommit: state.lastAnalyzedCommit,
|
|
255
|
-
trackingStartCommit: scan.status === 'establish-tracking' ? scan.head : state.trackingStartCommit,
|
|
256
|
-
pendingCount: scan.commits.length,
|
|
257
|
-
mode: scan.status === 'establish-tracking' ? 'tracking-start' : state.lastAnalyzedCommit ? 'incremental' : state.trackingMode === 'empty-repo' ? 'empty-repo-first-commit' : 'tracked',
|
|
258
|
-
range: scan.baseline && scan.head ? `${scan.baseline}..${scan.head}` : null,
|
|
259
|
-
commits: [],
|
|
260
|
-
continuation: scan.continuation,
|
|
261
|
-
error: scan.error || null,
|
|
262
|
-
};
|
|
263
|
-
for (const commitSha of scan.commits) {
|
|
264
|
-
const evidence = await scanner.collectEvidence(config, commitSha, { branch: scan.branch });
|
|
265
|
-
result.commits.push({
|
|
266
|
-
hash: commitSha,
|
|
267
|
-
short: commitSha.slice(0, 7),
|
|
268
|
-
date: evidence.date,
|
|
269
|
-
author: evidence.author,
|
|
270
|
-
subject: evidence.subject,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function applyScanResult(project, scan) {
|
|
277
|
-
project.headCommit = scan.headCommit;
|
|
278
|
-
project.repoStatus = scan.repoStatus;
|
|
279
|
-
if (!project.trackingStartCommit && scan.trackingStartCommit) project.trackingStartCommit = scan.trackingStartCommit;
|
|
280
|
-
project.lastSeenCommit = scan.headCommit || project.lastSeenCommit;
|
|
281
|
-
project.lastScanAt = new Date().toISOString();
|
|
282
|
-
project.lastScanPendingCount = scan.pendingCount;
|
|
283
|
-
project.lastScanMode = scan.mode;
|
|
284
|
-
project.lastScanError = scan.error || null;
|
|
285
|
-
return project;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
module.exports = {
|
|
289
|
-
EMPTY_TREE_SHA,
|
|
290
|
-
DEFAULT_MAX_PATCH_BYTES,
|
|
291
|
-
TrustedGitReader,
|
|
292
|
-
CommitScanner,
|
|
293
|
-
scanProject,
|
|
294
|
-
applyScanResult,
|
|
295
|
-
parseNameStatus,
|
|
296
|
-
sha256,
|
|
297
|
-
validateSha,
|
|
298
|
-
};
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { TextDecoder } = require('util');
|
|
5
|
+
const { execGit } = require('./git-runner');
|
|
6
|
+
const { DomainError } = require('./contracts');
|
|
7
|
+
const { ExactPatchEvidenceBundle } = require('./evidence-bundle');
|
|
8
|
+
|
|
9
|
+
const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
10
|
+
const DEFAULT_MAX_PATCH_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
function sha256(value) {
|
|
13
|
+
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateSha(value, name = 'commit') {
|
|
17
|
+
const sha = String(value || '').trim().toLowerCase();
|
|
18
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) throw new DomainError('INVALID_ARGUMENT', `${name} must be a full Git commit SHA.`);
|
|
19
|
+
return sha;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseNameStatus(text) {
|
|
23
|
+
return String(text || '').split(/\r?\n/).filter(Boolean).map(line => {
|
|
24
|
+
const columns = line.split('\t');
|
|
25
|
+
const status = columns.shift() || '';
|
|
26
|
+
const renamed = /^[RC]/.test(status) && columns.length >= 2;
|
|
27
|
+
return {
|
|
28
|
+
status,
|
|
29
|
+
path: renamed ? columns[1] : columns[0] || '',
|
|
30
|
+
oldPath: renamed ? columns[0] : null,
|
|
31
|
+
binary: false,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyNumstat(files, text) {
|
|
37
|
+
const byPath = new Map(files.map(file => [file.path, file]));
|
|
38
|
+
for (const line of String(text || '').split(/\r?\n/).filter(Boolean)) {
|
|
39
|
+
const [added, deleted, ...nameParts] = line.split('\t');
|
|
40
|
+
const name = nameParts[nameParts.length - 1] || '';
|
|
41
|
+
const file = byPath.get(name);
|
|
42
|
+
if (!file) continue;
|
|
43
|
+
file.binary = added === '-' && deleted === '-';
|
|
44
|
+
file.added = added === '-' ? null : Number(added);
|
|
45
|
+
file.deleted = deleted === '-' ? null : Number(deleted);
|
|
46
|
+
}
|
|
47
|
+
return files;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class TrustedGitReader {
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.execGit = options.execGit || execGit;
|
|
53
|
+
this.maxPatchBytes = Number(options.maxPatchBytes || DEFAULT_MAX_PATCH_BYTES);
|
|
54
|
+
this.evidenceBundle = options.evidenceBundle || new ExactPatchEvidenceBundle(options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async run(repoPath, args, timeoutMs = 30_000) {
|
|
58
|
+
const result = await this.execGit(repoPath, args, timeoutMs);
|
|
59
|
+
if (!result || !result.ok) {
|
|
60
|
+
throw new DomainError('INVALID_ARGUMENT', 'Trusted Git read failed.', {
|
|
61
|
+
status: 409,
|
|
62
|
+
details: { args: args.slice(0, 4), code: result && result.code, error: String(result && (result.stderr || result.error) || '').slice(0, 1000) },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return String(result.stdout || '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async runBuffer(repoPath, args, timeoutMs = 30_000) {
|
|
69
|
+
const result = await this.execGit(repoPath, args, timeoutMs);
|
|
70
|
+
if (!result || !result.ok) {
|
|
71
|
+
throw new DomainError('INVALID_ARGUMENT', 'Trusted Git read failed.', {
|
|
72
|
+
status: 409,
|
|
73
|
+
details: { args: args.slice(0, 4), code: result && result.code, error: String(result && (result.stderr || result.error) || '').slice(0, 1000) },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return Buffer.isBuffer(result.stdoutBuffer) ? result.stdoutBuffer : Buffer.from(String(result.stdout || ''), 'utf8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async isRepository(repoPath) {
|
|
80
|
+
if (!repoPath || !fs.existsSync(repoPath)) return false;
|
|
81
|
+
const result = await this.execGit(repoPath, ['rev-parse', '--is-inside-work-tree'], 10_000);
|
|
82
|
+
return Boolean(result && result.ok && String(result.stdout || '').trim() === 'true');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async head(repoPath) {
|
|
86
|
+
const result = await this.execGit(repoPath, ['rev-parse', '--verify', 'HEAD'], 10_000);
|
|
87
|
+
if (!result || !result.ok) return null;
|
|
88
|
+
const sha = String(result.stdout || '').trim().toLowerCase();
|
|
89
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async branch(repoPath) {
|
|
93
|
+
const result = await this.execGit(repoPath, ['symbolic-ref', '--quiet', '--short', 'HEAD'], 10_000);
|
|
94
|
+
return result && result.ok ? String(result.stdout || '').trim() : '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async isAncestor(repoPath, ancestor, commit) {
|
|
98
|
+
validateSha(ancestor, 'baseline');
|
|
99
|
+
validateSha(commit, 'HEAD');
|
|
100
|
+
const result = await this.execGit(repoPath, ['merge-base', '--is-ancestor', ancestor, commit], 10_000);
|
|
101
|
+
return Boolean(result && result.ok);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async listCommits(repoPath, baseline, head) {
|
|
105
|
+
validateSha(head, 'HEAD');
|
|
106
|
+
const revision = baseline ? `${validateSha(baseline, 'baseline')}..${head}` : head;
|
|
107
|
+
const output = await this.run(repoPath, ['rev-list', '--reverse', '--topo-order', revision]);
|
|
108
|
+
return output.split(/\r?\n/).map(value => value.trim().toLowerCase()).filter(value => /^[0-9a-f]{40}$/.test(value));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async collectEvidence(repoPath, commitSha, options = {}) {
|
|
112
|
+
const commit = validateSha(commitSha);
|
|
113
|
+
const raw = await this.run(repoPath, ['show', '-s', '--date=iso-strict', '--format=%H%x00%P%x00%an%x00%aI%x00%s', commit]);
|
|
114
|
+
const [resolved, parentsRaw, author, date, ...subjectParts] = raw.replace(/\r?\n$/, '').split('\u0000');
|
|
115
|
+
const parents = String(parentsRaw || '').split(/\s+/).filter(Boolean).map(parent => validateSha(parent, 'parent'));
|
|
116
|
+
const nameStatus = await this.run(repoPath, ['diff-tree', '--root', '--find-renames', '--name-status', '-r', '--no-commit-id', commit]);
|
|
117
|
+
const numstat = await this.run(repoPath, ['diff-tree', '--root', '--find-renames', '--numstat', '-r', '--no-commit-id', commit]);
|
|
118
|
+
const files = applyNumstat(parseNameStatus(nameStatus), numstat);
|
|
119
|
+
const base = parents.length ? parents[0] : EMPTY_TREE_SHA;
|
|
120
|
+
const patchBuffer = await this.runBuffer(repoPath, ['diff', '--no-ext-diff', '--binary', '--find-renames', '--unified=3', base, commit], options.patchTimeoutMs || 60_000);
|
|
121
|
+
const patchBytes = patchBuffer.length;
|
|
122
|
+
const maxPatchBytes = Number(options.maxPatchBytes || this.maxPatchBytes);
|
|
123
|
+
let patch = null;
|
|
124
|
+
try { patch = new TextDecoder('utf-8', { fatal: true }).decode(patchBuffer); } catch { patch = null; }
|
|
125
|
+
const patchChunked = patchBytes > maxPatchBytes || patch == null;
|
|
126
|
+
const branch = options.branch != null ? String(options.branch) : await this.branch(repoPath);
|
|
127
|
+
const metadata = {
|
|
128
|
+
commitSha: resolved || commit,
|
|
129
|
+
parents,
|
|
130
|
+
author: author || '',
|
|
131
|
+
date: date || '',
|
|
132
|
+
subject: subjectParts.join('\u0000').trim(),
|
|
133
|
+
branch,
|
|
134
|
+
patchBase: base,
|
|
135
|
+
patchMode: parents.length > 1 ? 'merge-first-parent' : parents.length === 0 ? 'root-empty-tree' : 'parent',
|
|
136
|
+
};
|
|
137
|
+
const patchHash = sha256(patchBuffer);
|
|
138
|
+
if (patchChunked && !options.evidenceRoot) {
|
|
139
|
+
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Large patch requires an exact evidence bundle root.', { status: 500 });
|
|
140
|
+
}
|
|
141
|
+
const evidenceBundle = options.evidenceRoot ? this.evidenceBundle.build({
|
|
142
|
+
evidenceRoot: options.evidenceRoot,
|
|
143
|
+
patch: patchBuffer,
|
|
144
|
+
patchHash,
|
|
145
|
+
commit: metadata,
|
|
146
|
+
files,
|
|
147
|
+
}) : null;
|
|
148
|
+
const evidenceHash = sha256(JSON.stringify({
|
|
149
|
+
metadata,
|
|
150
|
+
files,
|
|
151
|
+
patchHash,
|
|
152
|
+
patchBytes,
|
|
153
|
+
evidenceManifestHash: evidenceBundle && evidenceBundle.manifestHash || null,
|
|
154
|
+
}));
|
|
155
|
+
return {
|
|
156
|
+
schema: 'commit-evidence/v1',
|
|
157
|
+
...metadata,
|
|
158
|
+
files,
|
|
159
|
+
patch: patchChunked ? null : patch,
|
|
160
|
+
patchHash,
|
|
161
|
+
patchBytes,
|
|
162
|
+
patchInline: !patchChunked,
|
|
163
|
+
patchChunked,
|
|
164
|
+
patchOmitted: false,
|
|
165
|
+
patchLimitBytes: maxPatchBytes,
|
|
166
|
+
omittedReason: null,
|
|
167
|
+
evidenceBundle,
|
|
168
|
+
evidenceHash,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
verifyEvidence(evidence) {
|
|
173
|
+
if (!evidence || !evidence.patchHash || !Number.isInteger(evidence.patchBytes)) {
|
|
174
|
+
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Commit evidence is incomplete.', { status: 500 });
|
|
175
|
+
}
|
|
176
|
+
if (evidence.evidenceBundle) {
|
|
177
|
+
return this.evidenceBundle.verify(evidence.evidenceBundle.root, {
|
|
178
|
+
expectedPatchHash: evidence.patchHash,
|
|
179
|
+
expectedBytes: evidence.patchBytes,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (evidence.patchChunked) {
|
|
183
|
+
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Chunked patch has no frozen evidence bundle.', { status: 500 });
|
|
184
|
+
}
|
|
185
|
+
if (sha256(evidence.patch || '') !== evidence.patchHash) {
|
|
186
|
+
throw new DomainError('EVIDENCE_INTEGRITY_FAILED', 'Inline patch hash does not match.', { status: 500 });
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class CommitScanner {
|
|
193
|
+
constructor(options = {}) {
|
|
194
|
+
this.git = options.gitReader || new TrustedGitReader(options);
|
|
195
|
+
this.batchSize = Math.max(1, Number(options.batchSize || 200));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async scan(config, state, options = {}) {
|
|
199
|
+
const repoPath = config && config.repoPath;
|
|
200
|
+
if (!repoPath || !await this.git.isRepository(repoPath)) {
|
|
201
|
+
return { status: 'not-git', head: null, baseline: null, commits: [], continuation: null, error: 'repoPath is not an available Git worktree' };
|
|
202
|
+
}
|
|
203
|
+
const head = await this.git.head(repoPath);
|
|
204
|
+
if (!head) {
|
|
205
|
+
return { status: 'empty-repo', head: null, baseline: null, commits: [], continuation: null, branch: await this.git.branch(repoPath) };
|
|
206
|
+
}
|
|
207
|
+
const branch = await this.git.branch(repoPath);
|
|
208
|
+
const last = state && state.lastAnalyzedCommit || null;
|
|
209
|
+
const tracking = state && state.trackingStartCommit || null;
|
|
210
|
+
const baseline = last || tracking || null;
|
|
211
|
+
if (!baseline && (!state || state.trackingMode !== 'empty-repo')) {
|
|
212
|
+
return { status: 'establish-tracking', head, baseline: null, commits: [], continuation: null, branch };
|
|
213
|
+
}
|
|
214
|
+
if (baseline && !await this.git.isAncestor(repoPath, baseline, head)) {
|
|
215
|
+
return { status: 'history-diverged', head, baseline, commits: [], continuation: null, branch, error: 'tracking baseline is not an ancestor of HEAD' };
|
|
216
|
+
}
|
|
217
|
+
const all = await this.git.listCommits(repoPath, baseline, head);
|
|
218
|
+
const batchSize = Math.max(1, Number(options.batchSize || this.batchSize));
|
|
219
|
+
const commits = all.slice(0, batchSize);
|
|
220
|
+
return {
|
|
221
|
+
status: 'ok',
|
|
222
|
+
head,
|
|
223
|
+
baseline,
|
|
224
|
+
branch,
|
|
225
|
+
commits,
|
|
226
|
+
pendingCount: all.length,
|
|
227
|
+
continuation: all.length > commits.length ? { remaining: all.length - commits.length, after: commits[commits.length - 1] } : null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
collectEvidence(config, commitSha, options = {}) {
|
|
232
|
+
return this.git.collectEvidence(config.repoPath, commitSha, options);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
verifyEvidence(evidence) {
|
|
236
|
+
return this.git.verifyEvidence(evidence);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Transitional read-only adapter for callers migrated in T10.
|
|
241
|
+
async function scanProject(project, options = {}) {
|
|
242
|
+
const scanner = options.scanner || new CommitScanner(options);
|
|
243
|
+
const config = { repoPath: project && (project.repoPath || project.gitPath || project.localPath) };
|
|
244
|
+
const state = {
|
|
245
|
+
lastAnalyzedCommit: project && project.lastAnalyzedCommit || null,
|
|
246
|
+
trackingStartCommit: project && project.trackingStartCommit || null,
|
|
247
|
+
trackingMode: project && project.trackingMode || 'normal',
|
|
248
|
+
};
|
|
249
|
+
const scan = await scanner.scan(config, state, options);
|
|
250
|
+
const result = {
|
|
251
|
+
slug: project && (project.projectId || project.slug),
|
|
252
|
+
repoStatus: scan.status === 'empty-repo' ? 'empty' : scan.status === 'not-git' ? 'not-git' : scan.status === 'history-diverged' ? 'diverged' : 'ok',
|
|
253
|
+
headCommit: scan.head,
|
|
254
|
+
lastAnalyzedCommit: state.lastAnalyzedCommit,
|
|
255
|
+
trackingStartCommit: scan.status === 'establish-tracking' ? scan.head : state.trackingStartCommit,
|
|
256
|
+
pendingCount: scan.commits.length,
|
|
257
|
+
mode: scan.status === 'establish-tracking' ? 'tracking-start' : state.lastAnalyzedCommit ? 'incremental' : state.trackingMode === 'empty-repo' ? 'empty-repo-first-commit' : 'tracked',
|
|
258
|
+
range: scan.baseline && scan.head ? `${scan.baseline}..${scan.head}` : null,
|
|
259
|
+
commits: [],
|
|
260
|
+
continuation: scan.continuation,
|
|
261
|
+
error: scan.error || null,
|
|
262
|
+
};
|
|
263
|
+
for (const commitSha of scan.commits) {
|
|
264
|
+
const evidence = await scanner.collectEvidence(config, commitSha, { branch: scan.branch });
|
|
265
|
+
result.commits.push({
|
|
266
|
+
hash: commitSha,
|
|
267
|
+
short: commitSha.slice(0, 7),
|
|
268
|
+
date: evidence.date,
|
|
269
|
+
author: evidence.author,
|
|
270
|
+
subject: evidence.subject,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function applyScanResult(project, scan) {
|
|
277
|
+
project.headCommit = scan.headCommit;
|
|
278
|
+
project.repoStatus = scan.repoStatus;
|
|
279
|
+
if (!project.trackingStartCommit && scan.trackingStartCommit) project.trackingStartCommit = scan.trackingStartCommit;
|
|
280
|
+
project.lastSeenCommit = scan.headCommit || project.lastSeenCommit;
|
|
281
|
+
project.lastScanAt = new Date().toISOString();
|
|
282
|
+
project.lastScanPendingCount = scan.pendingCount;
|
|
283
|
+
project.lastScanMode = scan.mode;
|
|
284
|
+
project.lastScanError = scan.error || null;
|
|
285
|
+
return project;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
EMPTY_TREE_SHA,
|
|
290
|
+
DEFAULT_MAX_PATCH_BYTES,
|
|
291
|
+
TrustedGitReader,
|
|
292
|
+
CommitScanner,
|
|
293
|
+
scanProject,
|
|
294
|
+
applyScanResult,
|
|
295
|
+
parseNameStatus,
|
|
296
|
+
sha256,
|
|
297
|
+
validateSha,
|
|
298
|
+
};
|