frontend-project-context 1.0.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/CHANGELOG.md +14 -0
- package/LICENSE +201 -0
- package/NOTICE +4 -0
- package/PROJECT_STATE.json +176 -0
- package/README.md +148 -0
- package/RTK.md +13 -0
- package/UPGRADING.md +15 -0
- package/bin/project-context.mjs +7 -0
- package/docs/00-PRODUCT-CONSTITUTION.md +166 -0
- package/docs/01-PRODUCT-CORE.md +143 -0
- package/docs/02-MARKET-BOUNDARY.md +88 -0
- package/docs/03-FINAL-SOLUTION.md +203 -0
- package/docs/04-PROGRAM-DESIGN.md +428 -0
- package/docs/05-ACCEPTANCE-CONTRACT.md +348 -0
- package/docs/06-HISTORICAL-PROTOTYPE.md +55 -0
- package/docs/07-REAL-TASK-EVIDENCE.md +52 -0
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +199 -0
- package/docs/09-B0-DTG-TMC-MOBILE.md +173 -0
- package/docs/10-B0-DTG-TMC-PC.md +118 -0
- package/docs/11-V1-AUTHORING-CLOSURE-DESIGN.md +312 -0
- package/docs/12-KNOWLEDGE-MAINTENANCE-CLOSURE-ROADMAP.md +350 -0
- package/docs/13-READ-ONLY-GOVERNANCE-DASHBOARD-DESIGN.md +489 -0
- package/docs/14-FORMAL-RELEASE-READINESS.md +61 -0
- package/docs/15-SOURCE-LIFECYCLE-CLOSURE-DESIGN.md +260 -0
- package/docs/README.md +74 -0
- package/examples/README.md +17 -0
- package/examples/package.json +11 -0
- package/examples/project-context-check.yml +22 -0
- package/package.json +40 -0
- package/src/project-context/approver.mjs +177 -0
- package/src/project-context/authoring.mjs +190 -0
- package/src/project-context/canonical-json.mjs +55 -0
- package/src/project-context/checker.mjs +132 -0
- package/src/project-context/cli.mjs +409 -0
- package/src/project-context/contract-schema.mjs +316 -0
- package/src/project-context/dashboard-model.mjs +278 -0
- package/src/project-context/dashboard-renderer.mjs +637 -0
- package/src/project-context/discovery.mjs +251 -0
- package/src/project-context/errors.mjs +13 -0
- package/src/project-context/io.mjs +93 -0
- package/src/project-context/maintenance.mjs +400 -0
- package/src/project-context/path-policy.mjs +155 -0
- package/src/project-context/project-store.mjs +138 -0
- package/src/project-context/projection-store.mjs +107 -0
- package/src/project-context/renderer.mjs +135 -0
- package/src/project-context/scope-compiler.mjs +132 -0
- package/src/project-context/source-reader.mjs +124 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalJson } from "./canonical-json.mjs";
|
|
3
|
+
|
|
4
|
+
const STATUS_LABELS = new Map([
|
|
5
|
+
["approved", ["已批准", "Approved"]],
|
|
6
|
+
["attention", ["需关注", "Attention"]],
|
|
7
|
+
["blocked", ["已阻断", "Blocked"]],
|
|
8
|
+
["changed", ["已变化", "Changed"]],
|
|
9
|
+
["clean", ["通过", "Clean"]],
|
|
10
|
+
["conflict", ["所有权冲突", "Conflict"]],
|
|
11
|
+
["deprecated", ["已废弃", "Deprecated"]],
|
|
12
|
+
["diverged", ["已偏离", "Diverged"]],
|
|
13
|
+
["external", ["外部引用", "External"]],
|
|
14
|
+
["healthy", ["正常", "Healthy"]],
|
|
15
|
+
["manual", ["人工决定", "Manual"]],
|
|
16
|
+
["missing", ["缺失", "Missing"]],
|
|
17
|
+
["proposed", ["待批准", "Proposed"]],
|
|
18
|
+
["stale", ["已过期", "Stale"]],
|
|
19
|
+
["unchanged", ["正常", "Unchanged"]],
|
|
20
|
+
["unreadable", ["不可读", "Unreadable"]],
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const VIEW_LABELS = [
|
|
24
|
+
["overview", "总览", "Overview"],
|
|
25
|
+
["knowledge", "知识", "Knowledge"],
|
|
26
|
+
["sources", "来源", "Sources"],
|
|
27
|
+
["scopes", "范围", "Scopes"],
|
|
28
|
+
["projections", "投影", "Projections"],
|
|
29
|
+
["findings", "问题", "Findings"],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const KIND_LABELS = new Map([
|
|
33
|
+
["fact", ["事实", "Fact"]],
|
|
34
|
+
["policy", ["规则", "Policy"]],
|
|
35
|
+
["reference", ["参考", "Reference"]],
|
|
36
|
+
["validation-description", ["验证说明", "Validation"]],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const FINDING_LABELS = new Map([
|
|
40
|
+
["contract-conflict", ["合同知识存在冲突", "Contract knowledge conflicts"]],
|
|
41
|
+
["item-approval-pending", ["知识项等待批准", "Knowledge item awaiting approval"]],
|
|
42
|
+
["projection-diverged", ["投影内容与合同不一致", "Projection diverged from Contract"]],
|
|
43
|
+
["projection-item-missing", ["投影引用的知识项不存在", "Projection item is missing"]],
|
|
44
|
+
["projection-missing", ["受管投影文件缺失", "Managed projection is missing"]],
|
|
45
|
+
["projection-ownership-conflict", ["投影所有权冲突", "Projection ownership conflict"]],
|
|
46
|
+
["projection-path-invalid", ["投影路径无效", "Projection path is invalid"]],
|
|
47
|
+
["projection-renderer-stale", ["投影渲染器版本过期", "Projection renderer is stale"]],
|
|
48
|
+
["projection-stale", ["投影需要重新发布", "Projection needs republishing"]],
|
|
49
|
+
["projection-unreadable", ["受管投影无法读取", "Managed projection is unreadable"]],
|
|
50
|
+
["scope-override-cycle", ["Scope override 存在循环", "Scope override cycle"]],
|
|
51
|
+
["scope-override-invalid", ["Scope override 无效", "Invalid scope override"]],
|
|
52
|
+
["source-changed", ["依据来源发生变化", "Source changed"]],
|
|
53
|
+
["source-lock-mismatch", ["来源 checkpoint 不一致", "Source checkpoint mismatch"]],
|
|
54
|
+
["source-lock-missing", ["来源 checkpoint 缺失", "Source checkpoint missing"]],
|
|
55
|
+
["source-lock-deprecated", ["已废弃来源仍有 checkpoint", "Deprecated source still has a checkpoint"]],
|
|
56
|
+
["source-lock-orphan", ["来源 checkpoint 没有对应来源", "Orphan source checkpoint"]],
|
|
57
|
+
["source-reference-deprecated", ["当前知识引用已废弃来源", "Current knowledge references a deprecated source"]],
|
|
58
|
+
["source-missing", ["依据来源缺失", "Source is missing"]],
|
|
59
|
+
["source-unreadable", ["依据来源无法读取", "Source is unreadable"]],
|
|
60
|
+
["verification-failed", ["知识验证失败", "Knowledge verification failed"]],
|
|
61
|
+
["verification-kind-unsupported", ["知识验证类型不受支持", "Unsupported verification kind"]],
|
|
62
|
+
["verification-source-incompatible", ["知识验证来源不兼容", "Incompatible verification source"]],
|
|
63
|
+
["verification-source-missing", ["知识验证来源缺失", "Verification source is missing"]],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const CSS = String.raw`
|
|
67
|
+
:root {
|
|
68
|
+
color-scheme: light dark;
|
|
69
|
+
--bg: #f3f5f4;
|
|
70
|
+
--surface: #ffffff;
|
|
71
|
+
--surface-muted: #f0f2f1;
|
|
72
|
+
--surface-strong: #e5e9e7;
|
|
73
|
+
--text: #18201c;
|
|
74
|
+
--muted: #4d5a54;
|
|
75
|
+
--subtle: #64716a;
|
|
76
|
+
--border: #d0d7d3;
|
|
77
|
+
--border-strong: #a7b2ac;
|
|
78
|
+
--accent: #087657;
|
|
79
|
+
--accent-soft: #e1f1eb;
|
|
80
|
+
--accent-text: #075940;
|
|
81
|
+
--danger: #a02e38;
|
|
82
|
+
--danger-soft: #f7e7e9;
|
|
83
|
+
--warning: #765000;
|
|
84
|
+
--warning-soft: #f6edcf;
|
|
85
|
+
--focus: #006bc7;
|
|
86
|
+
--radius: 6px;
|
|
87
|
+
--shadow: 0 1px 2px rgb(16 24 20 / 6%);
|
|
88
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
89
|
+
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
90
|
+
}
|
|
91
|
+
@media (prefers-color-scheme: dark) {
|
|
92
|
+
:root {
|
|
93
|
+
--bg: #101512;
|
|
94
|
+
--surface: #171d1a;
|
|
95
|
+
--surface-muted: #1f2723;
|
|
96
|
+
--surface-strong: #29332e;
|
|
97
|
+
--text: #edf2ef;
|
|
98
|
+
--muted: #b9c3bd;
|
|
99
|
+
--subtle: #9aa69f;
|
|
100
|
+
--border: #37413c;
|
|
101
|
+
--border-strong: #5a6760;
|
|
102
|
+
--accent: #61c9a6;
|
|
103
|
+
--accent-soft: #17382e;
|
|
104
|
+
--accent-text: #9ce2ca;
|
|
105
|
+
--danger: #ff9ca5;
|
|
106
|
+
--danger-soft: #43262a;
|
|
107
|
+
--warning: #efc66b;
|
|
108
|
+
--warning-soft: #40361e;
|
|
109
|
+
--focus: #78bfff;
|
|
110
|
+
--shadow: none;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
* { box-sizing: border-box; }
|
|
114
|
+
html { background: var(--bg); }
|
|
115
|
+
html[lang="zh-CN"] [lang="en"], html[lang="en"] [lang="zh-CN"] { display: none !important; }
|
|
116
|
+
body { margin: 0; min-width: 0; color: var(--text); background: var(--bg); font: 14px/1.55 var(--sans); }
|
|
117
|
+
button, input, select { font: inherit; }
|
|
118
|
+
button, select { cursor: pointer; }
|
|
119
|
+
button { touch-action: manipulation; }
|
|
120
|
+
button:active { background: var(--surface-strong); }
|
|
121
|
+
button:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible {
|
|
122
|
+
outline: 3px solid var(--focus);
|
|
123
|
+
outline-offset: 2px;
|
|
124
|
+
}
|
|
125
|
+
[hidden] { display: none !important; }
|
|
126
|
+
.skip-link { position: fixed; z-index: 100; top: 8px; left: 8px; padding: 10px 14px; border-radius: var(--radius); color: #fff; background: #075ea8; font-weight: 700; transform: translateY(-160%); }
|
|
127
|
+
.skip-link:focus { transform: translateY(0); }
|
|
128
|
+
.app { min-height: 100dvh; display: grid; grid-template-columns: 240px minmax(0, 1fr); }
|
|
129
|
+
.sidebar { position: sticky; top: 0; height: 100dvh; padding: 24px 16px; border-right: 1px solid var(--border); background: var(--surface); }
|
|
130
|
+
.brand { padding: 0 8px 22px; border-bottom: 1px solid var(--border); }
|
|
131
|
+
.brand-mark { display: flex; align-items: center; gap: 10px; }
|
|
132
|
+
.brand-glyph { display: grid; width: 30px; height: 30px; place-items: center; border: 1px solid var(--accent); border-radius: 5px; color: var(--accent-text); font: 700 12px/1 var(--mono); }
|
|
133
|
+
.brand strong { display: block; font-size: 14px; letter-spacing: -.01em; }
|
|
134
|
+
.brand span { display: block; margin-top: 2px; color: var(--subtle); font: 11px/1.4 var(--mono); }
|
|
135
|
+
.nav { display: grid; gap: 2px; margin-top: 16px; }
|
|
136
|
+
.nav button { min-height: 44px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 10px 9px 12px; border: 0; border-left: 3px solid transparent; border-radius: 3px; color: var(--muted); background: transparent; text-align: left; }
|
|
137
|
+
.nav button:hover { color: var(--text); background: var(--surface-muted); }
|
|
138
|
+
.nav button[aria-selected="true"] { color: var(--accent-text); border-left-color: var(--accent); background: var(--accent-soft); font-weight: 700; }
|
|
139
|
+
.nav-label { display: grid; gap: 1px; }
|
|
140
|
+
.nav-label strong { font-size: 13px; line-height: 1.2; }
|
|
141
|
+
.nav-count { min-width: 24px; color: var(--subtle); font: 11px/1 var(--mono); text-align: right; }
|
|
142
|
+
.nav button[aria-selected="true"] .nav-count { color: inherit; }
|
|
143
|
+
.sidebar-note { position: absolute; right: 24px; bottom: 24px; left: 24px; color: var(--subtle); font: 11px/1.5 var(--mono); }
|
|
144
|
+
.main { min-width: 0; max-width: 1480px; padding: 24px clamp(20px, 3vw, 48px) 64px; }
|
|
145
|
+
.masthead { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 420px); align-items: start; gap: 32px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
|
|
146
|
+
.masthead-tools { display: grid; gap: 8px; }
|
|
147
|
+
.locale-switch { display: inline-grid; grid-template-columns: repeat(2, minmax(0, 1fr)); justify-self: end; padding: 3px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-muted); }
|
|
148
|
+
.locale-switch button { min-height: 36px; padding: 6px 12px; border: 0; border-radius: 3px; color: var(--muted); background: transparent; }
|
|
149
|
+
.locale-switch button[aria-pressed="true"] { color: var(--accent-text); background: var(--surface); box-shadow: var(--shadow); font-weight: 700; }
|
|
150
|
+
.eyebrow { margin: 0 0 5px; color: var(--accent-text); font: 700 11px/1.4 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
|
151
|
+
.masthead h1 { margin: 0; font-size: clamp(24px, 3vw, 36px); line-height: 1.15; letter-spacing: -.035em; }
|
|
152
|
+
.masthead p:not(.eyebrow) { margin: 7px 0 0; color: var(--muted); }
|
|
153
|
+
.digest { padding: 11px 13px; border: 1px solid var(--border); border-radius: var(--radius); color: var(--subtle); background: var(--surface); box-shadow: var(--shadow); font: 11px/1.55 var(--mono); overflow-wrap: anywhere; }
|
|
154
|
+
.digest strong { display: block; margin-bottom: 3px; color: var(--text); font-family: var(--sans); font-size: 12px; }
|
|
155
|
+
.panel { margin-top: 28px; }
|
|
156
|
+
.section-head { display: grid; grid-template-columns: minmax(220px, .55fr) minmax(0, 1fr); gap: 28px; align-items: end; margin-bottom: 16px; }
|
|
157
|
+
.section-head h2 { margin: 0; font-size: 22px; line-height: 1.25; letter-spacing: -.02em; }
|
|
158
|
+
.section-head p { max-width: 72ch; margin: 0; color: var(--muted); }
|
|
159
|
+
.health-banner { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 20px; align-items: center; padding: 20px 22px; border: 1px solid var(--border); border-left: 4px solid var(--accent); border-radius: var(--radius) var(--radius) 0 0; background: var(--surface); }
|
|
160
|
+
.health-banner h2 { margin: 3px 0 0; font-size: clamp(22px, 3vw, 30px); line-height: 1.2; letter-spacing: -.025em; }
|
|
161
|
+
.health-banner p { max-width: 70ch; margin: 6px 0 0; color: var(--muted); }
|
|
162
|
+
.health-code { color: var(--subtle); font: 12px/1.5 var(--mono); text-align: right; }
|
|
163
|
+
.health-banner > div:last-child { display: grid; justify-items: end; gap: 8px; }
|
|
164
|
+
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--border); border-top: 0; background: var(--surface); box-shadow: var(--shadow); }
|
|
165
|
+
.metric { min-width: 0; padding: 14px 18px; border-right: 1px solid var(--border); }
|
|
166
|
+
.metric:last-child { border-right: 0; }
|
|
167
|
+
.metric span { display: block; color: var(--subtle); font-size: 12px; }
|
|
168
|
+
.metric strong { display: block; margin-top: 2px; font: 650 22px/1.25 var(--mono); font-variant-numeric: tabular-nums; }
|
|
169
|
+
.metric small { display: block; margin-top: 2px; color: var(--muted); }
|
|
170
|
+
.flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 24px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
|
|
171
|
+
.flow-step { position: relative; min-width: 0; padding: 16px 18px; border-right: 1px solid var(--border); }
|
|
172
|
+
.flow-step:last-child { border-right: 0; }
|
|
173
|
+
.flow-step:not(:last-child)::after { content: ""; position: absolute; z-index: 1; top: 50%; right: -5px; width: 8px; height: 8px; border-top: 1px solid var(--border-strong); border-right: 1px solid var(--border-strong); background: var(--surface); transform: translateY(-50%) rotate(45deg); }
|
|
174
|
+
.flow-step small { display: block; color: var(--subtle); font: 11px/1.4 var(--mono); }
|
|
175
|
+
.flow-step strong { display: block; margin-top: 4px; font-size: 14px; }
|
|
176
|
+
.flow-step span { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; }
|
|
177
|
+
.split { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(280px, .6fr); gap: 20px; margin-top: 24px; }
|
|
178
|
+
.block { min-width: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
|
|
179
|
+
.block-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--surface-muted); }
|
|
180
|
+
.block-head h3 { margin: 0; font-size: 14px; }
|
|
181
|
+
.block-head span { color: var(--subtle); font: 11px/1.4 var(--mono); }
|
|
182
|
+
.knowledge-strip { display: grid; }
|
|
183
|
+
.knowledge-strip article { display: grid; grid-template-columns: minmax(150px, .45fr) minmax(0, 1fr); gap: 16px; padding: 13px 16px; border-bottom: 1px solid var(--border); }
|
|
184
|
+
.knowledge-strip article:last-child { border-bottom: 0; }
|
|
185
|
+
.knowledge-strip h4 { margin: 0; font-size: 13px; line-height: 1.45; }
|
|
186
|
+
.knowledge-strip p { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
187
|
+
.policy-list article { grid-template-columns: 1fr; gap: 3px; }
|
|
188
|
+
.notice { padding: 12px 14px; border: 1px solid var(--border); border-left: 3px solid var(--warning); border-radius: var(--radius); background: var(--warning-soft); color: var(--muted); }
|
|
189
|
+
.notice strong { color: var(--text); }
|
|
190
|
+
.notice-spaced { margin-top: 16px; }
|
|
191
|
+
.toolbar { display: grid; grid-template-columns: minmax(240px, 1fr) repeat(3, minmax(140px, auto)); gap: 8px; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0; background: var(--surface-muted); }
|
|
192
|
+
.field { display: grid; gap: 5px; }
|
|
193
|
+
.field label { color: var(--muted); font-size: 11px; font-weight: 700; }
|
|
194
|
+
.field input, .field select { width: 100%; min-height: 44px; padding: 9px 10px; border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text); background: var(--surface); }
|
|
195
|
+
.result-line { display: flex; justify-content: space-between; gap: 16px; margin: 0; padding: 10px 12px; border: 1px solid var(--border); border-top: 0; color: var(--muted); background: var(--surface); font-size: 12px; }
|
|
196
|
+
.result-line span { font-variant-numeric: tabular-nums; }
|
|
197
|
+
.records { overflow: clip; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
|
|
198
|
+
.toolbar + .result-line + .records { border-radius: 0 0 var(--radius) var(--radius); border-top: 0; }
|
|
199
|
+
.record { min-width: 0; border-bottom: 1px solid var(--border); background: var(--surface); }
|
|
200
|
+
.record:last-child { border-bottom: 0; }
|
|
201
|
+
article.record { padding: 15px 16px 14px; }
|
|
202
|
+
.record[data-status="deprecated"] { background: var(--surface-muted); opacity: .82; }
|
|
203
|
+
.record-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
|
|
204
|
+
.record h3, .record h4 { margin: 0; overflow-wrap: anywhere; }
|
|
205
|
+
.record h3 { font-size: 14px; line-height: 1.45; }
|
|
206
|
+
.original-label { display: block; margin-bottom: 3px; color: var(--subtle); font: 10px/1.4 var(--mono); letter-spacing: .04em; text-transform: uppercase; }
|
|
207
|
+
.translation-missing { display: block; margin-top: 4px; color: var(--warning); font: 11px/1.4 var(--sans); }
|
|
208
|
+
.record .id { margin-top: 3px; color: var(--subtle); font: 12px/1.4 var(--mono); overflow-wrap: anywhere; }
|
|
209
|
+
.value { max-height: 280px; margin: 12px 0 0; padding: 12px; overflow: auto; border: 1px solid var(--border); border-radius: 4px; color: var(--text); background: var(--surface-muted); font: 12px/1.55 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
210
|
+
.structured-value { margin-top: 12px; padding: 0 12px 12px; border-left: 2px solid var(--border-strong); background: var(--surface-muted); }
|
|
211
|
+
.structured-value > summary { min-height: 44px; padding: 10px 0; color: var(--muted); font-size: 12px; font-weight: 700; }
|
|
212
|
+
.value-tree { margin: 0; padding-left: 20px; }
|
|
213
|
+
dl.value-tree { display: grid; grid-template-columns: minmax(110px, .35fr) minmax(0, 1fr); gap: 5px 12px; padding: 0; }
|
|
214
|
+
.value-tree dt { font: 12px/1.5 var(--mono); }
|
|
215
|
+
.value-tree dd { margin: 0; }
|
|
216
|
+
.raw-value { margin-top: 8px; }
|
|
217
|
+
.raw-value > summary { min-height: 44px; padding: 10px 0; color: var(--accent-text); font-weight: 700; }
|
|
218
|
+
.status { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; min-height: 28px; padding: 3px 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface-muted); color: var(--muted); font-size: 12px; font-weight: 700; white-space: nowrap; }
|
|
219
|
+
.status svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
|
|
220
|
+
.status-clean, .status-approved, .status-unchanged, .status-healthy { color: var(--accent-text); background: var(--accent-soft); }
|
|
221
|
+
.status-attention, .status-proposed, .status-stale, .status-manual, .status-external { color: var(--warning); background: var(--warning-soft); }
|
|
222
|
+
.status-blocked, .status-changed, .status-missing, .status-unreadable, .status-diverged { color: var(--danger); background: var(--danger-soft); }
|
|
223
|
+
.status-conflict { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); }
|
|
224
|
+
details.audit { margin-top: 12px; border-top: 1px solid var(--border); }
|
|
225
|
+
details.audit > summary { min-height: 44px; padding: 11px 0 0; color: var(--accent-text); font-weight: 700; }
|
|
226
|
+
.audit-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; padding-top: 10px; }
|
|
227
|
+
dl { margin: 0; }
|
|
228
|
+
dt { color: var(--subtle); font-size: 12px; }
|
|
229
|
+
dd { margin: 2px 0 10px; overflow-wrap: anywhere; }
|
|
230
|
+
code, .mono { font-family: var(--mono); }
|
|
231
|
+
.tokens { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
232
|
+
.token { padding: 3px 7px; border: 1px solid var(--border); border-radius: 3px; color: var(--muted); background: var(--surface-muted); font: 12px/1.45 var(--mono); overflow-wrap: anywhere; }
|
|
233
|
+
.digest-pair { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
234
|
+
.digest-pair small { display: block; margin-top: 3px; color: var(--subtle); }
|
|
235
|
+
details.source, details.scope, details.projection, details.finding { overflow: clip; }
|
|
236
|
+
details.source > summary, details.scope > summary, details.projection > summary, details.finding > summary { min-height: 56px; display: grid; grid-template-columns: minmax(180px, 1fr) minmax(130px, .6fr) auto; align-items: center; gap: 12px; padding: 11px 14px; list-style-position: inside; cursor: pointer; }
|
|
237
|
+
details.source > summary:hover, details.scope > summary:hover, details.projection > summary:hover, details.finding > summary:hover { background: var(--surface-muted); }
|
|
238
|
+
details.source[open] > summary, details.scope[open] > summary, details.projection[open] > summary, details.finding[open] > summary { border-bottom: 1px solid var(--border); background: var(--surface-muted); }
|
|
239
|
+
.detail-body { padding: 16px; }
|
|
240
|
+
.relation-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
241
|
+
.relation-grid section { min-width: 0; padding: 12px; border-left: 2px solid var(--border-strong); background: var(--surface-muted); }
|
|
242
|
+
.relation-grid h4 { margin-bottom: 8px; }
|
|
243
|
+
.empty { padding: 40px 18px; border: 1px dashed var(--border-strong); border-radius: var(--radius); color: var(--muted); text-align: center; background: var(--surface); }
|
|
244
|
+
.empty strong { display: block; color: var(--text); font-size: 16px; }
|
|
245
|
+
.empty p { max-width: 55ch; margin: 7px auto 0; }
|
|
246
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
247
|
+
.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
|
|
248
|
+
@media (max-width: 1023px) {
|
|
249
|
+
.app { display: block; }
|
|
250
|
+
.sidebar { z-index: 20; position: sticky; height: auto; padding: 8px 16px; border-right: 0; border-bottom: 1px solid var(--border); }
|
|
251
|
+
.brand { display: none; }
|
|
252
|
+
.nav { grid-template-columns: repeat(6, minmax(80px, 1fr)); margin: 0; }
|
|
253
|
+
.nav button { justify-content: center; border-left: 0; border-bottom: 3px solid transparent; text-align: center; }
|
|
254
|
+
.nav-label { justify-items: center; }
|
|
255
|
+
.nav button[aria-selected="true"] { border-bottom-color: var(--accent); }
|
|
256
|
+
.nav-count, .sidebar-note { display: none; }
|
|
257
|
+
.main { padding-top: 18px; }
|
|
258
|
+
.split { grid-template-columns: 1fr; }
|
|
259
|
+
}
|
|
260
|
+
@media (max-width: 767px) {
|
|
261
|
+
body { font-size: 16px; }
|
|
262
|
+
.sidebar { position: static; }
|
|
263
|
+
.nav { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
264
|
+
.masthead, .section-head { grid-template-columns: 1fr; gap: 12px; }
|
|
265
|
+
.locale-switch { justify-self: start; }
|
|
266
|
+
.digest { margin-top: 0; }
|
|
267
|
+
.health-banner { grid-template-columns: 1fr; }
|
|
268
|
+
.health-code { text-align: left; }
|
|
269
|
+
.health-banner > div:last-child { justify-items: start; }
|
|
270
|
+
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
271
|
+
.metric:nth-child(2) { border-right: 0; }
|
|
272
|
+
.metric:nth-child(-n+2) { border-bottom: 1px solid var(--border); }
|
|
273
|
+
.flow { grid-template-columns: 1fr; }
|
|
274
|
+
.flow-step { border-right: 0; border-bottom: 1px solid var(--border); }
|
|
275
|
+
.flow-step:last-child { border-bottom: 0; }
|
|
276
|
+
.flow-step:not(:last-child)::after { top: auto; right: 20px; bottom: -5px; transform: rotate(135deg); }
|
|
277
|
+
.knowledge-strip article { grid-template-columns: 1fr; gap: 4px; }
|
|
278
|
+
.toolbar { grid-template-columns: 1fr; }
|
|
279
|
+
.audit-grid, .relation-grid { grid-template-columns: 1fr; }
|
|
280
|
+
details.source > summary, details.scope > summary, details.projection > summary, details.finding > summary { grid-template-columns: 1fr; align-items: start; }
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
const SCRIPT = String.raw`
|
|
285
|
+
(() => {
|
|
286
|
+
const localeButtons = Array.from(document.querySelectorAll('#locale-switch button'));
|
|
287
|
+
const setLocale = (locale) => {
|
|
288
|
+
document.documentElement.lang = locale;
|
|
289
|
+
localeButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.value === locale)));
|
|
290
|
+
document.querySelectorAll('[data-placeholder-zh][data-placeholder-en]').forEach((control) => {
|
|
291
|
+
control.placeholder = locale === 'zh-CN' ? control.dataset.placeholderZh : control.dataset.placeholderEn;
|
|
292
|
+
});
|
|
293
|
+
document.querySelectorAll('[data-label-zh][data-label-en]').forEach((option) => {
|
|
294
|
+
option.textContent = locale === 'zh-CN' ? option.dataset.labelZh : option.dataset.labelEn;
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
localeButtons.forEach((button) => button.addEventListener('click', () => setLocale(button.value)));
|
|
298
|
+
|
|
299
|
+
const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
|
|
300
|
+
const panels = Array.from(document.querySelectorAll('[role="tabpanel"]'));
|
|
301
|
+
const activate = (panelId, moveFocus = false) => {
|
|
302
|
+
tabs.forEach((tab) => {
|
|
303
|
+
const selected = tab.getAttribute('aria-controls') === panelId;
|
|
304
|
+
tab.setAttribute('aria-selected', String(selected));
|
|
305
|
+
tab.tabIndex = selected ? 0 : -1;
|
|
306
|
+
if (selected && moveFocus) tab.focus();
|
|
307
|
+
});
|
|
308
|
+
panels.forEach((panel) => { panel.hidden = panel.id !== panelId; });
|
|
309
|
+
};
|
|
310
|
+
tabs.forEach((tab, index) => {
|
|
311
|
+
tab.addEventListener('click', () => activate(tab.getAttribute('aria-controls')));
|
|
312
|
+
tab.addEventListener('keydown', (event) => {
|
|
313
|
+
const previous = event.key === 'ArrowLeft' || event.key === 'ArrowUp';
|
|
314
|
+
const next = event.key === 'ArrowRight' || event.key === 'ArrowDown';
|
|
315
|
+
const edge = event.key === 'Home' || event.key === 'End';
|
|
316
|
+
if (!previous && !next && !edge) return;
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
const targetIndex = event.key === 'Home' ? 0 : event.key === 'End'
|
|
319
|
+
? tabs.length - 1
|
|
320
|
+
: (index + (previous ? -1 : 1) + tabs.length) % tabs.length;
|
|
321
|
+
const target = tabs[targetIndex];
|
|
322
|
+
activate(target.getAttribute('aria-controls'), true);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const query = document.querySelector('#knowledge-query');
|
|
327
|
+
const status = document.querySelector('#knowledge-status');
|
|
328
|
+
const kind = document.querySelector('#knowledge-kind');
|
|
329
|
+
const scope = document.querySelector('#knowledge-scope');
|
|
330
|
+
const records = Array.from(document.querySelectorAll('.knowledge-record'));
|
|
331
|
+
const searchText = new Map(records.map((record) => [record, record.textContent.toLowerCase()]));
|
|
332
|
+
const count = document.querySelector('#knowledge-result-count');
|
|
333
|
+
const applyFilters = () => {
|
|
334
|
+
const needle = query.value.trim().toLowerCase();
|
|
335
|
+
let visible = 0;
|
|
336
|
+
records.forEach((record) => {
|
|
337
|
+
const match = (!needle || searchText.get(record).includes(needle)) &&
|
|
338
|
+
(!status.value || record.dataset.status === status.value) &&
|
|
339
|
+
(!kind.value || record.dataset.kind === kind.value) &&
|
|
340
|
+
(!scope.value || record.dataset.scope === scope.value);
|
|
341
|
+
record.hidden = !match;
|
|
342
|
+
if (match) visible += 1;
|
|
343
|
+
});
|
|
344
|
+
count.textContent = String(visible);
|
|
345
|
+
};
|
|
346
|
+
[query, status, kind, scope].forEach((control) => control.addEventListener('input', applyFilters));
|
|
347
|
+
query.addEventListener('keydown', (event) => {
|
|
348
|
+
if (event.key !== 'Escape' || !query.value) return;
|
|
349
|
+
query.value = '';
|
|
350
|
+
applyFilters();
|
|
351
|
+
});
|
|
352
|
+
})();
|
|
353
|
+
`;
|
|
354
|
+
|
|
355
|
+
function escapeHtml(value) {
|
|
356
|
+
return String(value)
|
|
357
|
+
.replaceAll("&", "&")
|
|
358
|
+
.replaceAll("<", "<")
|
|
359
|
+
.replaceAll(">", ">")
|
|
360
|
+
.replaceAll('"', """)
|
|
361
|
+
.replaceAll("'", "'");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function bi(zh, en) {
|
|
365
|
+
return `<span lang="zh-CN">${escapeHtml(zh)}</span><span lang="en">${escapeHtml(en)}</span>`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function localizedOption(value, zh, en) {
|
|
369
|
+
return `<option value="${escapeHtml(value)}" data-label-zh="${escapeHtml(zh)}" data-label-en="${escapeHtml(en)}">${escapeHtml(zh)}</option>`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function localizedStatement(statement) {
|
|
373
|
+
const match = /^\[zh-CN\] ([^\n]+)\n\[en\] ([\s\S]+)$/u.exec(statement);
|
|
374
|
+
if (match) return bi(match[1], match[2]);
|
|
375
|
+
return `${escapeHtml(statement)}<small class="translation-missing" lang="zh-CN">暂无中文译文</small>`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function kindLabel(kind) {
|
|
379
|
+
const labels = KIND_LABELS.get(kind);
|
|
380
|
+
return labels ? bi(...labels) : escapeHtml(kind);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function contentHash(value) {
|
|
384
|
+
return createHash("sha256").update(value).digest("base64");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function shortDigest(value) {
|
|
388
|
+
if (!value) return "不可用 / Unavailable";
|
|
389
|
+
return `${value.slice(0, 15)}…${value.slice(-8)}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function iconSvg(status) {
|
|
393
|
+
if (["clean", "approved", "unchanged", "healthy"].includes(status)) {
|
|
394
|
+
return '<svg viewBox="0 0 20 20" aria-hidden="true"><use href="#status-ok"></use></svg>';
|
|
395
|
+
}
|
|
396
|
+
if (status === "conflict") {
|
|
397
|
+
return '<svg viewBox="0 0 20 20" aria-hidden="true"><use href="#status-conflict"></use></svg>';
|
|
398
|
+
}
|
|
399
|
+
if (["blocked", "changed", "missing", "unreadable", "diverged"].includes(status)) {
|
|
400
|
+
return '<svg viewBox="0 0 20 20" aria-hidden="true"><use href="#status-error"></use></svg>';
|
|
401
|
+
}
|
|
402
|
+
return '<svg viewBox="0 0 20 20" aria-hidden="true"><use href="#status-attention"></use></svg>';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function statusBadge(status) {
|
|
406
|
+
const labels = STATUS_LABELS.get(status) ?? [status, status];
|
|
407
|
+
return `<span class="status status-${escapeHtml(status)}">${iconSvg(status)}${bi(...labels)}</span>`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function tokenList(values, empty = ["无", "None"]) {
|
|
411
|
+
if (!values || values.length === 0) return `<span class="token">${Array.isArray(empty) ? bi(...empty) : escapeHtml(empty)}</span>`;
|
|
412
|
+
return `<span class="tokens">${values.map((value) => `<span class="token">${escapeHtml(value)}</span>`).join("")}</span>`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function auditField(term, value, options = {}) {
|
|
416
|
+
return `<div><dt>${options.htmlTerm ? term : escapeHtml(term)}</dt><dd${options.mono ? ' class="mono"' : ""}>${value}</dd></div>`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function renderValueTree(value) {
|
|
420
|
+
if (Array.isArray(value)) {
|
|
421
|
+
if (value.length === 0) return '<span class="mono">[]</span>';
|
|
422
|
+
return `<ol class="value-tree">${value.map((entry) => `<li>${renderValueTree(entry)}</li>`).join("")}</ol>`;
|
|
423
|
+
}
|
|
424
|
+
if (value && typeof value === "object") {
|
|
425
|
+
const entries = Object.keys(value).sort().map((key) => `<dt>${escapeHtml(key)}</dt><dd>${renderValueTree(value[key])}</dd>`).join("");
|
|
426
|
+
return entries ? `<dl class="value-tree">${entries}</dl>` : '<span class="mono">{}</span>';
|
|
427
|
+
}
|
|
428
|
+
return `<span class="mono">${escapeHtml(canonicalJson(value))}</span>`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderOverview(model) {
|
|
432
|
+
const approved = model.items.filter((item) => item.status === "approved").slice(0, 4);
|
|
433
|
+
const policies = model.items.filter((item) => item.status === "approved" && item.kind === "policy").slice(0, 5);
|
|
434
|
+
const projectionText = model.summary.projections.configured === 0
|
|
435
|
+
? bi("未配置", "Not configured")
|
|
436
|
+
: bi(`${model.summary.projections.healthy}/${model.summary.projections.configured} 正常`, `${model.summary.projections.healthy}/${model.summary.projections.configured} healthy`);
|
|
437
|
+
const healthLabels = STATUS_LABELS.get(model.health.status) ?? [model.health.status, model.health.status];
|
|
438
|
+
return `
|
|
439
|
+
<section class="panel" role="tabpanel" id="view-overview" aria-labelledby="tab-overview">
|
|
440
|
+
<div class="health-banner">
|
|
441
|
+
<div><p class="eyebrow">${bi("治理快照", "Governance snapshot")}</p><h2>${bi(`合同健康:${healthLabels[0]}`, `Contract health: ${healthLabels[1]}`)}</h2><p>${bi("这是当前 Project Contract 的只读检查结果。状态、来源与投影覆盖分别计算。", "This is a read-only check of the current Project Contract. Health, sources, and projection coverage are evaluated separately.")}</p></div>
|
|
442
|
+
<div><div class="health-code">${bi(`检查退出码 ${model.health.exitCode} · 问题 ${model.health.findingCount}`, `CHECK EXIT ${model.health.exitCode} · FINDINGS ${model.health.findingCount}`)}</div>${statusBadge(model.health.status)}</div>
|
|
443
|
+
</div>
|
|
444
|
+
<span class="sr-only" id="metrics-label">${bi("治理摘要", "Governance summary")}</span><div class="metrics" aria-labelledby="metrics-label">
|
|
445
|
+
<div class="metric"><span>${bi("依据来源", "Sources")}</span><strong>${model.summary.sources.referenced}/${model.summary.sources.registered}</strong><small>${bi("已引用 / 已登记", "Referenced / Registered")}</small></div>
|
|
446
|
+
<div class="metric"><span>${bi("已批准知识", "Approved knowledge")}</span><strong>${model.summary.items.approved}</strong><small>${bi(`待批准 ${model.summary.items.proposed}`, `Proposed ${model.summary.items.proposed}`)}</small></div>
|
|
447
|
+
<div class="metric"><span>${bi("待处理问题", "Findings")}</span><strong>${model.health.findingCount}</strong><small>${bi(`退出码 ${model.health.exitCode}`, `Exit code ${model.health.exitCode}`)}</small></div>
|
|
448
|
+
<div class="metric"><span>${bi("Agent 投影", "Agent projections")}</span><strong>${model.summary.projections.configured}</strong><small>${projectionText}</small></div>
|
|
449
|
+
</div>
|
|
450
|
+
<span class="sr-only" id="flow-label">${bi("治理链路", "Governance flow")}</span><div class="flow" aria-labelledby="flow-label">
|
|
451
|
+
<div class="flow-step"><small>${bi("输入", "INPUT")}</small><strong>${bi("依据来源", "Sources")}</strong><span>${bi(`${model.summary.sources.registered} 个登记入口`, `${model.summary.sources.registered} registered`)}</span></div>
|
|
452
|
+
<div class="flow-step"><small>${bi("权威", "AUTHORITY")}</small><strong>${bi("项目合同", "Project Contract")}</strong><span>${bi(`${model.summary.items.approved} 个已批准知识项`, `${model.summary.items.approved} approved items`)}</span></div>
|
|
453
|
+
<div class="flow-step"><small>${bi("编译", "COMPILATION")}</small><strong>${bi("范围解释", "Scope")}</strong><span>${bi(`${model.scopeViews.length} 个已知路径`, `${model.scopeViews.length} known paths`)}</span></div>
|
|
454
|
+
<div class="flow-step"><small>${bi("输出", "OUTPUT")}</small><strong>${bi("Agent 投影", "Agent projections")}</strong><span>${projectionText}</span></div>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="split">
|
|
457
|
+
<section class="block"><header class="block-head"><h3>${bi("这个项目已经批准了什么", "Approved knowledge")}</h3><span>${bi(`${model.summary.items.approved} 已批准`, `${model.summary.items.approved} APPROVED`)}</span></header><div class="knowledge-strip">${approved.length ? approved.map((item) => `<article><h4><span class="original-label">${bi("知识解释", "Knowledge statement")}</span>${localizedStatement(item.statement)}</h4><p>${escapeHtml(canonicalJson(item.value))}</p></article>`).join("") : `<div class="notice">${bi("当前没有已批准知识。", "No approved knowledge.")}</div>`}</div></section>
|
|
458
|
+
<section class="block"><header class="block-head"><h3>${bi("必须遵守", "Policies")}</h3><span>${bi("规则", "POLICY")}</span></header><div class="knowledge-strip policy-list">${policies.length ? policies.map((item) => `<article><h4><span class="original-label">${bi("知识解释", "Knowledge statement")}</span>${localizedStatement(item.statement)}</h4><p class="mono">${escapeHtml(item.id)}</p></article>`).join("") : `<div class="notice">${bi("当前没有已批准规则。", "No approved policies.")}</div>`}</div></section>
|
|
459
|
+
</div>
|
|
460
|
+
${model.summary.projections.configured === 0 ? `<div class="notice notice-spaced"><strong>${bi("尚未配置受管投影。", "Managed projections are not configured.")}</strong><br>${bi("这不影响 Project Contract 健康,投影覆盖与合同健康分别计算。", "This does not affect Contract health; projection coverage is evaluated separately.")}</div>` : ""}
|
|
461
|
+
</section>`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function renderItem(item, sourceMap, effectiveScopePaths) {
|
|
465
|
+
const approval = item.approval
|
|
466
|
+
? `${escapeHtml(item.approval.by)},${escapeHtml(item.approval.at)}${item.approval.rationale ? `<br>${escapeHtml(item.approval.rationale)}` : ""}`
|
|
467
|
+
: bi("未批准", "Not approved");
|
|
468
|
+
const canonicalValue = canonicalJson(item.value);
|
|
469
|
+
return `<article class="record knowledge-record" data-status="${escapeHtml(item.status)}" data-kind="${escapeHtml(item.kind)}" data-scope="${escapeHtml(item.scope.kind)}">
|
|
470
|
+
<div class="record-head"><div><span class="original-label">${bi("知识解释", "Knowledge statement")}</span><h3>${localizedStatement(item.statement)}</h3><div class="id">${escapeHtml(item.id)} / ${kindLabel(item.kind)} / ${escapeHtml(item.scopeLabel)}</div></div>${statusBadge(item.status)}</div>
|
|
471
|
+
${item.status === "proposed" ? `<div class="notice notice-spaced"><strong>${bi("等待人工批准。", "Awaiting human approval.")}</strong><br>${bi("该知识不会进入 Context Bundle。", "This item is excluded from Context Bundles.")}</div>` : ""}
|
|
472
|
+
<details class="structured-value" open><summary>${bi("结构化值", "Structured value")}</summary>${renderValueTree(item.value)}</details>
|
|
473
|
+
<details class="raw-value"><summary>${bi("查看规范 JSON", "View canonical JSON")}</summary><pre class="value"><code>${escapeHtml(canonicalValue)}</code></pre></details>
|
|
474
|
+
<details class="audit"><summary>${bi("查看依据与审计字段", "Provenance and audit")}</summary><dl class="audit-grid">
|
|
475
|
+
${item.subject !== item.id ? auditField(bi("主题", "Subject"), escapeHtml(item.subject), { mono: true, htmlTerm: true }) : ""}
|
|
476
|
+
${auditField(bi("内容指纹", "Digest"), escapeHtml(item.itemDigest), { mono: true, htmlTerm: true })}
|
|
477
|
+
${auditField(bi("依据来源", "Sources"), tokenList(item.sources.map((id) => `${id}: ${sourceMap.get(id)?.locator ?? "unknown"}`)), { htmlTerm: true })}
|
|
478
|
+
${auditField(bi("批准记录", "Approval"), approval, { htmlTerm: true })}
|
|
479
|
+
${item.overrides.length ? auditField(bi("覆盖关系", "Overrides"), tokenList(item.overrides), { htmlTerm: true }) : ""}
|
|
480
|
+
${auditField(bi("生效范围", "Effective scopes"), tokenList(effectiveScopePaths), { htmlTerm: true })}
|
|
481
|
+
${item.verification && item.verification.kind !== "none" ? auditField(bi("验证", "Verification"), `<pre class="value"><code>${escapeHtml(canonicalJson(item.verification))}</code></pre>`, { htmlTerm: true }) : ""}
|
|
482
|
+
</dl></details>
|
|
483
|
+
</article>`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function renderKnowledge(model) {
|
|
487
|
+
const sourceMap = new Map(model.sources.map((source) => [source.id, source]));
|
|
488
|
+
const effectiveScopes = new Map(model.items.map((item) => [item.id, []]));
|
|
489
|
+
for (const view of model.scopeViews) {
|
|
490
|
+
for (const id of view.itemIds) effectiveScopes.get(id)?.push(view.path);
|
|
491
|
+
}
|
|
492
|
+
return `<section class="panel" role="tabpanel" id="view-knowledge" aria-labelledby="tab-knowledge" hidden>
|
|
493
|
+
<header class="section-head"><h2>${bi("项目知识", "Knowledge")}</h2><p>${bi("知识解释和 value 是主要内容,ID、digest 与 provenance 保留在审计详情中。", "Knowledge statements and values are primary; IDs, digests, and provenance remain available in audit details.")}</p></header>
|
|
494
|
+
<span class="sr-only" id="filters-label">${bi("知识筛选", "Knowledge filters")}</span><div class="toolbar" aria-labelledby="filters-label">
|
|
495
|
+
<div class="field"><label for="knowledge-query">${bi("搜索已加载快照", "Search snapshot")}</label><input id="knowledge-query" type="search" autocomplete="off" data-placeholder-zh="搜索解释、value 或 ID" data-placeholder-en="Search statement, value, or ID" placeholder="搜索解释、value 或 ID"></div>
|
|
496
|
+
<div class="field"><label for="knowledge-status">${bi("状态", "Status")}</label><select id="knowledge-status">${localizedOption("", "全部状态", "All statuses")}${localizedOption("proposed", "待批准", "Proposed")}${localizedOption("approved", "已批准", "Approved")}${localizedOption("deprecated", "已废弃", "Deprecated")}</select></div>
|
|
497
|
+
<div class="field"><label for="knowledge-kind">${bi("类型", "Kind")}</label><select id="knowledge-kind">${localizedOption("", "全部类型", "All kinds")}${[...KIND_LABELS].map(([value, labels]) => localizedOption(value, ...labels)).join("")}</select></div>
|
|
498
|
+
<div class="field"><label for="knowledge-scope">${bi("范围", "Scope")}</label><select id="knowledge-scope">${localizedOption("", "全部范围", "All scopes")}${localizedOption("project", "项目", "Project")}${localizedOption("path-prefix", "路径前缀", "Path prefix")}${localizedOption("file", "文件", "File")}</select></div>
|
|
499
|
+
</div>
|
|
500
|
+
<p class="result-line" aria-live="polite"><span><strong id="knowledge-result-count">${model.items.length}</strong> ${bi("个知识项", "items")}</span><span>${bi("Esc 清空搜索", "Esc clears search")}</span></p>
|
|
501
|
+
<div class="records">${model.items.map((item) => renderItem(item, sourceMap, effectiveScopes.get(item.id))).join("")}</div>
|
|
502
|
+
</section>`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function digestFields(source) {
|
|
506
|
+
const digestValue = (value) => value
|
|
507
|
+
? `<span class="digest-pair">${escapeHtml(shortDigest(value))}<small>${escapeHtml(value)}</small></span>`
|
|
508
|
+
: bi("不可用", "Unavailable");
|
|
509
|
+
const values = [source.contractDigest, source.lockedDigest, source.currentDigest].filter(Boolean);
|
|
510
|
+
if (values.length === 0 && !source.reason) return "";
|
|
511
|
+
if (values.length >= 2 && new Set(values).size === 1) {
|
|
512
|
+
return `<dl class="audit-grid">${auditField(bi("已匹配指纹", "Matched digest"), digestValue(values[0]), { htmlTerm: true })}</dl>`;
|
|
513
|
+
}
|
|
514
|
+
return `<dl class="audit-grid">
|
|
515
|
+
${source.contractDigest ? auditField(bi("合同指纹", "Contract digest"), digestValue(source.contractDigest), { htmlTerm: true }) : ""}
|
|
516
|
+
${source.lockedDigest ? auditField(bi("锁定指纹", "Locked digest"), digestValue(source.lockedDigest), { htmlTerm: true }) : ""}
|
|
517
|
+
${source.currentDigest ? auditField(bi("当前指纹", "Current digest"), digestValue(source.currentDigest), { htmlTerm: true }) : ""}
|
|
518
|
+
${source.reason ? auditField(bi("原因", "Reason"), escapeHtml(source.reason), { mono: true, htmlTerm: true }) : ""}
|
|
519
|
+
</dl>`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function renderSource(source) {
|
|
523
|
+
const affectedCount = new Set([...source.directItemIds, ...source.verificationItemIds, ...source.overrideDependentItemIds]).size;
|
|
524
|
+
const projectionCount = new Set([...source.directProjectionPaths, ...source.staleProjectionPaths]).size;
|
|
525
|
+
const relations = [
|
|
526
|
+
[source.directItemIds, ["直接引用", "Direct references"]],
|
|
527
|
+
[source.verificationItemIds, ["验证引用", "Verification references"]],
|
|
528
|
+
[source.overrideDependentItemIds, ["覆盖依赖项", "Override dependents"]],
|
|
529
|
+
[source.fallbackItemIds, ["回退项", "Fallback items"]],
|
|
530
|
+
[source.directProjectionPaths, ["直接投影", "Direct projections"]],
|
|
531
|
+
[source.staleProjectionPaths, ["接受后过期", "Stale after accept"]],
|
|
532
|
+
].filter(([values]) => values.length > 0);
|
|
533
|
+
const deprecation = source.deprecation
|
|
534
|
+
? `<dl class="audit-grid">
|
|
535
|
+
${auditField(bi("废弃负责人", "Deprecated by"), escapeHtml(source.deprecation.by), { htmlTerm: true })}
|
|
536
|
+
${auditField(bi("废弃时间", "Deprecated at"), escapeHtml(source.deprecation.at), { mono: true, htmlTerm: true })}
|
|
537
|
+
${auditField(bi("废弃原因", "Deprecation rationale"), escapeHtml(source.deprecation.rationale), { htmlTerm: true })}
|
|
538
|
+
</dl>`
|
|
539
|
+
: "";
|
|
540
|
+
return `<details class="record source"><summary><span><strong>${escapeHtml(source.id)}</strong><br><span class="mono">${escapeHtml(source.locator)}</span></span><span>${bi(`类型 ${source.kind} · 引用 ${source.referenceCount} · 影响 ${affectedCount} · 投影 ${projectionCount}`, `Kind ${source.kind} · References ${source.referenceCount} · Affects ${affectedCount} · Projections ${projectionCount}`)}</span>${statusBadge(source.status)}</summary>
|
|
541
|
+
<div class="detail-body">${digestFields(source)}${deprecation}${relations.length ? `<div class="relation-grid">${relations.map(([values, labels]) => `<section><h4>${bi(...labels)}</h4>${tokenList(values)}</section>`).join("")}</div>` : ""}</div>
|
|
542
|
+
</details>`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function renderSources(model) {
|
|
546
|
+
return `<section class="panel" role="tabpanel" id="view-sources" aria-labelledby="tab-sources" hidden>
|
|
547
|
+
<header class="section-head"><h2>${bi("依据来源", "Sources")}</h2><p>${bi("这里只显示 locator、checkpoint 和关系,不读取或展示来源正文与 JSON Pointer 当前值。", "Only locators, checkpoints, and relationships are shown; source bodies and current JSON Pointer values remain private.")}</p></header>
|
|
548
|
+
<div class="records">${model.sources.map(renderSource).join("")}</div>
|
|
549
|
+
</section>`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function renderScope(scope) {
|
|
553
|
+
const groups = Object.entries(scope.groups).filter(([, ids]) => ids.length > 0);
|
|
554
|
+
return `<details class="record scope"><summary><strong class="mono">${escapeHtml(scope.path)}</strong><span>${bi(`${scope.itemIds.length} 个生效知识项`, `${scope.itemIds.length} effective items`)}</span>${scope.error ? statusBadge("blocked") : statusBadge("clean")}</summary>
|
|
555
|
+
<div class="detail-body">${scope.error ? `<div class="notice"><strong>${escapeHtml(scope.error.code)}</strong><br>${escapeHtml(scope.error.message)}</div>` : groups.length ? `<div class="relation-grid">${groups.map(([kind, ids]) => `<section><h4>${kindLabel(kind)}</h4>${tokenList(ids)}</section>`).join("")}</div>` : `<div class="empty"><strong>${bi("没有生效知识", "No effective knowledge")}</strong><p>${bi("该已知路径当前没有已批准知识项。", "This known path has no approved items.")}</p></div>`}
|
|
556
|
+
${scope.excludedItems.length ? `<div class="notice notice-spaced"><strong>${bi("被覆盖排除", "Excluded by override")}</strong><br>${tokenList(scope.excludedItems.map((item) => item.id))}</div>` : ""}
|
|
557
|
+
${scope.siblingDifferences.length ? `<div class="notice notice-spaced"><strong>${bi("同级路径差异", "Sibling differences")}</strong>${scope.siblingDifferences.map((difference) => `<dl class="audit-grid"><div><dt>${escapeHtml(difference.path)} ${bi("仅当前路径", "Only here")}</dt><dd>${tokenList(difference.onlyHereItemIds)}</dd></div><div><dt>${escapeHtml(difference.path)} ${bi("仅对方路径", "Only there")}</dt><dd>${tokenList(difference.onlyThereItemIds)}</dd></div></dl>`).join("")}</div>` : ""}</div>
|
|
558
|
+
</details>`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function renderScopes(model) {
|
|
562
|
+
return `<section class="panel" role="tabpanel" id="view-scopes" aria-labelledby="tab-scopes" hidden>
|
|
563
|
+
<header class="section-head"><h2>${bi("范围解释", "Scopes")}</h2><p>${bi("所有结果都由既有 scope compiler 预计算,只提供真实 Contract 中的已知路径。", "Results are precomputed by the existing scope compiler and limited to known Contract paths.")}</p></header>
|
|
564
|
+
<div class="records">${model.scopeViews.map(renderScope).join("")}</div>
|
|
565
|
+
</section>`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function renderProjection(projection) {
|
|
569
|
+
return `<details class="record projection"><summary><strong class="mono">${escapeHtml(projection.path)}</strong><span>${escapeHtml(projection.target)} · ${bi(`渲染器 ${projection.rendererVersion}`, `Renderer ${projection.rendererVersion}`)}</span>${statusBadge(projection.status)}</summary><div class="detail-body"><dl class="audit-grid">
|
|
570
|
+
${auditField(bi("范围路径", "Scope paths"), tokenList(projection.paths), { htmlTerm: true })}
|
|
571
|
+
${auditField(bi("知识项 ID", "Item IDs"), tokenList(projection.itemIds), { htmlTerm: true })}
|
|
572
|
+
${auditField(bi("问题代码", "Finding codes"), tokenList(projection.findingCodes), { htmlTerm: true })}
|
|
573
|
+
${auditField(bi("合同指纹", "Contract digest"), escapeHtml(projection.contractDigest), { mono: true, htmlTerm: true })}
|
|
574
|
+
</dl></div></details>`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function renderProjections(model) {
|
|
578
|
+
const body = model.projections.length
|
|
579
|
+
? `<div class="records">${model.projections.map(renderProjection).join("")}</div>`
|
|
580
|
+
: `<div class="empty"><strong>${bi("未配置", "Not configured")}</strong><p>${bi("当前没有受管 Agent 投影。这不是错误,也不会被计算为 100% 覆盖。", "No managed Agent projections exist. This is not an error and is never shown as 100% coverage.")}</p></div>`;
|
|
581
|
+
return `<section class="panel" role="tabpanel" id="view-projections" aria-labelledby="tab-projections" hidden><header class="section-head"><h2>${bi("Agent 投影", "Agent projections")}</h2><p>${bi("投影是受管输出,不是第二份真源。看板不会创建、覆盖或重新发布它们。", "Projections are managed outputs, never a second source of truth; this dashboard cannot create, overwrite, or republish them.")}</p></header>${body}</section>`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renderFinding(finding) {
|
|
585
|
+
const labels = FINDING_LABELS.get(finding.code) ?? [finding.code, finding.code];
|
|
586
|
+
return `<details class="record finding"><summary><span><strong>${bi(...labels)}</strong><br><span class="mono">${escapeHtml(finding.code)}</span></span><span class="mono">${escapeHtml(finding.target)}</span>${statusBadge(finding.severity)}</summary><div class="detail-body"><pre class="value"><code>${escapeHtml(canonicalJson(finding))}</code></pre></div></details>`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function renderFindings(model) {
|
|
590
|
+
const body = model.findings.length
|
|
591
|
+
? `<div class="records">${model.findings.map(renderFinding).join("")}</div>`
|
|
592
|
+
: `<div class="empty"><strong>${bi("没有待处理问题", "No findings")}</strong><p>${bi("checker 当前没有报告来源、合同、范围、验证或投影问题。", "The checker reports no source, Contract, scope, verification, or projection findings.")}</p></div>`;
|
|
593
|
+
return `<section class="panel" role="tabpanel" id="view-findings" aria-labelledby="tab-findings" hidden><header class="section-head"><h2>${bi("待处理问题", "Findings")}</h2><p>${bi("这里完整保留 checker code 和 details,不自动修复、不隐藏 warning。", "Checker codes and details remain intact; nothing is repaired automatically or hidden.")}</p></header>${body}</section>`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function renderDashboardHtml(model) {
|
|
597
|
+
const styleHash = contentHash(CSS);
|
|
598
|
+
const scriptHash = contentHash(SCRIPT);
|
|
599
|
+
const csp = `default-src 'none'; style-src 'sha256-${styleHash}'; script-src 'sha256-${scriptHash}'; img-src 'none'; font-src 'none'; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'`;
|
|
600
|
+
const viewCounts = new Map([
|
|
601
|
+
["knowledge", model.items.length],
|
|
602
|
+
["sources", model.sources.length],
|
|
603
|
+
["scopes", model.scopeViews.length],
|
|
604
|
+
["projections", model.projections.length],
|
|
605
|
+
["findings", model.findings.length],
|
|
606
|
+
]);
|
|
607
|
+
const tabs = VIEW_LABELS.map(([view, zhLabel, enLabel], index) => `<button id="tab-${view}" role="tab" aria-controls="view-${view}" aria-selected="${index === 0}" tabindex="${index === 0 ? 0 : -1}"><span class="nav-label"><strong lang="zh-CN">${zhLabel}</strong><strong lang="en">${enLabel}</strong></span>${viewCounts.has(view) ? `<span class="nav-count">${viewCounts.get(view)}</span>` : ""}</button>`).join("");
|
|
608
|
+
return `<!doctype html>
|
|
609
|
+
<html lang="zh-CN">
|
|
610
|
+
<head>
|
|
611
|
+
<meta charset="utf-8">
|
|
612
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
613
|
+
<meta name="color-scheme" content="light dark">
|
|
614
|
+
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
615
|
+
<title>${escapeHtml(model.project.name)} · Project Context</title>
|
|
616
|
+
<style>${CSS}</style>
|
|
617
|
+
</head>
|
|
618
|
+
<body>
|
|
619
|
+
<svg class="svg-defs" aria-hidden="true"><defs><g id="status-ok"><circle cx="10" cy="10" r="7"></circle><path d="m6.5 10 2.2 2.2 4.8-5"></path></g><g id="status-conflict"><path d="M10 3 17 16H3Z"></path><path d="M10 7v4M10 14v.1"></path></g><g id="status-error"><circle cx="10" cy="10" r="7"></circle><path d="m7.5 7.5 5 5m0-5-5 5"></path></g><g id="status-attention"><circle cx="10" cy="10" r="7"></circle><path d="M10 6.5v4.5M10 14v.1"></path></g></defs></svg>
|
|
620
|
+
<a class="skip-link" href="#main-content">${bi("跳到主要内容", "Skip to content")}</a>
|
|
621
|
+
<div class="app">
|
|
622
|
+
<aside class="sidebar"><div class="brand"><div class="brand-mark"><span class="brand-glyph" aria-hidden="true">PC</span><div><strong>${bi("项目上下文", "Project Context")}</strong><span>${bi("只读治理", "READ-ONLY GOVERNANCE")}</span></div></div></div><span class="sr-only" id="nav-label">${bi("治理看板视图", "Governance dashboard views")}</span><nav class="nav" role="tablist" aria-labelledby="nav-label">${tabs}</nav><p class="sidebar-note">${bi("本地快照", "LOCAL SNAPSHOT")}<br>${bi("无网络 · 无写入", "NO NETWORK · NO WRITES")}</p></aside>
|
|
623
|
+
<main class="main" id="main-content" tabindex="-1">
|
|
624
|
+
<header class="masthead"><div><p class="eyebrow">${bi("项目治理", "Project governance")}</p><h1>${escapeHtml(model.project.name)}</h1><p>${bi("已批准项目知识、来源、范围与投影状态", "Approved project knowledge, sources, scopes, and projection status")}</p></div><div class="masthead-tools"><span class="sr-only" id="locale-label">${bi("显示语言", "Display language")}</span><div class="locale-switch" id="locale-switch" role="group" aria-labelledby="locale-label"><button type="button" value="zh-CN" aria-pressed="true">中文</button><button type="button" value="en" aria-pressed="false">English</button></div><div class="digest"><strong>${bi("快照标识", "Snapshot identity")}</strong>${bi("项目合同", "Contract")} ${escapeHtml(shortDigest(model.digests.contract))}<br><span class="sr-only">${bi("完整内容指纹", "Full digest")}:</span>${escapeHtml(model.digests.contract)}</div></div></header>
|
|
625
|
+
${renderOverview(model)}
|
|
626
|
+
${renderKnowledge(model)}
|
|
627
|
+
${renderSources(model)}
|
|
628
|
+
${renderScopes(model)}
|
|
629
|
+
${renderProjections(model)}
|
|
630
|
+
${renderFindings(model)}
|
|
631
|
+
</main>
|
|
632
|
+
</div>
|
|
633
|
+
<script>${SCRIPT}</script>
|
|
634
|
+
</body>
|
|
635
|
+
</html>
|
|
636
|
+
`;
|
|
637
|
+
}
|