@xenonbyte/da-vinci-workflow 0.1.25 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +48 -67
- package/README.zh-CN.md +36 -66
- package/SKILL.md +3 -0
- package/commands/claude/dv/continue.md +5 -0
- package/commands/claude/dv/design.md +1 -0
- package/commands/codex/prompts/dv-continue.md +6 -1
- package/commands/codex/prompts/dv-design.md +1 -0
- package/commands/gemini/dv/continue.toml +5 -0
- package/commands/gemini/dv/design.toml +1 -0
- package/commands/templates/dv-continue.shared.md +33 -0
- package/docs/dv-command-reference.md +45 -2
- package/docs/execution-chain-migration.md +46 -0
- package/docs/execution-chain-plan.md +125 -0
- package/docs/pencil-rendering-workflow.md +9 -7
- package/docs/prompt-entrypoints.md +6 -0
- package/docs/prompt-presets/README.md +4 -0
- package/docs/visual-assist-presets/README.md +4 -0
- package/docs/workflow-examples.md +23 -11
- package/docs/workflow-overview.md +27 -0
- package/docs/zh-CN/dv-command-reference.md +45 -2
- package/docs/zh-CN/execution-chain-migration.md +46 -0
- package/docs/zh-CN/pencil-rendering-workflow.md +9 -7
- package/docs/zh-CN/prompt-entrypoints.md +6 -0
- package/docs/zh-CN/prompt-presets/README.md +5 -1
- package/docs/zh-CN/visual-assist-presets/README.md +5 -1
- package/docs/zh-CN/workflow-examples.md +23 -11
- package/docs/zh-CN/workflow-overview.md +27 -0
- package/examples/greenfield-spec-markupflow/README.md +6 -1
- package/lib/artifact-parsers.js +120 -0
- package/lib/async-offload-worker.js +26 -0
- package/lib/async-offload.js +82 -0
- package/lib/audit-parsers.js +152 -32
- package/lib/audit.js +145 -23
- package/lib/cli.js +1068 -437
- package/lib/diff-spec.js +242 -0
- package/lib/execution-signals.js +136 -0
- package/lib/fs-safety.js +1 -4
- package/lib/icon-aliases.js +7 -7
- package/lib/icon-search.js +21 -14
- package/lib/icon-sync.js +220 -41
- package/lib/install.js +128 -60
- package/lib/lint-bindings.js +143 -0
- package/lib/lint-spec.js +408 -0
- package/lib/lint-tasks.js +176 -0
- package/lib/mcp-runtime-gate.js +4 -7
- package/lib/pen-persistence.js +318 -46
- package/lib/pencil-lock.js +237 -25
- package/lib/pencil-preflight.js +233 -12
- package/lib/pencil-session.js +216 -36
- package/lib/planning-parsers.js +567 -0
- package/lib/scaffold.js +193 -0
- package/lib/scope-check.js +603 -0
- package/lib/sidecars.js +369 -0
- package/lib/supervisor-review.js +82 -35
- package/lib/utils.js +129 -0
- package/lib/verify.js +652 -0
- package/lib/workflow-bootstrap.js +255 -0
- package/lib/workflow-contract.js +107 -0
- package/lib/workflow-persisted-state.js +297 -0
- package/lib/workflow-state.js +785 -0
- package/package.json +21 -3
- package/references/artifact-templates.md +26 -0
- package/references/checkpoints.md +16 -0
- package/references/design-inputs.md +2 -0
- package/references/modes.md +10 -0
- package/references/pencil-design-to-code.md +2 -0
- package/scripts/fixtures/complex-sample.pen +0 -295
- package/scripts/fixtures/mock-pencil.js +0 -49
- package/scripts/test-audit-context-delta.js +0 -446
- package/scripts/test-audit-design-supervisor.js +0 -691
- package/scripts/test-audit-safety.js +0 -92
- package/scripts/test-icon-aliases.js +0 -96
- package/scripts/test-icon-search.js +0 -77
- package/scripts/test-icon-sync.js +0 -178
- package/scripts/test-mcp-runtime-gate.js +0 -287
- package/scripts/test-mode-consistency.js +0 -344
- package/scripts/test-pen-persistence.js +0 -403
- package/scripts/test-pencil-lock.js +0 -130
- package/scripts/test-pencil-preflight.js +0 -169
- package/scripts/test-pencil-session.js +0 -192
- package/scripts/test-persistence-flows.js +0 -345
- package/scripts/test-supervisor-review-cli.js +0 -619
- package/scripts/test-supervisor-review-integration.js +0 -115
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
如果你想看 Pencil 渲染、持久化、门禁和审计的专门说明,请看 [pencil-rendering-workflow.md](/Users/xubo/x-skills/da-vinci/docs/zh-CN/pencil-rendering-workflow.md)。
|
|
16
16
|
如果你想看逐个 `dv:` 命令的职责、下一步推荐和 `verify` 回退规则,请看 [dv-command-reference.md](/Users/xubo/x-skills/da-vinci/docs/zh-CN/dv-command-reference.md)。
|
|
17
17
|
如果你想看约束文件总览(哪些字段是硬门禁、哪些是指导项),请看 [constraint-files.md](/Users/xubo/x-skills/da-vinci/docs/zh-CN/constraint-files.md)。
|
|
18
|
+
如果你想看 sidecar、enforcement、persisted-state 回退和 scaffold 约束,请看 [execution-chain-migration.md](/Users/xubo/x-skills/da-vinci/docs/zh-CN/execution-chain-migration.md)。
|
|
18
19
|
|
|
19
20
|
## 核心契约
|
|
20
21
|
|
|
@@ -42,6 +43,30 @@ Da Vinci 围绕一个固定契约工作:
|
|
|
42
43
|
8. 实现。
|
|
43
44
|
9. 验证需求漂移和设计漂移。
|
|
44
45
|
|
|
46
|
+
## Workflow-State 命令面
|
|
47
|
+
|
|
48
|
+
在选择下一步 `dv:` 路由前,可先使用这些命令判断当前阶段就绪度:
|
|
49
|
+
|
|
50
|
+
- `da-vinci workflow-status --project <path> [--change <id>] [--json]`
|
|
51
|
+
- 从工件真相和部分 audit 可见证据推导阶段、blocker、handoff gate 与路由建议
|
|
52
|
+
- `da-vinci next-step --project <path> [--change <id>] [--json]`
|
|
53
|
+
- 基于同一 workflow-state 模型,给出优先续跑动作
|
|
54
|
+
- `da-vinci lint-spec --project <path> [--change <id>] [--strict] [--json]`
|
|
55
|
+
- 在进入实现规划或实现前,校验运行时 `spec.md` 结构质量
|
|
56
|
+
- 默认 advisory;显式 `--strict` 才会把发现升级为阻断
|
|
57
|
+
- `da-vinci scope-check --project <path> [--change <id>] [--strict] [--json]`
|
|
58
|
+
- 校验 proposal、page-map、运行时 spec、pencil-design、tasks 之间页面/状态传播是否一致
|
|
59
|
+
- 输出页面和状态的机器可读覆盖矩阵,供后续 verify/status 消费
|
|
60
|
+
- `da-vinci generate-sidecars --project <path> [--change <id>] [--json]`
|
|
61
|
+
- 显式生成确定性的 planning sidecars,供 diff 和下游工具消费
|
|
62
|
+
- `da-vinci verify-bindings|verify-implementation|verify-structure|verify-coverage --project <path> [--change <id>] [--strict] [--json]`
|
|
63
|
+
- 校验从 bindings 到实现与结构覆盖的执行链证据
|
|
64
|
+
- `da-vinci diff-spec --project <path> [--change <id>] [--from <sidecars-dir>] [--json]`
|
|
65
|
+
- 报告 spec/tasks/page-map/bindings sidecars 的规范化差异,支持安全续跑
|
|
66
|
+
|
|
67
|
+
这些命令不替代 `da-vinci audit --mode integrity|completion`。
|
|
68
|
+
`audit` 仍是 integrity/completion 的正式真相面。
|
|
69
|
+
|
|
45
70
|
## 标准工件骨架
|
|
46
71
|
|
|
47
72
|
根据 mode 的不同,Da Vinci 一般会围绕这组工件推进:
|
|
@@ -60,6 +85,8 @@ Da Vinci 围绕一个固定契约工作:
|
|
|
60
85
|
|
|
61
86
|
`DA-VINCI.md` 仍然是项目级工作流和视觉约束文件。
|
|
62
87
|
|
|
88
|
+
如果你是从空仓库、文档示例、或只水合到一半的工作区开始,先执行 `da-vinci bootstrap-project --project <project-path> --change <change-id>`,再跑严格 audit。这个命令会补齐最小项目骨架,并 seed 一个工作流管理的项目内 `.pen` 基线。
|
|
89
|
+
|
|
63
90
|
## 分阶段说明
|
|
64
91
|
|
|
65
92
|
### 1. Intake 与 Mode 选择
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# MarkupFlow Forward Test
|
|
2
2
|
|
|
3
|
-
This example is a repo-local
|
|
3
|
+
This example is a repo-local forward-test reference for Da Vinci.
|
|
4
|
+
|
|
5
|
+
Important:
|
|
6
|
+
|
|
7
|
+
- this walkthrough is documentation-first and keeps the narrative artifacts at the repository root for readability
|
|
8
|
+
- it is not intended to pass `da-vinci audit` directly until those artifacts are materialized into the current `.da-vinci/` audit layout with persisted `.pen` session/state metadata
|
|
4
9
|
|
|
5
10
|
Mode:
|
|
6
11
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const { getMarkdownSection, hasMarkdownHeading } = require("./audit-parsers");
|
|
2
|
+
|
|
3
|
+
const RUNTIME_SPEC_SECTION_DEFS = [
|
|
4
|
+
{
|
|
5
|
+
id: "capability",
|
|
6
|
+
heading: "Capability",
|
|
7
|
+
required: false
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "behavior",
|
|
11
|
+
heading: "Behavior",
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "states",
|
|
16
|
+
heading: "States",
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "inputs",
|
|
21
|
+
heading: "Inputs",
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "outputs",
|
|
26
|
+
heading: "Outputs",
|
|
27
|
+
required: true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "acceptance",
|
|
31
|
+
heading: "Acceptance",
|
|
32
|
+
required: true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "edgeCases",
|
|
36
|
+
heading: "Edge Cases",
|
|
37
|
+
required: true
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const LIST_ITEM_PATTERN = /^\s*(?:[-*+]|\d+[.)])\s+(.+?)\s*$/;
|
|
42
|
+
|
|
43
|
+
function extractSectionItems(sectionText) {
|
|
44
|
+
const normalized = String(sectionText || "").replace(/\r\n?/g, "\n").trim();
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines = normalized.split("\n");
|
|
50
|
+
const listItems = [];
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const match = line.match(LIST_ITEM_PATTERN);
|
|
53
|
+
if (!match) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const value = String(match[1] || "").trim();
|
|
57
|
+
if (value) {
|
|
58
|
+
listItems.push(value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (listItems.length > 0) {
|
|
63
|
+
return listItems;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return normalized
|
|
67
|
+
.split(/\n\s*\n/g)
|
|
68
|
+
.map((item) => item.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseRuntimeSpecMarkdown(markdownText) {
|
|
73
|
+
const source = String(markdownText || "");
|
|
74
|
+
const sections = {};
|
|
75
|
+
const missingSections = [];
|
|
76
|
+
const emptySections = [];
|
|
77
|
+
|
|
78
|
+
for (const sectionDef of RUNTIME_SPEC_SECTION_DEFS) {
|
|
79
|
+
const present = hasMarkdownHeading(source, sectionDef.heading, {
|
|
80
|
+
allowParentheticalSuffix: true
|
|
81
|
+
});
|
|
82
|
+
const raw = getMarkdownSection(source, sectionDef.heading, {
|
|
83
|
+
allowParentheticalSuffix: true
|
|
84
|
+
});
|
|
85
|
+
const empty = present && String(raw || "").trim() === "";
|
|
86
|
+
const items = extractSectionItems(raw);
|
|
87
|
+
|
|
88
|
+
sections[sectionDef.id] = {
|
|
89
|
+
heading: sectionDef.heading,
|
|
90
|
+
required: sectionDef.required,
|
|
91
|
+
present,
|
|
92
|
+
empty,
|
|
93
|
+
raw,
|
|
94
|
+
items
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (!sectionDef.required) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!present) {
|
|
102
|
+
missingSections.push(sectionDef.heading);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (empty) {
|
|
106
|
+
emptySections.push(sectionDef.heading);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
sections,
|
|
112
|
+
missingSections,
|
|
113
|
+
emptySections
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
RUNTIME_SPEC_SECTION_DEFS,
|
|
119
|
+
parseRuntimeSpecMarkdown
|
|
120
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { parentPort, workerData } = require("worker_threads");
|
|
2
|
+
|
|
3
|
+
(async () => {
|
|
4
|
+
try {
|
|
5
|
+
const loaded = require(workerData.modulePath);
|
|
6
|
+
const fn = loaded[workerData.exportName];
|
|
7
|
+
if (typeof fn !== "function") {
|
|
8
|
+
throw new Error(`Export "${workerData.exportName}" is not a function.`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result = await fn(...workerData.args);
|
|
12
|
+
parentPort.postMessage({ ok: true, result });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
parentPort.postMessage({
|
|
15
|
+
ok: false,
|
|
16
|
+
error: {
|
|
17
|
+
message: error && error.message ? error.message : String(error),
|
|
18
|
+
stack: error && error.stack ? error.stack : "",
|
|
19
|
+
code: error && error.code ? error.code : "",
|
|
20
|
+
name: error && error.name ? error.name : "",
|
|
21
|
+
cause:
|
|
22
|
+
error && Object.prototype.hasOwnProperty.call(error, "cause") ? error.cause : undefined
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { Worker } = require("worker_threads");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const WORKER_SCRIPT_PATH = path.join(__dirname, "async-offload-worker.js");
|
|
5
|
+
|
|
6
|
+
function normalizeError(error) {
|
|
7
|
+
if (!error || typeof error !== "object") {
|
|
8
|
+
return new Error(String(error));
|
|
9
|
+
}
|
|
10
|
+
const message = error.message || String(error);
|
|
11
|
+
const wrapped = new Error(message);
|
|
12
|
+
if (typeof error.name === "string" && error.name) {
|
|
13
|
+
wrapped.name = error.name;
|
|
14
|
+
}
|
|
15
|
+
if (error.stack) {
|
|
16
|
+
wrapped.stack = error.stack;
|
|
17
|
+
}
|
|
18
|
+
if (error.code) {
|
|
19
|
+
wrapped.code = error.code;
|
|
20
|
+
}
|
|
21
|
+
if (Object.prototype.hasOwnProperty.call(error, "cause")) {
|
|
22
|
+
wrapped.cause = error.cause;
|
|
23
|
+
}
|
|
24
|
+
return wrapped;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runModuleExportInWorker(modulePath, exportName, args = []) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const worker = new Worker(WORKER_SCRIPT_PATH, {
|
|
30
|
+
workerData: {
|
|
31
|
+
modulePath: path.resolve(modulePath),
|
|
32
|
+
exportName,
|
|
33
|
+
args
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let settled = false;
|
|
38
|
+
function settle(callback, value) {
|
|
39
|
+
if (settled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
settled = true;
|
|
43
|
+
|
|
44
|
+
const termination = worker.terminate();
|
|
45
|
+
if (termination && typeof termination.then === "function") {
|
|
46
|
+
termination
|
|
47
|
+
.catch(() => {})
|
|
48
|
+
.finally(() => {
|
|
49
|
+
callback(value);
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
callback(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
worker.once("message", (message) => {
|
|
58
|
+
if (message && message.ok) {
|
|
59
|
+
settle(resolve, message.result);
|
|
60
|
+
} else {
|
|
61
|
+
settle(reject, normalizeError(message ? message.error : "Worker execution failed."));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
worker.once("error", (error) => {
|
|
65
|
+
settle(reject, normalizeError(error));
|
|
66
|
+
});
|
|
67
|
+
worker.once("exit", (code) => {
|
|
68
|
+
if (settled) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (code !== 0) {
|
|
72
|
+
settle(reject, new Error(`Worker exited with code ${code}.`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
settle(reject, new Error("Worker exited without returning a result."));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
runModuleExportInWorker
|
|
82
|
+
};
|
package/lib/audit-parsers.js
CHANGED
|
@@ -1,32 +1,70 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const { escapeRegExp } = require("./utils");
|
|
2
|
+
|
|
3
|
+
function getMarkdownLines(text) {
|
|
4
|
+
return String(text || "").replace(/\r\n?/g, "\n").split("\n");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getMarkdownHeadingLevel(line) {
|
|
8
|
+
const match = String(line || "").match(/^\s{0,3}(#{1,6})\s+/);
|
|
9
|
+
return match ? match[1].length : 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getMarkdownHeadingMatch(line, heading, options = {}) {
|
|
13
|
+
const escapedHeading = escapeRegExp(heading);
|
|
14
|
+
const allowParentheticalSuffix = options.allowParentheticalSuffix === true;
|
|
15
|
+
const suffixPattern = allowParentheticalSuffix
|
|
16
|
+
? String.raw`(?:\s*[((][^))\n]*[))])?`
|
|
17
|
+
: "";
|
|
18
|
+
const headingPattern = new RegExp(
|
|
19
|
+
String.raw`^\s{0,3}(#{1,6})\s+${escapedHeading}${suffixPattern}\s*$`,
|
|
20
|
+
"i"
|
|
21
|
+
);
|
|
22
|
+
const match = String(line || "").match(headingPattern);
|
|
23
|
+
if (!match) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
level: match[1].length
|
|
29
|
+
};
|
|
3
30
|
}
|
|
4
31
|
|
|
5
|
-
function
|
|
32
|
+
function hasMarkdownHeading(text, heading, options = {}) {
|
|
33
|
+
return getMarkdownLines(text).some((line) => Boolean(getMarkdownHeadingMatch(line, heading, options)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isMarkdownSectionHeading(line) {
|
|
37
|
+
return getMarkdownHeadingLevel(line) > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getMarkdownSection(text, heading, options = {}) {
|
|
6
41
|
if (!text) {
|
|
7
42
|
return "";
|
|
8
43
|
}
|
|
9
44
|
|
|
10
|
-
const
|
|
11
|
-
const headingPattern = new RegExp(`^##\\s+${escapedHeading}\\s*$`, "i");
|
|
12
|
-
const anyHeadingPattern = /^##\s+/;
|
|
13
|
-
const lines = String(text).replace(/\r\n?/g, "\n").split("\n");
|
|
45
|
+
const lines = getMarkdownLines(text);
|
|
14
46
|
const sectionLines = [];
|
|
15
47
|
let capturing = false;
|
|
48
|
+
let sectionLevel = 0;
|
|
16
49
|
|
|
17
50
|
for (const line of lines) {
|
|
18
|
-
|
|
51
|
+
const headingLevel = getMarkdownHeadingLevel(line);
|
|
52
|
+
|
|
53
|
+
if (capturing && headingLevel > 0 && headingLevel <= sectionLevel) {
|
|
19
54
|
break;
|
|
20
55
|
}
|
|
21
56
|
|
|
22
|
-
if (!capturing
|
|
57
|
+
if (!capturing) {
|
|
58
|
+
const headingMatch = getMarkdownHeadingMatch(line, heading, options);
|
|
59
|
+
if (!headingMatch) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
23
62
|
capturing = true;
|
|
63
|
+
sectionLevel = headingMatch.level;
|
|
24
64
|
continue;
|
|
25
65
|
}
|
|
26
66
|
|
|
27
|
-
|
|
28
|
-
sectionLines.push(line);
|
|
29
|
-
}
|
|
67
|
+
sectionLines.push(line);
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
return sectionLines.join("\n").trim();
|
|
@@ -37,17 +75,11 @@ function getMarkdownSectionsByHeading(text, heading, options = {}) {
|
|
|
37
75
|
return [];
|
|
38
76
|
}
|
|
39
77
|
|
|
40
|
-
const
|
|
41
|
-
const allowParentheticalSuffix = options.allowParentheticalSuffix === true;
|
|
42
|
-
const suffixPattern = allowParentheticalSuffix
|
|
43
|
-
? String.raw`(?:\s*[((][^))\n]*[))])?`
|
|
44
|
-
: "";
|
|
45
|
-
const headingPattern = new RegExp(`^##\\s+${escapedHeading}${suffixPattern}\\s*$`, "i");
|
|
46
|
-
const anyHeadingPattern = /^##\s+/;
|
|
47
|
-
const lines = String(text).replace(/\r\n?/g, "\n").split("\n");
|
|
78
|
+
const lines = getMarkdownLines(text);
|
|
48
79
|
const sections = [];
|
|
49
80
|
let capturing = false;
|
|
50
81
|
let currentHeading = "";
|
|
82
|
+
let currentLevel = 0;
|
|
51
83
|
let sectionLines = [];
|
|
52
84
|
|
|
53
85
|
function flushSection() {
|
|
@@ -61,30 +93,70 @@ function getMarkdownSectionsByHeading(text, heading, options = {}) {
|
|
|
61
93
|
});
|
|
62
94
|
capturing = false;
|
|
63
95
|
currentHeading = "";
|
|
96
|
+
currentLevel = 0;
|
|
64
97
|
sectionLines = [];
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
for (const line of lines) {
|
|
68
|
-
|
|
101
|
+
const headingLevel = getMarkdownHeadingLevel(line);
|
|
102
|
+
|
|
103
|
+
if (capturing && headingLevel > 0 && headingLevel <= currentLevel) {
|
|
69
104
|
flushSection();
|
|
70
105
|
}
|
|
71
106
|
|
|
72
|
-
if (!capturing
|
|
107
|
+
if (!capturing) {
|
|
108
|
+
const headingMatch = getMarkdownHeadingMatch(line, heading, options);
|
|
109
|
+
if (!headingMatch) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
73
112
|
capturing = true;
|
|
74
113
|
currentHeading = line.trim();
|
|
114
|
+
currentLevel = headingMatch.level;
|
|
75
115
|
sectionLines = [];
|
|
76
116
|
continue;
|
|
77
117
|
}
|
|
78
118
|
|
|
79
|
-
|
|
80
|
-
sectionLines.push(line);
|
|
81
|
-
}
|
|
119
|
+
sectionLines.push(line);
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
flushSection();
|
|
85
123
|
return sections;
|
|
86
124
|
}
|
|
87
125
|
|
|
126
|
+
const ACCEPTED_WARN_NEGATIVE_PATTERNS = [
|
|
127
|
+
/\bnot\s+accepted\b/i,
|
|
128
|
+
/\bnot\s+yet\s+accepted\b/i,
|
|
129
|
+
/\bunaccepted\b/i,
|
|
130
|
+
/\bstill\s+blocked\b/i,
|
|
131
|
+
/未接受/u,
|
|
132
|
+
/不接受/u,
|
|
133
|
+
/尚未接受/u,
|
|
134
|
+
/仍然?阻塞/u
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const ACCEPTED_WARN_POSITIVE_PATTERNS = [
|
|
138
|
+
/\baccepted(?:\s+with\s+follow-?up)?\b/i,
|
|
139
|
+
/\baccepted\s+warning\b/i,
|
|
140
|
+
/\bwarn(?:ing)?\s+accepted\b/i,
|
|
141
|
+
/已接受/u,
|
|
142
|
+
/接受警告/u,
|
|
143
|
+
/警告已接受/u,
|
|
144
|
+
/(?:^|[\s::,,((])接受(?:$|[\s,,.。!!;;))])/u
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
function isAcceptedWarnOutcome(revisionOutcome) {
|
|
148
|
+
const normalized = String(revisionOutcome || "").trim();
|
|
149
|
+
if (!normalized) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (ACCEPTED_WARN_NEGATIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return ACCEPTED_WARN_POSITIVE_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
158
|
+
}
|
|
159
|
+
|
|
88
160
|
function extractReviewFieldValue(section, fieldNames) {
|
|
89
161
|
const lines = String(section || "").replace(/\r\n?/g, "\n").split("\n");
|
|
90
162
|
const escapedFieldNames = fieldNames.map((fieldName) => escapeRegExp(fieldName)).join("|");
|
|
@@ -96,7 +168,7 @@ function extractReviewFieldValue(section, fieldNames) {
|
|
|
96
168
|
let capturing = false;
|
|
97
169
|
|
|
98
170
|
for (const rawLine of lines) {
|
|
99
|
-
if (
|
|
171
|
+
if (isMarkdownSectionHeading(rawLine)) {
|
|
100
172
|
if (capturing) {
|
|
101
173
|
break;
|
|
102
174
|
}
|
|
@@ -192,7 +264,8 @@ function parseCheckpointStatusMap(markdownText) {
|
|
|
192
264
|
function hasContextDeltaExpectationSignals(markdownText) {
|
|
193
265
|
const text = String(markdownText || "");
|
|
194
266
|
return (
|
|
195
|
-
|
|
267
|
+
hasMarkdownHeading(text, "Checkpoint Status") ||
|
|
268
|
+
hasMarkdownHeading(text, "MCP Runtime Gate") ||
|
|
196
269
|
/(?:^|\n)\s*(?:[-*]\s*)?`?Context Delta Required`?\s*:\s*(?:true|yes|on|1)\b/i.test(text)
|
|
197
270
|
);
|
|
198
271
|
}
|
|
@@ -204,6 +277,50 @@ function parseSupersedesTokens(value) {
|
|
|
204
277
|
.filter(Boolean);
|
|
205
278
|
}
|
|
206
279
|
|
|
280
|
+
function isValidIsoLikeDateCandidate(value) {
|
|
281
|
+
const match = String(value || "").match(
|
|
282
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d{1,3})?)?)?(?:Z|([+-])(\d{2}):?(\d{2}))?$/i
|
|
283
|
+
);
|
|
284
|
+
if (!match) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const year = Number(match[1]);
|
|
289
|
+
const month = Number(match[2]);
|
|
290
|
+
const day = Number(match[3]);
|
|
291
|
+
if (month < 1 || month > 12) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
296
|
+
if (day < 1 || day > daysInMonth) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (match[8] && !match[4]) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (match[4]) {
|
|
305
|
+
const hour = Number(match[4]);
|
|
306
|
+
const minute = Number(match[5]);
|
|
307
|
+
const second = match[6] ? Number(match[6]) : 0;
|
|
308
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (match[8]) {
|
|
314
|
+
const offsetHour = Number(match[9]);
|
|
315
|
+
const offsetMinute = Number(match[10]);
|
|
316
|
+
if (offsetHour < 0 || offsetHour > 23 || offsetMinute < 0 || offsetMinute > 59) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
207
324
|
function normalizeTimeToken(value) {
|
|
208
325
|
const raw = String(value || "").trim();
|
|
209
326
|
if (!raw) {
|
|
@@ -222,6 +339,9 @@ function normalizeTimeToken(value) {
|
|
|
222
339
|
parseCandidates.push(raw);
|
|
223
340
|
|
|
224
341
|
for (const candidate of parseCandidates) {
|
|
342
|
+
if (!isValidIsoLikeDateCandidate(candidate)) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
225
345
|
const timestamp = Date.parse(candidate);
|
|
226
346
|
if (!Number.isFinite(timestamp)) {
|
|
227
347
|
continue;
|
|
@@ -582,11 +702,7 @@ function inspectDesignSupervisorReview(pencilDesignText) {
|
|
|
582
702
|
const hasExecutedField = /(?:^|\n)\s*-\s*(?:Executed reviewers|Executed reviewer skills|执行评审)\s*:/i.test(section);
|
|
583
703
|
const hasReviewSourceField = /(?:^|\n)\s*-\s*(?:Review source|评审来源)\s*:/i.test(section);
|
|
584
704
|
const structuredFieldPresent = hasConfiguredField || hasExecutedField || hasReviewSourceField;
|
|
585
|
-
const acceptedWarn =
|
|
586
|
-
status === "WARN" &&
|
|
587
|
-
/(accepted|accepted with follow-up|accepted warning|warn accepted|接受|已接受|接受警告)/i.test(
|
|
588
|
-
revisionOutcome
|
|
589
|
-
);
|
|
705
|
+
const acceptedWarn = status === "WARN" && isAcceptedWarnOutcome(revisionOutcome);
|
|
590
706
|
return {
|
|
591
707
|
index,
|
|
592
708
|
heading: sectionRecord.heading,
|
|
@@ -636,6 +752,10 @@ function inspectDesignSupervisorReview(pencilDesignText) {
|
|
|
636
752
|
}
|
|
637
753
|
|
|
638
754
|
module.exports = {
|
|
755
|
+
hasMarkdownHeading,
|
|
756
|
+
getMarkdownSection,
|
|
757
|
+
isAcceptedWarnOutcome,
|
|
758
|
+
normalizeTimeToken,
|
|
639
759
|
normalizeCheckpointLabel,
|
|
640
760
|
parseCheckpointStatusMap,
|
|
641
761
|
hasContextDeltaExpectationSignals,
|