Skip to content

Commit bd73272

Browse files
committed
Build RFC component workbench with evidence-backed outcomes
1 parent 4da578f commit bd73272

8 files changed

Lines changed: 769 additions & 60 deletions

File tree

‎.github/workflows/rfc-portability.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ jobs:
2525
with:
2626
node-version: '22'
2727
- run: python -m pip install -e '.[dev]'
28-
- run: python -m pytest test/test_rfc_application.py test/test_rfc_providers.py test/test_rfc_http.py test/test_rfc_integration.py test/test_rfc_security.py test/test_rfc_migration.py test/test_rfc_graph.py test/test_rfc_markdown.py
28+
- run: python -m pytest test/test_rfc_application.py test/test_rfc_providers.py test/test_rfc_http.py test/test_rfc_integration.py test/test_rfc_security.py test/test_rfc_migration.py test/test_rfc_graph.py test/test_rfc_markdown.py test/test_rfc_components.py

‎doc/architecture/SPEC/rfc_service.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,14 @@
6161
源指令与回调不执行。节点提供受角色限制的工作/验收操作、键盘访问;支持
6262
缩放与下载,SVG 中的工作链接返回受保护的 RFC 页面。
6363

64-
- **正文渲染。** 详情、草稿与导入预览使用随包交付的 markdown-it 15.0.2 渲染
64+
- **组件工作台。** 详情按成果、目标与范围、路线图与工作、设计、验收、风险与参考
65+
组织。共享浏览器投影使用 Markdown 标题 token 与原始行号,保留层级;中英文规则
66+
集中配置,未分类与历史内容折叠保留。工作描述仅挂载到可见的稳定特性编号。
67+
已合并实现与验收独立统计;最近三项成果必须有通过结论及未过期的版本/环境证据,
68+
来源性能陈述不得转为已验证事实。长章节按需渲染,正文版本缓存复用内容节点与图表。
69+
原文编辑在有权限的独立弹窗内完成,保留未提交输入并校验保存版本;不新增 API/表结构。
70+
71+
- **正文渲染。** 内容组件、草稿与导入预览使用随包交付的 markdown-it 15.0.2 渲染
6572
Markdown;禁用源 HTML 与图片加载,链接只允许 HTTP(S) 或当前文档锚点。
6673
原文保留用于编辑、发布和导出。
6774

‎doc/features/rfc-service.md‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,25 @@ not in user action payloads.
4545

4646
## Repositories and a draft
4747

48-
RFC details, draft previews, and import previews display rendered Markdown,
49-
including headings, lists, tables, links, and code blocks. The browser uses the
48+
RFC details are a component-based workbench: Outcomes, goals and scope, interactive
49+
roadmaps and work grouped by track, design, acceptance and evidence, then risks and
50+
references. A shared browser projection parses heading tokens and original source
51+
spans, recognizing common English and Chinese sections. Unclassified content and
52+
historical snapshots remain available as collapsed supporting material. Task
53+
descriptions appear under the matching stable feature ID, never as a second full
54+
roadmap. Long sections render when expanded and unchanged sections are reused.
55+
56+
The top Outcome separates declared goals, implemented work and acceptance counts.
57+
Its three recent verified results require passing criteria with non-stale evidence,
58+
verification version and environment. Merged PRs establish implementation only;
59+
performance prose and historical delivery snapshots remain labeled source claims.
60+
Outcome actions locate the corresponding tasks or acceptance records. Authorized
61+
users edit the complete source in a separate dialog, with revision conflict checks
62+
and unsaved input preserved across close/reopen and work updates. API and storage
63+
formats are unchanged.
64+
65+
Draft previews, import previews, and individual content components display rendered
66+
Markdown, including headings, lists, tables, links, and code blocks. The browser uses the
5067
bundled markdown-it 15.0.2 parser (MIT, shipped with its license), with raw HTML
5168
disabled and external image fetching disabled. Original Markdown remains intact
5269
for editing, publication, and export.

‎src/infermatrix_copilot/rfc_service/http.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"/roadmap-markdown.js": ("markdown-it.min.js", "text/javascript; charset=utf-8"),
3131
"/roadmap-graph.mjs": ("roadmap-graph.mjs", "text/javascript; charset=utf-8"),
3232
"/roadmap-markdown-display.mjs": ("roadmap-markdown-display.mjs", "text/javascript; charset=utf-8"),
33+
"/roadmap-components.mjs": ("roadmap-components.mjs", "text/javascript; charset=utf-8"),
3334
"/roadmap-mermaid.js": ("roadmap-mermaid.js", "text/javascript; charset=utf-8"),
3435
"/style.css": ("style.css", "text/css; charset=utf-8"),
3536
}

‎src/infermatrix_copilot/rfc_service/web/app.js‎

Lines changed: 184 additions & 56 deletions
Large diffs are not rendered by default.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import {displayMarkdown} from "./roadmap-markdown-display.mjs";
2+
3+
/* Classification is a display projection; it never changes the source or tracking model. */
4+
export const sectionRules = [
5+
{category: "supplement", claim: "historical", pattern: /histor(?:y|ical)|timeline|(?:delivery|release|status|progress)\s+(?:snapshot|update)|live\s+pr\s+status|delivery\s+and\s+next\s+steps|(?:执行|交付|发布)顺序|历史|时间线|快照|交付进展|当前\s*pr\s*状态/i},
6+
{category: "scope", pattern: /non[ -]?goals?|out[ -]of[ -]scope|scope|非目标|不(?:在|包括|包含).*范围|范围|边界/i},
7+
{category: "goals", pattern: /(?:^|\b)(?:goals?|objectives?|expected\s+outcomes?)(?:\b|$)|目标|预期成果|期望结果/i},
8+
{category: "risks", pattern: /risks?|open\s+(?:questions?|issues?)|unresolved|feedback\s+period|风险|未决|开放问题|反馈期/i},
9+
{category: "references", pattern: /related\s+work|references?|resources?|bibliography|参考|相关工作|相关资料/i},
10+
{category: "acceptance", pattern: /acceptance|success\s+criteria|validation\s+criteria|验收|成功标准/i},
11+
{category: "design", claim: "source", pattern: /performance|benchmark|measurement|estimat(?:e|ion)|measured|experiment\s+plan|性能|测量|估算|基准|实验计划/i},
12+
{category: "design", pattern: /design|architectur|alternatives?|models?|execution\s+semantics|(?:proposed\s+)?solution|approach|implementation\s+details|pr\s+review\s+contract|事实与依据|备选|方案|设计|模型|架构|执行语义|评审契约/i},
13+
{category: "overview", pattern: /(?:^|\b)(?:topic|summary|overview|motivation|background|problem)(?:\b|$)|概述|摘要|背景|动机|问题/i},
14+
{category: "work", pattern: /feature\s+roadmap|(?:implementation|work)\s+plan|工作计划|工作拆分|工作轨道|实现计划|功能路线图/i},
15+
];
16+
17+
const categories = ["overview", "goals", "scope", "design", "risks", "references", "supplement", "acceptance"];
18+
19+
function classification(title, parent, featureHeading = true) {
20+
if (parent?.featureId) return {category: "work", claim: parent.claim, featureId: parent.featureId};
21+
const feature = featureHeading && title.match(/^([A-Za-z][A-Za-z0-9_-]*)[.、:]\s+(.+)/);
22+
if (feature) return {category: "work", claim: "source", featureId: feature[1]};
23+
// A historical section stays historical even when a child is named "Goals".
24+
if (parent?.claim === "historical") return {category: "supplement", claim: "historical", featureId: null};
25+
// Domain headings inside goals or acceptance must not become model/design claims.
26+
if (["goals", "scope", "risks", "references", "acceptance"].includes(parent?.category)) {
27+
return {category: parent.category, claim: parent.claim, featureId: null};
28+
}
29+
const rule = sectionRules.find(rule => rule.pattern.test(title));
30+
return {category: rule?.category || parent?.category || "supplement", claim: rule?.claim || parent?.claim || null, featureId: null};
31+
}
32+
33+
function commentLines(lines, tokens) {
34+
const fenced = new Set();
35+
for (const token of tokens) if (["fence", "code_block"].includes(token.type) && token.map) {
36+
for (let line = token.map[0]; line < token.map[1]; line += 1) fenced.add(line);
37+
}
38+
const hidden = new Set();
39+
for (let index = 0; index < lines.length; index += 1) {
40+
if (fenced.has(index) || !/^ {0,3}(?:\\)?<!--/.test(lines[index])) continue;
41+
let end = index;
42+
while (end < lines.length && !lines[end].includes("-->")) end += 1;
43+
if (end < lines.length && !lines[end].slice(lines[end].indexOf("-->") + 3).trim()) {
44+
for (let line = index; line <= end; line += 1) hidden.add(line);
45+
index = end;
46+
}
47+
}
48+
return hidden;
49+
}
50+
51+
/** Project original Markdown into exclusive fragments with 1-based source spans. */
52+
export function projectRFC(body, markdownParser) {
53+
const source = String(body || ""), lines = source.split(/\r?\n/);
54+
const tokens = markdownParser.parse(source, {});
55+
const comments = commentLines(lines, tokens);
56+
const suppressed = new Set(comments);
57+
// Match the graph module's source grammar; other diagrams remain readable code.
58+
for (const token of tokens) if (token.type === "fence" && token.markup[0] === "`" && /^mermaid\s*$/.test(token.info) &&
59+
/^\s*(?:flowchart|graph)\s+(?:LR|RL|TD|TB|BT)\b/.test(token.content) && token.map) {
60+
for (let line = token.map[0]; line < token.map[1]; line += 1) suppressed.add(line);
61+
}
62+
const headings = [];
63+
for (let index = 0; index < tokens.length; index += 1) {
64+
const token = tokens[index];
65+
if (token.type !== "heading_open" || token.level !== 0 || !token.map || comments.has(token.map[0])) continue;
66+
const inline = tokens[index + 1];
67+
const title = (inline.children || []).map(child => child.type === "image" ? child.content : child.content || "").join("").trim();
68+
headings.push({title, level: Number(token.tag.slice(1)), start: token.map[0], contentStart: token.map[1]});
69+
}
70+
const result = Object.fromEntries(categories.map(category => [category, []]));
71+
result.tree = [];
72+
result.featureSections = Object.create(null);
73+
const stack = [];
74+
const clean = (start, end) => displayMarkdown(lines.slice(start, end).map((line, offset) => suppressed.has(start + offset) ? "" : line).join("\n")).trim();
75+
const preambleEnd = headings[0]?.start ?? lines.length;
76+
const preamble = clean(0, preambleEnd);
77+
if (preamble) {
78+
const fragment = {id: "section-preamble", title: "概述", level: 0, startLine: 1, endLine: preambleEnd, markdown: preamble, category: "overview", claim: null, featureId: null, parentId: null, children: []};
79+
result.overview.push(fragment); result.tree.push(fragment);
80+
}
81+
headings.forEach((heading, index) => {
82+
while (stack.length && stack.at(-1).level >= heading.level) stack.pop();
83+
const parent = stack.at(-1), next = headings[index + 1];
84+
const assignment = classification(heading.title, parent, heading.level >= 3);
85+
const fragment = {id: `section-${heading.start + 1}`, title: heading.title, level: heading.level,
86+
startLine: heading.start + 1, endLine: next?.start ?? lines.length,
87+
markdown: clean(heading.contentStart, next?.start ?? lines.length),
88+
...assignment, parentId: parent?.id || null, children: []};
89+
if (parent) parent.children.push(fragment); else result.tree.push(fragment);
90+
stack.push(fragment);
91+
// Heading containers have no duplicated body; empty containers remain in the tree.
92+
if (!fragment.markdown) return;
93+
if (fragment.featureId) {
94+
(result.featureSections[fragment.featureId] ||= []).push(fragment);
95+
} else if (fragment.category === "work") {
96+
result.supplement.push({...fragment, category: "supplement"});
97+
} else {
98+
result[fragment.category].push(fragment);
99+
}
100+
});
101+
// Tree spans include descendants while fragment spans remain exclusive.
102+
const finish = nodes => nodes.forEach(node => {
103+
finish(node.children);
104+
node.subtreeEndLine = node.children.at(-1)?.subtreeEndLine ?? node.endLine;
105+
});
106+
finish(result.tree);
107+
return result;
108+
}
109+
110+
function timestamp(value) {
111+
if (value === null || value === undefined || value === "") return null;
112+
const numeric = typeof value === "number" || (typeof value === "string" && /^[+-]?\d+(?:\.\d+)?$/.test(value));
113+
const parsed = numeric ? Number(value) * (Math.abs(Number(value)) < 1e12 ? 1000 : 1) : Date.parse(value);
114+
return Number.isFinite(parsed) ? parsed : null;
115+
}
116+
117+
const evidenceTimestamp = evidence => timestamp(evidence.recorded_at ?? evidence.verified_at ?? evidence.at);
118+
119+
function currentEvidence(criterion) {
120+
return (criterion.evidence || []).filter(evidence => evidence && typeof evidence === "object" && !evidence.stale && evidence.revision && evidence.environment)
121+
.sort((a, b) => (evidenceTimestamp(b) ?? 0) - (evidenceTimestamp(a) ?? 0));
122+
}
123+
124+
/** Outcomes are authorized tracking facts, never performance claims in the prose. */
125+
export function outcomeModel(rfc, projection) {
126+
const features = (rfc.features || []).filter(feature => !feature.dropped);
127+
const criteria = rfc.criteria || [];
128+
const implementation = {total: features.length, implemented: 0, partial: 0, inProgress: 0, blocked: 0, planned: 0};
129+
for (const feature of features) {
130+
const state = feature.implementation || feature.state || "planned";
131+
const key = state === "in_progress" ? "inProgress" : ["implemented", "partial", "blocked"].includes(state) ? state : "planned";
132+
implementation[key] += 1;
133+
}
134+
const acceptance = {total: criteria.length, passed: 0, failed: 0, unverified: 0, waived: 0};
135+
const verified = [];
136+
for (const criterion of criteria) {
137+
const evidence = currentEvidence(criterion);
138+
const verdict = criterion.verdict;
139+
if (verdict === "passing" && evidence.length) {
140+
acceptance.passed += 1;
141+
verified.push({criterionId: criterion.id, featureId: criterion.feature_ids?.find(id => features.some(feature => feature.id === id)) || null,
142+
title: criterion.title, evidence: evidence[0], verifiedAt: evidenceTimestamp(evidence[0]) === null ? null : new Date(evidenceTimestamp(evidence[0])).toISOString()});
143+
} else if (verdict === "failing") acceptance.failed += 1;
144+
else if (verdict === "waived") acceptance.waived += 1;
145+
else acceptance.unverified += 1;
146+
}
147+
verified.sort((a, b) => (timestamp(b.verifiedAt) ?? 0) - (timestamp(a.verifiedAt) ?? 0));
148+
const next = [], seen = new Set();
149+
const add = item => { const key = `${item.kind}:${item.id}`; if (!seen.has(key)) { seen.add(key); next.push(item); } };
150+
for (const feature of features) if (feature.blockers?.length || (feature.implementation || feature.state) === "blocked") {
151+
add({kind: "feature", id: feature.id, featureId: feature.id, criterionId: null, title: feature.title,
152+
reason: feature.blockers?.length ? `等待:${feature.blockers.join("、")}` : "工作被阻塞"});
153+
}
154+
for (const criterion of [...criteria].sort((a, b) => Number(b.verdict === "failing") - Number(a.verdict === "failing"))) {
155+
if (criterion.verdict === "waived" || (criterion.verdict === "passing" && currentEvidence(criterion).length)) continue;
156+
add({kind: "criterion", id: criterion.id, featureId: criterion.feature_ids?.find(id => features.some(feature => feature.id === id)) || null,
157+
criterionId: criterion.id, title: criterion.title, reason: criterion.verdict === "failing" ? "验收未通过,需修复并重新验证" : "补充当前版本与环境的验收证据"});
158+
}
159+
for (const feature of features) if ((feature.implementation || feature.state) !== "implemented") {
160+
add({kind: "feature", id: feature.id, featureId: feature.id, criterionId: null, title: feature.title, reason: "推进剩余实现工作"});
161+
}
162+
if (!features.length && !criteria.length) add({kind: "work", id: "work", featureId: null, criterionId: null, title: "补充工作拆分与验收标准", reason: "当前 RFC 尚未建立可验证的工作计划"});
163+
return {goals: projection.goals, implementation, acceptance, verified: verified.slice(0, 3), next: next.slice(0, 3)};
164+
}

‎src/infermatrix_copilot/rfc_service/web/style.css‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)