@sanqianx/project-knowledge 4.7.0 → 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 -799
- 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/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/codex-conversation-projection-test.js +2 -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/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-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 +11 -9
- 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 +1 -0
- 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 -9
- 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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { validateProjectId } = require('./runtime/contracts');
|
|
7
|
+
const { RuntimeLayout } = require('./runtime/layout');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 单文件 JSON 状态层(<data>/engine-state.json):注册项目、分析运行、事件流。
|
|
11
|
+
* 任务、消费游标与受控写入元数据一起原子落盘,运行期凭据不进入本层。
|
|
12
|
+
*/
|
|
13
|
+
class EngineStore {
|
|
14
|
+
constructor(dataDir, options = {}) {
|
|
15
|
+
this.dataDir = dataDir;
|
|
16
|
+
this.file = path.join(dataDir, 'engine-state.json');
|
|
17
|
+
this.state = { schemaVersion: 2, projects: [], runs: [], events: [], cursor: 0, journalCursors: {}, memories: [], sharedScopes: [] };
|
|
18
|
+
if (!options.deferLoad) this.load();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
load() {
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(fs.readFileSync(this.file, 'utf8'));
|
|
24
|
+
if (raw && (raw.schemaVersion === undefined || raw.schemaVersion === 1)) {
|
|
25
|
+
const backup = path.join(this.dataDir, `legacy-debug-state-${crypto.randomUUID()}.json`);
|
|
26
|
+
require('./runtime/atomic-file').writeJsonAtomic(backup, raw);
|
|
27
|
+
this.state.legacyStateBackup = backup;
|
|
28
|
+
this.save();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (raw?.schemaVersion !== 2) throw new Error('Unsupported engine state');
|
|
32
|
+
if (!raw || !Array.isArray(raw.projects) || !Array.isArray(raw.runs) || !Array.isArray(raw.events) || !Number.isSafeInteger(raw.cursor) || raw.cursor < 0) throw new Error('Invalid engine state');
|
|
33
|
+
if (raw.memories && !Array.isArray(raw.memories) || raw.sharedScopes && !Array.isArray(raw.sharedScopes) || raw.journalCursors && (typeof raw.journalCursors !== 'object' || Array.isArray(raw.journalCursors))) throw new Error('Invalid engine projections');
|
|
34
|
+
const ids = new Set();
|
|
35
|
+
for (const project of raw.projects) {
|
|
36
|
+
validateProjectId(project.projectId);
|
|
37
|
+
if (ids.has(project.projectId) || !path.isAbsolute(project.repoPath || '') || !path.isAbsolute(project.knowledgePath || '') || !project.runtime?.analysis || !project.runtime?.index) throw new Error('Invalid project runtime');
|
|
38
|
+
ids.add(project.projectId);
|
|
39
|
+
}
|
|
40
|
+
const runIds = new Set();
|
|
41
|
+
for (const run of raw.runs) {
|
|
42
|
+
validateProjectId(run.projectId);
|
|
43
|
+
if (!/^run-[a-f0-9-]{36}$/.test(run.runId || '') || runIds.has(run.runId) || !/^[a-f0-9]{40}$/.test(run.commitSha || '') || !['queued', 'running', 'failed', 'succeeded', 'index-pending'].includes(run.state) || !Number.isSafeInteger(run.sequence) || run.sequence < 1) throw new Error('Invalid queued run');
|
|
44
|
+
runIds.add(run.runId);
|
|
45
|
+
}
|
|
46
|
+
for (const [journalId, sequence] of Object.entries(raw.journalCursors || {})) if (!/^(global|sha256:[a-f0-9]{64})$/.test(journalId) || !Number.isSafeInteger(sequence) || sequence < 0) throw new Error('Invalid journal cursor');
|
|
47
|
+
for (const scope of raw.sharedScopes || []) if (!scope || !['brain', 'domain'].includes(scope.kind) || !path.isAbsolute(scope.knowledgePath || '')) throw new Error('Invalid shared scope');
|
|
48
|
+
for (const memory of raw.memories || []) if (!memory || !['candidate', 'confirmed', 'ignored'].includes(memory.status) || typeof memory.text !== 'string' || !memory.evidence?.projectId || !/^[a-f0-9]{40}$/.test(memory.evidence.commitSha || '')) throw new Error('Invalid memory record');
|
|
49
|
+
this.state = { ...this.state, ...raw };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code !== 'ENOENT') throw Object.assign(new Error('引擎状态损坏,已保留原文件;请从备份恢复。'), { code: 'DATA_CORRUPT', cause: error });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
isEmpty() {
|
|
56
|
+
return this.state.projects.length === 0 && this.state.runs.length === 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
replace(next) {
|
|
60
|
+
this.state = next;
|
|
61
|
+
this.save();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
save() {
|
|
65
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
66
|
+
const tmp = this.file + '.tmp';
|
|
67
|
+
const fd = fs.openSync(tmp, 'w', 0o600);
|
|
68
|
+
try { fs.writeFileSync(fd, JSON.stringify(this.state, null, 2), 'utf8'); fs.fsyncSync(fd); }
|
|
69
|
+
finally { fs.closeSync(fd); }
|
|
70
|
+
fs.renameSync(tmp, this.file);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
listProjects() {
|
|
74
|
+
return this.state.projects.slice();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getProject(projectId) {
|
|
78
|
+
return this.state.projects.find(p => p.projectId === projectId) || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
addProject(input) {
|
|
82
|
+
validateProjectId(input.projectId);
|
|
83
|
+
const layout = new RuntimeLayout(this.dataDir);
|
|
84
|
+
if (!path.isAbsolute(input.repoPath || '') || !path.isAbsolute(input.knowledgePath || '')) throw Object.assign(new Error('代码目录和知识目录必须为绝对路径。'), { status: 400, code: 'INVALID_ARGUMENT' });
|
|
85
|
+
const existing = this.getProject(input.projectId);
|
|
86
|
+
if (input.knowledgeLanguage !== undefined && !['zh-CN', 'en-US'].includes(input.knowledgeLanguage)) throw Object.assign(new Error('知识语言无效。'), { status: 400, code: 'INVALID_ARGUMENT' });
|
|
87
|
+
if (this.state.runs.some(r => r.projectId === input.projectId && ['queued', 'running'].includes(r.state)) && existing
|
|
88
|
+
&& (!layout.pathsEqual(existing.repoPath, input.repoPath) || !layout.pathsEqual(existing.knowledgePath, input.knowledgePath))) throw Object.assign(new Error('运行中不能改变目录绑定。'), { status: 409, code: 'PROJECT_BUSY' });
|
|
89
|
+
for (const project of this.state.projects) {
|
|
90
|
+
if (project.projectId !== input.projectId && (layout.isPathInside(project.knowledgePath, input.knowledgePath, { realpath: true }) || layout.isPathInside(input.knowledgePath, project.knowledgePath, { realpath: true }))) throw Object.assign(new Error('知识目录与已登记项目冲突。'), { status: 409, code: 'KNOWLEDGE_PATH_CONFLICT' });
|
|
91
|
+
}
|
|
92
|
+
for (const scope of this.state.sharedScopes) {
|
|
93
|
+
if (layout.isPathInside(scope.knowledgePath, input.knowledgePath, { realpath: true }) || layout.isPathInside(input.knowledgePath, scope.knowledgePath, { realpath: true })) throw Object.assign(new Error('项目目录不能占用共享知识目录。'), { status: 409, code: 'KNOWLEDGE_PATH_CONFLICT' });
|
|
94
|
+
}
|
|
95
|
+
// Registration is a projection, not a second settings/credential store.
|
|
96
|
+
const fields = { projectId: input.projectId, name: input.name || input.projectId, repoPath: path.resolve(input.repoPath), knowledgePath: path.resolve(input.knowledgePath), brainId: input.brainId || existing?.brainId || 'brain-default', knowledgeLanguage: input.knowledgeLanguage || existing?.knowledgeLanguage || 'zh-CN', enabled: input.enabled !== false };
|
|
97
|
+
for (const key of ['brainId', 'domainId', 'journalId', 'vectorProjectId', 'profileId', 'trackingStartCommit']) {
|
|
98
|
+
if (input[key] !== undefined) fields[key] = input[key] === null ? null : String(input[key]);
|
|
99
|
+
}
|
|
100
|
+
if (input.relatedProjectIds) fields.relatedProjectIds = input.relatedProjectIds.map(validateProjectId);
|
|
101
|
+
if (Number.isSafeInteger(input.conversationBaselineCursor)) fields.conversationBaselineCursor = input.conversationBaselineCursor;
|
|
102
|
+
if (input.conversationBaselineCursors) fields.conversationBaselineCursors = Object.fromEntries(Object.entries(input.conversationBaselineCursors).map(([id, cursor]) => {
|
|
103
|
+
if (!/^(global|sha256:[a-f0-9]{64})$/.test(id) || !Number.isSafeInteger(cursor) || cursor < 0) throw Object.assign(new Error('会话跟踪基线无效。'), { status: 400, code: 'INVALID_ARGUMENT' });
|
|
104
|
+
return [id, cursor];
|
|
105
|
+
}));
|
|
106
|
+
if (input.repoIdentity) fields.repoIdentity = Object.fromEntries(['schema', 'workspaceId', 'workspaceRoot', 'commonDir', 'remote'].map(key => [key, input.repoIdentity[key] == null ? null : String(input.repoIdentity[key])]));
|
|
107
|
+
if (existing) {
|
|
108
|
+
const priorBaselines = { ...(existing.runtime.conversationBaselineCursors || {}) };
|
|
109
|
+
if (!Object.hasOwn(priorBaselines, 'global')) priorBaselines.global = existing.runtime.conversationBaselineCursor ?? 0;
|
|
110
|
+
for (const [journalId, cursor] of Object.entries(fields.conversationBaselineCursors || {})) if (!Object.hasOwn(priorBaselines, journalId)) priorBaselines[journalId] = cursor;
|
|
111
|
+
existing.runtime.conversationBaselineCursors = priorBaselines;
|
|
112
|
+
delete fields.conversationBaselineCursor;
|
|
113
|
+
delete fields.conversationBaselineCursors;
|
|
114
|
+
Object.assign(existing, fields, { updatedAt: new Date().toISOString() });
|
|
115
|
+
this.save();
|
|
116
|
+
return { project: existing, reused: true };
|
|
117
|
+
}
|
|
118
|
+
const project = {
|
|
119
|
+
...fields,
|
|
120
|
+
runtime: initialRuntime(input),
|
|
121
|
+
lastCommitSha: null,
|
|
122
|
+
lastRunAt: null,
|
|
123
|
+
createdAt: new Date().toISOString(),
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
this.state.projects.push(project);
|
|
127
|
+
this.save();
|
|
128
|
+
return { project, reused: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
removeProject(projectId) {
|
|
132
|
+
if (this.state.runs.some(r => r.projectId === projectId && ['queued', 'running'].includes(r.state))) throw Object.assign(new Error('请等待当前维护运行结束后再解绑。'), { status: 409, code: 'PROJECT_BUSY' });
|
|
133
|
+
const before = this.state.projects.length;
|
|
134
|
+
this.state.projects = this.state.projects.filter(p => p.projectId !== projectId);
|
|
135
|
+
this.save();
|
|
136
|
+
return this.state.projects.length < before;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
listRuns(projectId) {
|
|
140
|
+
const runs = projectId
|
|
141
|
+
? this.state.runs.filter(r => r.projectId === projectId)
|
|
142
|
+
: this.state.runs;
|
|
143
|
+
return runs.slice().sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getRun(runId) {
|
|
147
|
+
return this.state.runs.find(r => r.runId === runId) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
addRun(input) {
|
|
151
|
+
if (!/^[a-f0-9]{40}$/i.test(input.commitSha || '')) throw Object.assign(new Error('必须提供完整的真实提交 SHA。'), { status: 400, code: 'INVALID_ARGUMENT' });
|
|
152
|
+
const prior = this.state.runs.find(r => r.projectId === input.projectId && r.commitSha === input.commitSha.toLowerCase());
|
|
153
|
+
if (prior) return prior;
|
|
154
|
+
const run = {
|
|
155
|
+
runId: 'run-' + crypto.randomUUID(),
|
|
156
|
+
projectId: input.projectId,
|
|
157
|
+
commitSha: input.commitSha.toLowerCase(),
|
|
158
|
+
trigger: input.trigger || 'manual',
|
|
159
|
+
note: input.note || '',
|
|
160
|
+
state: 'queued',
|
|
161
|
+
startedAt: new Date().toISOString(),
|
|
162
|
+
finishedAt: null,
|
|
163
|
+
durationMs: null,
|
|
164
|
+
error: null,
|
|
165
|
+
staging: null,
|
|
166
|
+
phase: 'queued',
|
|
167
|
+
sequence: this.state.runs.length + 1,
|
|
168
|
+
indexState: 'pending',
|
|
169
|
+
journalId: input.journalId || null,
|
|
170
|
+
journalSequence: input.journalSequence || null,
|
|
171
|
+
};
|
|
172
|
+
this.state.runs.push(run);
|
|
173
|
+
const project = this.getProject(input.projectId);
|
|
174
|
+
if (project) {
|
|
175
|
+
project.lastCommitSha = run.commitSha;
|
|
176
|
+
project.lastRunAt = run.startedAt;
|
|
177
|
+
}
|
|
178
|
+
this.save();
|
|
179
|
+
return run;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
updateRun(runId, patch) {
|
|
183
|
+
const run = this.getRun(runId);
|
|
184
|
+
if (!run) return null;
|
|
185
|
+
Object.assign(run, patch);
|
|
186
|
+
if (run.finishedAt && run.state !== 'queued') {
|
|
187
|
+
run.durationMs = Math.max(0, new Date(run.finishedAt) - new Date(run.startedAt));
|
|
188
|
+
}
|
|
189
|
+
this.save();
|
|
190
|
+
return run;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
addEvent(input) {
|
|
194
|
+
const event = {
|
|
195
|
+
seq: ++this.state.cursor,
|
|
196
|
+
type: input.type,
|
|
197
|
+
projectId: input.projectId || null,
|
|
198
|
+
summary: input.summary || '',
|
|
199
|
+
at: new Date().toISOString(),
|
|
200
|
+
runId: input.runId || null,
|
|
201
|
+
commitSha: input.commitSha || null,
|
|
202
|
+
phase: input.phase || null,
|
|
203
|
+
};
|
|
204
|
+
this.state.events.push(event);
|
|
205
|
+
if (this.state.events.length > 500) this.state.events = this.state.events.slice(-500);
|
|
206
|
+
this.save();
|
|
207
|
+
return event;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
listEvents(limit) {
|
|
211
|
+
const cap = Math.max(1, Math.min(Number(limit) || 50, 500));
|
|
212
|
+
return this.state.events.slice(-cap).reverse();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
readRuntime(projectId) {
|
|
216
|
+
const project = this.getProject(projectId);
|
|
217
|
+
if (!project) throw Object.assign(new Error('项目未登记。'), { code: 'PROJECT_NOT_FOUND', status: 404 });
|
|
218
|
+
return structuredClone(project.runtime || initialRuntime(project));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async updateRuntime(projectId, fn) {
|
|
222
|
+
const project = this.getProject(projectId);
|
|
223
|
+
const draft = this.readRuntime(projectId);
|
|
224
|
+
const result = fn(draft);
|
|
225
|
+
if (result && typeof result.then === 'function') throw new Error('Runtime mutations must be synchronous');
|
|
226
|
+
project.runtime = draft;
|
|
227
|
+
this.save();
|
|
228
|
+
return structuredClone(draft);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setJournalCursor(journalId, sequence) {
|
|
232
|
+
this.state.journalCursors[journalId] = Math.max(this.state.journalCursors[journalId] || 0, sequence);
|
|
233
|
+
this.save();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function initialRuntime(project = {}) { return { lastAnalyzedCommit: project.trackingStartCommit || null, conversationBaselineCursor: project.conversationBaselineCursor ?? null, conversationBaselineCursors: project.conversationBaselineCursors || {}, consumedRequirements: [], analysis: { status: 'idle', activeClaim: null, lastError: null }, index: { generation: 0, dirty: false, status: 'pending' } }; }
|
|
238
|
+
|
|
239
|
+
module.exports = { EngineStore, initialRuntime };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/* 与 event-bridge / vector-hub 一致:灰阶四层、单一蓝与克制的语义色。 */
|
|
2
|
+
:root {
|
|
3
|
+
--bg-side: #191a1d; --bg-main: #202125; --bg-card: #292a2f; --bg-card-raised: #2d2f35; --bg-input: #34363c;
|
|
4
|
+
--bg-hover: #303237; --bg-active: #393b42; --line: #3b3d44; --line-soft: #2d2f35; --text: #ebebed;
|
|
5
|
+
--text-2: #a9abb1; --text-3: #73757c; --label: #91939a; --blue: #5b93f7; --blue-soft: rgba(91, 147, 247, .13);
|
|
6
|
+
--green: #42cf9d; --green-soft: rgba(66, 207, 157, .11); --red: #ec776c; --red-soft: rgba(236, 119, 108, .12);
|
|
7
|
+
--amber: #e8ae5c; --amber-soft: rgba(232, 174, 92, .12); --mono: Consolas, "Microsoft YaHei", monospace;
|
|
8
|
+
}
|
|
9
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
.debug-only { display: none !important; }
|
|
11
|
+
.engine-debug .debug-only { display: flex !important; }
|
|
12
|
+
body.kb-embedded:not([data-kb-theme="dark"]) {
|
|
13
|
+
--bg-side: #f8f9fb; --bg-main: #ffffff; --bg-card: #ffffff; --bg-card-raised: #f8f9fb; --bg-input: #edf1f4;
|
|
14
|
+
--bg-hover: #edf1f4; --bg-active: #e6edf3; --line: #dce2e8; --line-soft: #dce2e8;
|
|
15
|
+
--text: #202733; --text-2: #687587; --text-3: #8e99a7; --label: #687587;
|
|
16
|
+
--blue: #516f89; --blue-soft: #e6edf3; --green: #397761; --green-soft: #edf6f1; --red: #a05252; --red-soft: #fbeeee;
|
|
17
|
+
color-scheme: light;
|
|
18
|
+
}
|
|
19
|
+
body.kb-embedded[data-kb-theme="dark"] { --bg-side:#1b2028; --bg-main:#20252d; --bg-card:#252b34; --bg-card-raised:#2c333e; --bg-input:#2c333e; --bg-hover:#2c333e; --bg-active:#293748; --line:#39424f; --line-soft:#39424f; --text:#e6ebf0; --text-2:#a0aaba; --text-3:#788393; --label:#a0aaba; --blue:#8aa8cf; --blue-soft:#293748; --green:#81b29a; --red:#d88b8b; }
|
|
20
|
+
.kb-embedded aside { width: 208px; min-width: 208px; }
|
|
21
|
+
.kb-embedded .brand { display: none; }
|
|
22
|
+
.kb-embedded #btnShowRegister, .kb-embedded #registerForm, .kb-embedded [data-del] { display: none; }
|
|
23
|
+
.kb-embedded main { padding: 24px 32px; max-width: none; }
|
|
24
|
+
.kb-embedded .metric strong { font-size: 24px; }
|
|
25
|
+
html, body { min-height: 100%; }
|
|
26
|
+
body { background: var(--bg-main); color: var(--text); font: 13px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; color-scheme: dark; }
|
|
27
|
+
button, input, select { font: inherit; }
|
|
28
|
+
button { color: inherit; background: none; border: 0; cursor: pointer; }
|
|
29
|
+
button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
30
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
31
|
+
::-webkit-scrollbar-thumb { background: #4a4c53; border: 3px solid transparent; border-radius: 99px; background-clip: content-box; }
|
|
32
|
+
.hidden { display: none !important; }
|
|
33
|
+
.mono { color: var(--text-2); font-family: var(--mono); font-size: 11.5px; }
|
|
34
|
+
|
|
35
|
+
#app { display: flex; min-height: 100vh; }
|
|
36
|
+
aside { width: 224px; min-width: 224px; background: var(--bg-side); border-right: 1px solid var(--line-soft); display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; }
|
|
37
|
+
.brand { display: flex; align-items: center; gap: 9px; padding: 17px 15px 16px; border-bottom: 1px solid var(--line-soft); }
|
|
38
|
+
.mark { height: 25px; width: 25px; display: grid; place-items: center; border-radius: 7px; background: var(--text); color: #17181b; font: 700 10px/1 var(--mono); }
|
|
39
|
+
.brand-copy { display: grid; line-height: 1.2; }
|
|
40
|
+
.brand-copy strong { font-size: 12px; font-weight: 600; }
|
|
41
|
+
.brand-copy span { margin-top: 3px; color: var(--text-3); font-size: 10.5px; }
|
|
42
|
+
.nav { flex: 1; padding: 13px 8px; overflow-y: auto; }
|
|
43
|
+
.nav-label { padding: 0 10px 7px; color: var(--text-3); font-size: 10px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; }
|
|
44
|
+
.nav-label-lower { margin-top: 17px; }
|
|
45
|
+
.nav-item { width: 100%; display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 6px; color: var(--text-2); font-size: 12.5px; text-align: left; }
|
|
46
|
+
.nav-symbol { width: 14px; color: var(--text-3); font-family: var(--mono); text-align: center; }
|
|
47
|
+
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
48
|
+
.nav-item.on { background: var(--bg-active); color: var(--text); }
|
|
49
|
+
.nav-item.on .nav-symbol { color: var(--blue); }
|
|
50
|
+
.side-foot { display: flex; align-items: center; gap: 8px; padding: 12px 15px; border-top: 1px solid var(--line-soft); color: var(--text-3); font-size: 11px; }
|
|
51
|
+
.dot, .readiness-dot { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--red); }
|
|
52
|
+
.dot.up, .is-ok { background: var(--green); box-shadow: 0 0 0 3px var(--green-soft); }
|
|
53
|
+
|
|
54
|
+
main { flex: 1; min-width: 0; padding: 28px clamp(22px, 4vw, 54px) 48px; }
|
|
55
|
+
.topbar, .view { max-width: 1220px; margin: 0 auto; }
|
|
56
|
+
.topbar { display: flex; align-items: end; justify-content: space-between; gap: 20px; padding-bottom: 22px; }
|
|
57
|
+
.eyebrow, .section-kicker { color: var(--text-3); font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
|
|
58
|
+
h1 { margin-top: 3px; font-size: 24px; font-weight: 600; line-height: 1.2; }
|
|
59
|
+
h2 { font-size: 14px; font-weight: 600; line-height: 1.35; }
|
|
60
|
+
.scope-control { display: grid; gap: 4px; min-width: 180px; color: var(--text-3); font-size: 10.5px; }
|
|
61
|
+
select, input { min-width: 0; width: 100%; padding: 7px 9px; background: var(--bg-input); border: 1px solid var(--line); border-radius: 6px; color: var(--text); font-size: 12.5px; }
|
|
62
|
+
select:focus, input:focus { border-color: var(--blue); outline: none; }
|
|
63
|
+
.view { display: none; }
|
|
64
|
+
.view.on { display: block; }
|
|
65
|
+
.view-intro { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 16px; color: var(--text-2); }
|
|
66
|
+
.view-intro p { max-width: 720px; }
|
|
67
|
+
.view-intro.compact { margin-bottom: 14px; }
|
|
68
|
+
.btn { padding: 6px 11px; background: var(--bg-card-raised); border: 1px solid var(--line); border-radius: 6px; color: var(--text-2); font-size: 12px; white-space: nowrap; }
|
|
69
|
+
.btn:hover { background: var(--bg-hover); color: var(--text); }
|
|
70
|
+
.btn.primary { border-color: var(--blue); background: var(--blue); color: white; }
|
|
71
|
+
.btn.primary:hover { filter: brightness(1.08); }
|
|
72
|
+
.btn.danger:hover { border-color: var(--red); background: var(--red-soft); color: var(--red); }
|
|
73
|
+
.quiet-link { color: var(--blue); font-size: 11.5px; }
|
|
74
|
+
.quiet-link:hover { text-decoration: underline; }
|
|
75
|
+
|
|
76
|
+
.panel, .flow-panel { background: var(--bg-card); border: 1px solid var(--line-soft); border-radius: 9px; padding: 15px 16px; }
|
|
77
|
+
.panel-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 13px; }
|
|
78
|
+
.panel-heading h2 { margin-top: 2px; }
|
|
79
|
+
.phase-pill, .badge { display: inline-flex; align-items: center; min-height: 20px; padding: 1px 8px; border-radius: 99px; font: 10.5px/1.5 var(--mono); white-space: nowrap; }
|
|
80
|
+
.phase-pill { border: 1px solid var(--line); color: var(--text-3); }
|
|
81
|
+
.flow-panel { margin-bottom: 12px; background: linear-gradient(110deg, var(--bg-card), #282b32); }
|
|
82
|
+
.flow-track { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); list-style: none; overflow: hidden; border: 1px solid var(--line); border-radius: 7px; }
|
|
83
|
+
.flow-step { position: relative; display: flex; gap: 10px; min-height: 78px; padding: 13px 11px; background: rgba(0,0,0,.06); border-right: 1px solid var(--line); }
|
|
84
|
+
.flow-step:last-child { border-right: 0; }
|
|
85
|
+
.flow-step::after { content: ""; position: absolute; right: -1px; top: 13px; height: 18px; border-right: 2px solid var(--blue); opacity: .78; }
|
|
86
|
+
.flow-step:last-child::after { display: none; }
|
|
87
|
+
.flow-step.is-active { background: var(--blue-soft); }
|
|
88
|
+
.flow-step.is-done .flow-number { border-color: rgba(66,207,157,.55); color: var(--green); }
|
|
89
|
+
.flow-step.is-active .flow-number { border-color: rgba(91,147,247,.7); color: var(--blue); }
|
|
90
|
+
.flow-step.is-pending { opacity: .65; }
|
|
91
|
+
.flow-number { width: 21px; height: 21px; flex: 0 0 21px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 50%; color: var(--text-3); font: 10px/1 var(--mono); }
|
|
92
|
+
.flow-step strong { display: block; font-size: 12px; font-weight: 500; }
|
|
93
|
+
.flow-step small { display: block; margin-top: 4px; color: var(--text-3); font-size: 10.5px; line-height: 1.35; }
|
|
94
|
+
|
|
95
|
+
.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 12px; }
|
|
96
|
+
.metric { padding: 13px 15px; background: var(--bg-card); border: 1px solid var(--line-soft); border-radius: 8px; }
|
|
97
|
+
.metric span, .metric small { display: block; }
|
|
98
|
+
.metric span { color: var(--label); font-size: 11px; }
|
|
99
|
+
.metric strong { display: block; margin: 2px 0; font: 600 23px/1.2 var(--mono); }
|
|
100
|
+
.metric small { color: var(--text-3); font-size: 10.5px; }
|
|
101
|
+
.metric-warn strong { color: var(--red); }
|
|
102
|
+
.overview-grid { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(300px, .85fr); gap: 12px; align-items: start; }
|
|
103
|
+
.table-wrap { overflow-x: auto; }
|
|
104
|
+
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
105
|
+
th { padding: 5px 8px 7px; border-bottom: 1px solid var(--line-soft); color: var(--label); font-size: 10.5px; font-weight: 500; text-align: left; white-space: nowrap; }
|
|
106
|
+
td { padding: 8px; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
|
|
107
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
108
|
+
tbody tr.click { cursor: pointer; }
|
|
109
|
+
tbody tr.click:hover, tbody tr.sel { background: var(--bg-hover); }
|
|
110
|
+
td.wrap { min-width: 170px; word-break: break-word; }
|
|
111
|
+
.empty { padding: 16px; color: var(--text-3); text-align: center; }
|
|
112
|
+
.badge.queued, .badge.running { background: var(--blue-soft); color: var(--blue); }
|
|
113
|
+
.badge.running { animation: pulse 1.3s ease infinite; }
|
|
114
|
+
.badge.succeeded { background: var(--green-soft); color: var(--green); }
|
|
115
|
+
.badge.failed { background: var(--red-soft); color: var(--red); }
|
|
116
|
+
.badge.type { border: 1px solid var(--line); color: var(--text-2); }
|
|
117
|
+
@keyframes pulse { 50% { opacity: .55; } }
|
|
118
|
+
.attention-body { min-height: 48px; margin: -2px 0 12px; color: var(--text-2); }
|
|
119
|
+
.service-list { border-top: 1px solid var(--line-soft); font-size: 11.5px; }
|
|
120
|
+
.service-list div { display: grid; grid-template-columns: 105px 1fr; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--line-soft); }
|
|
121
|
+
.service-list div:last-child { border: 0; }
|
|
122
|
+
.service-list dt { color: var(--text-3); }
|
|
123
|
+
.service-list dd { min-width: 0; color: var(--text-2); overflow-wrap: anywhere; }
|
|
124
|
+
|
|
125
|
+
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; }
|
|
126
|
+
.filters select { width: auto; min-width: 145px; }
|
|
127
|
+
.form { margin-bottom: 12px; }
|
|
128
|
+
.form-row { display: flex; gap: 10px; margin-bottom: 10px; }
|
|
129
|
+
.form label { display: flex; flex: 1; flex-direction: column; gap: 4px; color: var(--label); font-size: 11px; }
|
|
130
|
+
.form .checkbox { flex: 0 0 auto; flex-direction: row; align-items: center; gap: 7px; padding-top: 20px; color: var(--text-2); font-size: 12px; }
|
|
131
|
+
.form .checkbox input { width: auto; }
|
|
132
|
+
.form-actions { display: flex; gap: 8px; }
|
|
133
|
+
.detail-grid { display: grid; grid-template-columns: minmax(225px, .7fr) minmax(0, 1.3fr); gap: 20px; }
|
|
134
|
+
.kv div { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 10px; padding: 5px 0; }
|
|
135
|
+
.kv dt { color: var(--text-3); }
|
|
136
|
+
.kv dd { min-width: 0; overflow-wrap: anywhere; }
|
|
137
|
+
.panel-sub { margin: 0 0 6px; color: var(--text-3); font-size: 10.5px; letter-spacing: .03em; text-transform: uppercase; }
|
|
138
|
+
.code { max-height: 270px; padding: 10px; overflow: auto; background: var(--bg-side); border: 1px solid var(--line-soft); border-radius: 6px; color: var(--text-2); font: 11px/1.55 var(--mono); white-space: pre-wrap; word-break: break-word; }
|
|
139
|
+
.run-timeline { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; list-style: none; margin-bottom: 16px; }
|
|
140
|
+
.run-timeline li { position: relative; padding: 7px 8px; border-left: 2px solid var(--line); color: var(--text-3); font-size: 11px; }
|
|
141
|
+
.run-timeline li.done { border-color: var(--green); color: var(--green); }
|
|
142
|
+
.run-timeline li.current { border-color: var(--blue); color: var(--blue); }
|
|
143
|
+
.run-timeline li.failed { border-color: var(--red); color: var(--red); }
|
|
144
|
+
|
|
145
|
+
.knowledge-hero { display: flex; align-items: end; justify-content: space-between; gap: 22px; margin-bottom: 12px; }
|
|
146
|
+
.knowledge-path { max-width: 650px; margin-top: 5px; overflow-wrap: anywhere; }
|
|
147
|
+
.knowledge-summary { min-width: 145px; text-align: right; }
|
|
148
|
+
.knowledge-summary span, .knowledge-summary small { display: block; color: var(--text-3); font-size: 10.5px; }
|
|
149
|
+
.knowledge-summary strong { display: block; margin: 2px 0; font: 600 16px/1.3 var(--mono); }
|
|
150
|
+
.knowledge-list { display: grid; gap: 0; }
|
|
151
|
+
.knowledge-operation { display: grid; grid-template-columns: 74px minmax(0, 1fr) auto; gap: 11px; align-items: start; padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
|
|
152
|
+
.knowledge-operation:last-child { border: 0; }
|
|
153
|
+
.operation-kind { color: var(--blue); font: 10.5px/1.7 var(--mono); }
|
|
154
|
+
.knowledge-operation strong { display: block; font: 12px/1.45 var(--mono); overflow-wrap: anywhere; }
|
|
155
|
+
.knowledge-operation small { display: block; margin-top: 2px; color: var(--text-3); font-size: 11px; }
|
|
156
|
+
.knowledge-operation em { color: var(--text-3); font-size: 10.5px; font-style: normal; white-space: nowrap; }
|
|
157
|
+
.readiness-list > div { display: grid; grid-template-columns: 12px 1fr auto; gap: 9px; align-items: start; padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
|
|
158
|
+
.readiness-list > div:last-child { border: 0; }
|
|
159
|
+
.readiness-dot { margin-top: 5px; }
|
|
160
|
+
.readiness-dot.is-pending { background: var(--amber); box-shadow: 0 0 0 3px var(--amber-soft); }
|
|
161
|
+
.readiness-list strong { display: block; font-size: 12px; font-weight: 500; }
|
|
162
|
+
.readiness-list small { display: block; margin-top: 2px; color: var(--text-3); font-size: 11px; }
|
|
163
|
+
.readiness-list b { color: var(--text-3); font: 10.5px/1.7 var(--mono); white-space: nowrap; }
|
|
164
|
+
.answer { color: var(--text); line-height: 1.75; white-space: pre-wrap; }
|
|
165
|
+
.citations { padding-left: 19px; color: var(--text-2); font-size: 11.5px; }
|
|
166
|
+
.citations li { margin: 3px 0; }
|
|
167
|
+
.score { color: var(--blue); font-family: var(--mono); }
|
|
168
|
+
#toast { position: fixed; bottom: 18px; left: 50%; z-index: 5; max-width: min(92vw, 560px); padding: 8px 14px; transform: translateX(-50%) translateY(10px); background: var(--bg-card-raised); border: 1px solid var(--line); border-radius: 7px; color: var(--text); opacity: 0; pointer-events: none; transition: opacity .18s ease, transform .18s ease; }
|
|
169
|
+
#toast.show { transform: translateX(-50%); opacity: 1; }
|
|
170
|
+
|
|
171
|
+
@media (max-width: 980px) { .flow-track { grid-template-columns: 1fr; } .flow-step { min-height: auto; border-right: 0; border-bottom: 1px solid var(--line); } .flow-step:last-child { border-bottom: 0; } .flow-step::after { top: auto; bottom: -1px; right: auto; left: 16px; width: 18px; height: 0; border-right: 0; border-bottom: 2px solid var(--blue); } .metric-grid { grid-template-columns: repeat(2, 1fr); } .overview-grid { grid-template-columns: 1fr; } }
|
|
172
|
+
@media (max-width: 720px) { #app { display: block; } aside { position: static; width: auto; min-width: 0; height: auto; } .brand { padding-bottom: 11px; } .nav { display: flex; gap: 3px; padding: 7px 8px; overflow-x: auto; } .nav-label, .nav-symbol, .side-foot { display: none; } .nav-item { width: auto; white-space: nowrap; } main { padding: 21px 15px 36px; } .topbar { align-items: start; } .scope-control { min-width: 130px; } h1 { font-size: 21px; } .view-intro, .knowledge-hero { align-items: flex-start; flex-direction: column; } .form-row, .detail-grid { display: grid; grid-template-columns: 1fr; } .form .checkbox { padding-top: 0; } .run-timeline { grid-template-columns: 1fr; } .knowledge-summary { text-align: left; } }
|