@xenonbyte/da-vinci-workflow 0.1.24 → 0.1.25
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 +12 -1
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/SKILL.md +1 -0
- package/commands/claude/dv/design.md +1 -1
- package/commands/codex/prompts/dv-design.md +1 -1
- package/commands/gemini/dv/design.toml +1 -1
- package/docs/constraint-files.md +1 -0
- package/docs/zh-CN/constraint-files.md +1 -0
- package/lib/audit-parsers.js +79 -27
- package/lib/audit.js +7 -0
- package/lib/pen-persistence.js +47 -0
- package/package.json +1 -1
- package/references/artifact-templates.md +1 -0
- package/scripts/test-audit-design-supervisor.js +155 -1
- package/scripts/test-mode-consistency.js +5 -0
- package/scripts/test-pen-persistence.js +149 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.1.25 - 2026-03-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `scripts/test-audit-design-supervisor.js` now covers Chinese legacy review headings (for example `## Design-Supervisor Review(第2轮尝试)`) and ensures structured fallback parsing remains deterministic
|
|
7
|
+
- `scripts/test-pen-persistence.js` now verifies metadata-payload error paths prefer concise relative paths when `--nodes-file` is under current working directory
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- `pen-persistence` now formats `--nodes-file` metadata-payload error locations using relative paths when possible (absolute path fallback when outside cwd)
|
|
11
|
+
- design-supervisor heading parsing now accepts both ASCII and full-width parentheses in heading suffixes
|
|
12
|
+
- legacy round-attempt heading detection now supports both English `Round ... Attempt` and Chinese `第N轮尝试` variants
|
|
13
|
+
|
|
3
14
|
## v0.1.24 - 2026-03-29
|
|
4
15
|
|
|
5
16
|
### Added
|
|
@@ -9,7 +20,7 @@
|
|
|
9
20
|
- `scripts/test-supervisor-review-integration.js` optional real `codex exec` smoke test (`DA_VINCI_RUN_SUPERVISOR_INTEGRATION=1`)
|
|
10
21
|
|
|
11
22
|
### Changed
|
|
12
|
-
- supervisor-review audit parsing now
|
|
23
|
+
- supervisor-review audit parsing now prefers the latest structured `## Design-Supervisor Review` block, warns when the newest entry is a legacy/non-structured round attempt, and still supports multiline issue/outcome fields plus `Result` as a status alias
|
|
13
24
|
- completion audit now enforces skill-backed supervisor evidence (`Review source: skill` + executed configured reviewers) when `Require Supervisor Review: true`
|
|
14
25
|
- design prompts/skill docs now require configured reviewer skills to actually execute review before writing supervisor results
|
|
15
26
|
- command references and constraint docs (EN/ZH) now document structured reviewer fields and required skill-backed behavior for hard-gate projects
|
package/README.md
CHANGED
|
@@ -28,12 +28,14 @@ This workflow is intended for:
|
|
|
28
28
|
|
|
29
29
|
Latest published npm package:
|
|
30
30
|
|
|
31
|
-
- `@xenonbyte/da-vinci-workflow@0.1.
|
|
31
|
+
- `@xenonbyte/da-vinci-workflow@0.1.25`
|
|
32
32
|
|
|
33
33
|
Release highlights:
|
|
34
34
|
|
|
35
35
|
- `da-vinci supervisor-review --run-reviewers` now supports configurable parallel reviewer execution and resilient retry backoff (`--review-concurrency`, `--review-retries`, `--review-retry-delay-ms`)
|
|
36
36
|
- added optional `test:supervisor-review-integration` smoke coverage for real `codex exec` review runner flows (enabled via `DA_VINCI_RUN_SUPERVISOR_INTEGRATION=1`)
|
|
37
|
+
- supervisor-review parsing now explicitly handles legacy Chinese round-attempt headings (for example `## Design-Supervisor Review(第2轮尝试)`) and keeps canonical structured review fallback deterministic
|
|
38
|
+
- `--nodes-file` metadata-payload guard now reports shorter relative file paths when possible, while preserving absolute-path fallback outside current working directory
|
|
37
39
|
- audit parser logic is now split into `lib/audit-parsers.js`, reducing `lib/audit.js` size and making maintenance safer
|
|
38
40
|
- recursive file traversal now uses bounded safe scans with depth/count limits and symlink skipping
|
|
39
41
|
- completion/integrity audit now rejects out-of-root `.pen` references from `design-registry.md`
|
package/README.zh-CN.md
CHANGED
|
@@ -30,12 +30,14 @@ Da Vinci 是一个把产品需求一路推进到结构化规格、Pencil 设计
|
|
|
30
30
|
|
|
31
31
|
最新已发布 npm 包:
|
|
32
32
|
|
|
33
|
-
- `@xenonbyte/da-vinci-workflow@0.1.
|
|
33
|
+
- `@xenonbyte/da-vinci-workflow@0.1.25`
|
|
34
34
|
|
|
35
35
|
已发布版本重点:
|
|
36
36
|
|
|
37
37
|
- `da-vinci supervisor-review --run-reviewers` 现已支持 reviewer 并发执行与失败重试退避(`--review-concurrency`、`--review-retries`、`--review-retry-delay-ms`)
|
|
38
38
|
- 新增可选 `test:supervisor-review-integration` 烟测,用于覆盖真实 `codex exec` 评审执行链路(通过 `DA_VINCI_RUN_SUPERVISOR_INTEGRATION=1` 启用)
|
|
39
|
+
- supervisor-review 解析现已显式覆盖中文 legacy 轮次标题(例如 `## Design-Supervisor Review(第2轮尝试)`),并保持“优先结构化评审块”的回退策略可预测
|
|
40
|
+
- `--nodes-file` 元数据误传保护现在优先输出更短的相对路径(在 cwd 外仍回退绝对路径),便于日志阅读
|
|
39
41
|
- audit 解析职责已拆分到 `lib/audit-parsers.js`,`lib/audit.js` 体量下降,可维护性更好
|
|
40
42
|
- 递归文件扫描改为安全有界遍历:增加深度/数量上限,并跳过符号链接
|
|
41
43
|
- completion/integrity audit 现在会拦截并忽略 `design-registry.md` 中越出项目根目录的 `.pen` 引用
|
package/SKILL.md
CHANGED
|
@@ -283,6 +283,7 @@ During active Pencil work:
|
|
|
283
283
|
- if `DA-VINCI.md` declares `Design-supervisor reviewers`, run an explicit review pass with those reviewer skills on the approved anchor set, then persist the structured result with `da-vinci supervisor-review --project <project-path> --change <change-id> --run-reviewers --write` (or the compatibility alias `design-supervisor review --run-reviewers --write`)
|
|
284
284
|
- keep `Design-supervisor reviewers` separate from `Preferred adapters`; adapters lead the design pass, reviewers judge whether the final style quality is strong enough to expand or implement
|
|
285
285
|
- when `design-supervisor review` is active, review screenshots together with Pencil theme variables, `visual-thesis.md`, `content-plan.md`, and `interaction-thesis.md`; record `Configured reviewers`, `Executed reviewers`, `Review source`, explicit `PASS`/`WARN`/`BLOCK`, issue list, and revision outcome in `pencil-design.md`
|
|
286
|
+
- do not hand-write ad-hoc headings such as `## Design-Supervisor Review (Round X Attempt)`; use `da-vinci supervisor-review --write` (or overwrite the canonical `## Design-Supervisor Review` block) so audit can consume structured evidence deterministically
|
|
286
287
|
- if `DA-VINCI.md` sets `Require Supervisor Review: true`, treat missing, blocked, or unaccepted `design-supervisor review` as a blocker before broad expansion, implementation-task handoff, or terminal completion
|
|
287
288
|
|
|
288
289
|
## Load References On Demand
|
|
@@ -26,5 +26,5 @@ If a registered project-local `.pen` already exists, reopen it for continuity bu
|
|
|
26
26
|
After the first successful Pencil write, run `da-vinci audit --mode integrity <project-path>` before broad expansion continues.
|
|
27
27
|
If Pencil MCP is active, run the MCP runtime gate after the first successful Pencil write and record it in `pencil-design.md`.
|
|
28
28
|
Screenshot review must record an explicit `PASS`, `WARN`, or `BLOCK` plus the issue list and revision outcome; "looks good" is not a valid review record.
|
|
29
|
-
If `DA-VINCI.md` declares `Design-supervisor reviewers`, execute those reviewer skills on the approved anchor screenshots (do not skip to inferred review), then persist the structured result via `da-vinci supervisor-review --project <project-path> --change <change-id> --run-reviewers --write` (or the compatibility alias `design-supervisor review --run-reviewers --write`). Record configured/ executed reviewers, review source, status, issue list, and revision outcome in `pencil-design.md`. If `Require Supervisor Review: true`, treat missing, blocked, unaccepted, or non-skill-backed review results as blocking before broad expansion or terminal completion.
|
|
29
|
+
If `DA-VINCI.md` declares `Design-supervisor reviewers`, execute those reviewer skills on the approved anchor screenshots (do not skip to inferred review), then persist the structured result via `da-vinci supervisor-review --project <project-path> --change <change-id> --run-reviewers --write` (or the compatibility alias `design-supervisor review --run-reviewers --write`). Record configured/ executed reviewers, review source, status, issue list, and revision outcome in `pencil-design.md`. Do not add ad-hoc headings like `## Design-Supervisor Review (Round X Attempt)`; keep a canonical structured section so audit can parse it deterministically. If `Require Supervisor Review: true`, treat missing, blocked, unaccepted, or non-skill-backed review results as blocking before broad expansion or terminal completion.
|
|
30
30
|
Before reporting `design complete` or `workflow complete`, run `da-vinci audit --mode completion --change <change-id> <project-path>` and treat any failure as blocking.
|
|
@@ -20,5 +20,5 @@ If a registered project-local `.pen` already exists, reopen it for continuity bu
|
|
|
20
20
|
After the first successful Pencil write, run `da-vinci audit --mode integrity <project-path>` before broad expansion continues.
|
|
21
21
|
If Pencil MCP is active, run the MCP runtime gate after the first successful Pencil write and record it in `pencil-design.md`.
|
|
22
22
|
Screenshot review must record an explicit `PASS`, `WARN`, or `BLOCK` plus the issue list and revision outcome; "looks good" is not a valid review record.
|
|
23
|
-
If `DA-VINCI.md` declares `Design-supervisor reviewers`, execute those reviewer skills on the approved anchor screenshots (do not skip to inferred review), then persist the structured result via `da-vinci supervisor-review --project <project-path> --change <change-id> --run-reviewers --write` (or the compatibility alias `design-supervisor review --run-reviewers --write`). Record configured/ executed reviewers, review source, status, issue list, and revision outcome in `pencil-design.md`. If `Require Supervisor Review: true`, treat missing, blocked, unaccepted, or non-skill-backed review results as blocking before broad expansion or terminal completion.
|
|
23
|
+
If `DA-VINCI.md` declares `Design-supervisor reviewers`, execute those reviewer skills on the approved anchor screenshots (do not skip to inferred review), then persist the structured result via `da-vinci supervisor-review --project <project-path> --change <change-id> --run-reviewers --write` (or the compatibility alias `design-supervisor review --run-reviewers --write`). Record configured/ executed reviewers, review source, status, issue list, and revision outcome in `pencil-design.md`. Do not add ad-hoc headings like `## Design-Supervisor Review (Round X Attempt)`; keep a canonical structured section so audit can parse it deterministically. If `Require Supervisor Review: true`, treat missing, blocked, unaccepted, or non-skill-backed review results as blocking before broad expansion or terminal completion.
|
|
24
24
|
Before claiming `design complete` or `workflow complete`, run `da-vinci audit --mode completion --change <change-id> <project-path>` and treat any failure as blocking.
|
|
@@ -19,6 +19,6 @@ If a registered project-local `.pen` already exists, reopen it for continuity bu
|
|
|
19
19
|
After the first successful Pencil write, run `da-vinci audit --mode integrity <project-path>` before broad expansion continues.
|
|
20
20
|
If Pencil MCP is active, run the MCP runtime gate after the first successful Pencil write and record it in `pencil-design.md`.
|
|
21
21
|
Screenshot review must record an explicit `PASS`, `WARN`, or `BLOCK` plus the issue list and revision outcome; "looks good" is not a valid review record.
|
|
22
|
-
If `DA-VINCI.md` declares `Design-supervisor reviewers`, execute those reviewer skills on the approved anchor screenshots (do not skip to inferred review), then persist the structured result via `da-vinci supervisor-review --project <project-path> --change <change-id> --run-reviewers --write` (or the compatibility alias `design-supervisor review --run-reviewers --write`). Record configured/ executed reviewers, review source, status, issue list, and revision outcome in `pencil-design.md`. If `Require Supervisor Review: true`, treat missing, blocked, unaccepted, or non-skill-backed review results as blocking before broad expansion or terminal completion.
|
|
22
|
+
If `DA-VINCI.md` declares `Design-supervisor reviewers`, execute those reviewer skills on the approved anchor screenshots (do not skip to inferred review), then persist the structured result via `da-vinci supervisor-review --project <project-path> --change <change-id> --run-reviewers --write` (or the compatibility alias `design-supervisor review --run-reviewers --write`). Record configured/ executed reviewers, review source, status, issue list, and revision outcome in `pencil-design.md`. Do not add ad-hoc headings like `## Design-Supervisor Review (Round X Attempt)`; keep a canonical structured section so audit can parse it deterministically. If `Require Supervisor Review: true`, treat missing, blocked, unaccepted, or non-skill-backed review results as blocking before broad expansion or terminal completion.
|
|
23
23
|
Before reporting `design complete` or `workflow complete`, run `da-vinci audit --mode completion --change <change-id> <project-path>` and treat any failure as blocking.
|
|
24
24
|
"""
|
package/docs/constraint-files.md
CHANGED
|
@@ -46,6 +46,7 @@ The workflow audit currently parses specific fields. Keep these names stable.
|
|
|
46
46
|
- `Revision outcome`
|
|
47
47
|
|
|
48
48
|
When supervisor review is required, missing, blocked, unaccepted, or non-skill-backed review evidence blocks completion.
|
|
49
|
+
Avoid ad-hoc headings such as `## Design-Supervisor Review (Round X Attempt)`; keep a canonical structured supervisor-review section (prefer `da-vinci supervisor-review --write`) so audit can parse the latest review deterministically.
|
|
49
50
|
|
|
50
51
|
## Advisory Constraint Sections (Customizable)
|
|
51
52
|
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
- `Revision outcome`
|
|
49
49
|
|
|
50
50
|
当 supervisor review 是必需时,缺失、`BLOCK`、未接受、或非 skill-backed 的评审证据都会阻断 completion。
|
|
51
|
+
避免写临时标题(例如 `## Design-Supervisor Review (Round X Attempt)`);应保持规范化、结构化的 supervisor-review 段(优先使用 `da-vinci supervisor-review --write`),这样 audit 才能稳定解析最新评审证据。
|
|
51
52
|
|
|
52
53
|
## 指导型约束段(可定制)
|
|
53
54
|
|
package/lib/audit-parsers.js
CHANGED
|
@@ -39,7 +39,9 @@ function getMarkdownSectionsByHeading(text, heading, options = {}) {
|
|
|
39
39
|
|
|
40
40
|
const escapedHeading = escapeRegExp(heading);
|
|
41
41
|
const allowParentheticalSuffix = options.allowParentheticalSuffix === true;
|
|
42
|
-
const suffixPattern = allowParentheticalSuffix
|
|
42
|
+
const suffixPattern = allowParentheticalSuffix
|
|
43
|
+
? String.raw`(?:\s*[((][^))\n]*[))])?`
|
|
44
|
+
: "";
|
|
43
45
|
const headingPattern = new RegExp(`^##\\s+${escapedHeading}${suffixPattern}\\s*$`, "i");
|
|
44
46
|
const anyHeadingPattern = /^##\s+/;
|
|
45
47
|
const lines = String(text).replace(/\r\n?/g, "\n").split("\n");
|
|
@@ -547,39 +549,89 @@ function inspectDesignSupervisorReview(pencilDesignText) {
|
|
|
547
549
|
status: null,
|
|
548
550
|
acceptedWarn: false,
|
|
549
551
|
hasIssueList: false,
|
|
550
|
-
hasRevisionOutcome: false
|
|
552
|
+
hasRevisionOutcome: false,
|
|
553
|
+
selectedFromLatest: true,
|
|
554
|
+
usedStructuredSectionFallback: false,
|
|
555
|
+
latestSectionLooksLegacyAttempt: false,
|
|
556
|
+
latestSectionStructuredFieldPresent: false,
|
|
557
|
+
selectedSectionHeading: "",
|
|
558
|
+
selectedSectionLooksLegacyAttempt: false
|
|
551
559
|
};
|
|
552
560
|
}
|
|
553
561
|
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const revisionOutcome = extractReviewFieldValue(section, ["Revision outcome", "修订结果"]);
|
|
561
|
-
const configuredReviewersRaw = extractReviewFieldValue(section, ["Configured reviewers", "配置评审"]);
|
|
562
|
-
const executedReviewersRaw = extractReviewFieldValue(section, ["Executed reviewers", "Executed reviewer skills", "执行评审"]);
|
|
563
|
-
const reviewSource = extractReviewFieldValue(section, ["Review source", "评审来源"]).toLowerCase();
|
|
564
|
-
const acceptedWarn =
|
|
565
|
-
status === "WARN" &&
|
|
566
|
-
/(accepted|accepted with follow-up|accepted warning|warn accepted|接受|已接受|接受警告)/i.test(
|
|
567
|
-
revisionOutcome
|
|
562
|
+
const legacyRoundAttemptPattern =
|
|
563
|
+
/[((]\s*(?:round[^)\n)]*attempt|第?\s*[0-9一二三四五六七八九十百零两]+\s*轮\s*尝试)\s*[))]/i;
|
|
564
|
+
const parsedSections = sections.map((sectionRecord, index) => {
|
|
565
|
+
const section = sectionRecord.content;
|
|
566
|
+
const statusMatch = section.match(
|
|
567
|
+
/(?:^|\n)\s*-\s*(?:Status|状态|Result|结果)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
|
|
568
568
|
);
|
|
569
|
-
|
|
570
|
-
|
|
569
|
+
const status = statusMatch ? statusMatch[1].toUpperCase() : null;
|
|
570
|
+
const issueList = extractReviewFieldValue(section, ["Issue list", "问题列表"]);
|
|
571
|
+
const revisionOutcome = extractReviewFieldValue(section, ["Revision outcome", "修订结果"]);
|
|
572
|
+
const configuredReviewersRaw = extractReviewFieldValue(section, ["Configured reviewers", "配置评审"]);
|
|
573
|
+
const executedReviewersRaw = extractReviewFieldValue(section, [
|
|
574
|
+
"Executed reviewers",
|
|
575
|
+
"Executed reviewer skills",
|
|
576
|
+
"执行评审"
|
|
577
|
+
]);
|
|
578
|
+
const reviewSource = extractReviewFieldValue(section, ["Review source", "评审来源"]).toLowerCase();
|
|
579
|
+
const configuredReviewers = parseListTokens(configuredReviewersRaw);
|
|
580
|
+
const executedReviewers = parseListTokens(executedReviewersRaw);
|
|
581
|
+
const hasConfiguredField = /(?:^|\n)\s*-\s*(?:Configured reviewers|配置评审)\s*:/i.test(section);
|
|
582
|
+
const hasExecutedField = /(?:^|\n)\s*-\s*(?:Executed reviewers|Executed reviewer skills|执行评审)\s*:/i.test(section);
|
|
583
|
+
const hasReviewSourceField = /(?:^|\n)\s*-\s*(?:Review source|评审来源)\s*:/i.test(section);
|
|
584
|
+
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
|
+
);
|
|
590
|
+
return {
|
|
591
|
+
index,
|
|
592
|
+
heading: sectionRecord.heading,
|
|
593
|
+
status,
|
|
594
|
+
issueList,
|
|
595
|
+
revisionOutcome,
|
|
596
|
+
configuredReviewers,
|
|
597
|
+
executedReviewers,
|
|
598
|
+
reviewSource,
|
|
599
|
+
acceptedWarn,
|
|
600
|
+
hasIssueList: Boolean(issueList),
|
|
601
|
+
hasRevisionOutcome: Boolean(revisionOutcome),
|
|
602
|
+
hasConfiguredReviewers: configuredReviewers.length > 0,
|
|
603
|
+
hasExecutedReviewers: executedReviewers.length > 0,
|
|
604
|
+
structuredFieldPresent,
|
|
605
|
+
looksLegacyRoundAttempt: legacyRoundAttemptPattern.test(sectionRecord.heading)
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const latestSection = parsedSections[parsedSections.length - 1];
|
|
610
|
+
const selectedSection =
|
|
611
|
+
[...parsedSections].reverse().find((section) => section.structuredFieldPresent) || latestSection;
|
|
612
|
+
const selectedFromLatest = selectedSection.index === latestSection.index;
|
|
613
|
+
const usedStructuredSectionFallback =
|
|
614
|
+
!selectedFromLatest &&
|
|
615
|
+
latestSection.structuredFieldPresent === false &&
|
|
616
|
+
selectedSection.structuredFieldPresent === true;
|
|
571
617
|
|
|
572
618
|
return {
|
|
573
619
|
found: true,
|
|
574
|
-
status,
|
|
575
|
-
acceptedWarn,
|
|
576
|
-
hasIssueList:
|
|
577
|
-
hasRevisionOutcome:
|
|
578
|
-
configuredReviewers,
|
|
579
|
-
executedReviewers,
|
|
580
|
-
reviewSource,
|
|
581
|
-
hasConfiguredReviewers:
|
|
582
|
-
hasExecutedReviewers:
|
|
620
|
+
status: selectedSection.status,
|
|
621
|
+
acceptedWarn: selectedSection.acceptedWarn,
|
|
622
|
+
hasIssueList: selectedSection.hasIssueList,
|
|
623
|
+
hasRevisionOutcome: selectedSection.hasRevisionOutcome,
|
|
624
|
+
configuredReviewers: selectedSection.configuredReviewers,
|
|
625
|
+
executedReviewers: selectedSection.executedReviewers,
|
|
626
|
+
reviewSource: selectedSection.reviewSource,
|
|
627
|
+
hasConfiguredReviewers: selectedSection.hasConfiguredReviewers,
|
|
628
|
+
hasExecutedReviewers: selectedSection.hasExecutedReviewers,
|
|
629
|
+
selectedFromLatest,
|
|
630
|
+
usedStructuredSectionFallback,
|
|
631
|
+
latestSectionLooksLegacyAttempt: latestSection.looksLegacyRoundAttempt,
|
|
632
|
+
latestSectionStructuredFieldPresent: latestSection.structuredFieldPresent,
|
|
633
|
+
selectedSectionHeading: selectedSection.heading,
|
|
634
|
+
selectedSectionLooksLegacyAttempt: selectedSection.looksLegacyRoundAttempt
|
|
583
635
|
};
|
|
584
636
|
}
|
|
585
637
|
|
package/lib/audit.js
CHANGED
|
@@ -474,6 +474,13 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
474
474
|
const enforceAsFailure =
|
|
475
475
|
mode === "completion" && scopedChangeDirs.includes(changeDir) && designSupervisorRequired;
|
|
476
476
|
|
|
477
|
+
if (review.usedStructuredSectionFallback) {
|
|
478
|
+
warnings.push(
|
|
479
|
+
`Latest design-supervisor review entry in ${relativeTo(projectRoot, pencilDesignPath)} ` +
|
|
480
|
+
`is a legacy/non-structured format; gate evaluation used ${review.selectedSectionHeading}.`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
477
484
|
let reviewRecordValid = true;
|
|
478
485
|
|
|
479
486
|
if (!review.found) {
|
package/lib/pen-persistence.js
CHANGED
|
@@ -110,7 +110,52 @@ function hasTruncatedChildren(value) {
|
|
|
110
110
|
return Object.values(value).some((item) => hasTruncatedChildren(item));
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function looksLikePenStatePayload(payload) {
|
|
114
|
+
if (!isPlainObject(payload)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const hasLiveNodes = Array.isArray(payload.nodes);
|
|
119
|
+
if (hasLiveNodes) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hasSnapshotHash = typeof payload.snapshotHash === "string" && payload.snapshotHash.length > 0;
|
|
124
|
+
const hasPenPath = typeof payload.penPath === "string" && payload.penPath.length > 0;
|
|
125
|
+
const hasStateCounters =
|
|
126
|
+
Object.prototype.hasOwnProperty.call(payload, "topLevelCount") ||
|
|
127
|
+
Object.prototype.hasOwnProperty.call(payload, "topLevelIds");
|
|
128
|
+
return hasSnapshotHash && hasPenPath && hasStateCounters;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertNodesPayloadIsLiveSnapshot(payload, sourcePath) {
|
|
132
|
+
if (!looksLikePenStatePayload(payload)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const location = sourcePath ? ` (${formatPathForError(sourcePath)})` : "";
|
|
137
|
+
throw new Error(
|
|
138
|
+
`\`--nodes-file\` payload${location} appears to be pen state metadata, not a live MCP nodes snapshot. ` +
|
|
139
|
+
"Use JSON from `batch_get` (an array or an object with `nodes`)."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatPathForError(sourcePath) {
|
|
144
|
+
const resolved = path.resolve(sourcePath);
|
|
145
|
+
const relative = path.relative(process.cwd(), resolved);
|
|
146
|
+
if (!relative || relative === "") {
|
|
147
|
+
return ".";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
151
|
+
return relative;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return resolved;
|
|
155
|
+
}
|
|
156
|
+
|
|
113
157
|
function normalizeNodesPayload(payload) {
|
|
158
|
+
assertNodesPayloadIsLiveSnapshot(payload);
|
|
114
159
|
const nodes = getNodesArray(payload);
|
|
115
160
|
|
|
116
161
|
if (!nodes) {
|
|
@@ -318,6 +363,7 @@ function verifyPenFileWithPencil(filePath, options = {}) {
|
|
|
318
363
|
function writePenFromPayloadFiles(options) {
|
|
319
364
|
const outputPath = path.resolve(options.outputPath);
|
|
320
365
|
const nodes = readJsonPayload(options.nodesFile);
|
|
366
|
+
assertNodesPayloadIsLiveSnapshot(nodes, options.nodesFile);
|
|
321
367
|
const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
|
|
322
368
|
const document = buildPenDocument({
|
|
323
369
|
version: options.version || DEFAULT_PEN_VERSION,
|
|
@@ -447,6 +493,7 @@ function ensurePenFile(options) {
|
|
|
447
493
|
|
|
448
494
|
function hashPayloadFiles(options) {
|
|
449
495
|
const nodes = readJsonPayload(options.nodesFile);
|
|
496
|
+
assertNodesPayloadIsLiveSnapshot(nodes, options.nodesFile);
|
|
450
497
|
const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
|
|
451
498
|
const document = buildPenDocument({
|
|
452
499
|
version: options.version || DEFAULT_PEN_VERSION,
|
package/package.json
CHANGED
|
@@ -569,6 +569,7 @@ Use this structure:
|
|
|
569
569
|
- Revision outcome
|
|
570
570
|
- Whether broad expansion is approved
|
|
571
571
|
- Whether implementation-task handoff is approved
|
|
572
|
+
- Keep one canonical structured supervisor-review section; avoid ad-hoc headings such as `## Design-Supervisor Review (Round X Attempt)`
|
|
572
573
|
|
|
573
574
|
## Anchor Surfaces
|
|
574
575
|
- Which 1-3 anchor screens were designed first
|
|
@@ -5,6 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const { spawnSync } = require("child_process");
|
|
6
6
|
|
|
7
7
|
const { auditProject } = require("../lib/audit");
|
|
8
|
+
const { inspectDesignSupervisorReview } = require("../lib/audit-parsers");
|
|
8
9
|
|
|
9
10
|
const repo = path.resolve(__dirname, "..");
|
|
10
11
|
const cli = path.join(repo, "bin", "da-vinci.js");
|
|
@@ -347,7 +348,7 @@ runTest("completion audit passes when required supervisor review is recorded as
|
|
|
347
348
|
);
|
|
348
349
|
});
|
|
349
350
|
|
|
350
|
-
runTest("completion audit accepts the latest round section when multiple Design-Supervisor Review headings exist", () => {
|
|
351
|
+
runTest("completion audit accepts the latest structured round section when multiple Design-Supervisor Review headings exist", () => {
|
|
351
352
|
const harness = createHarness();
|
|
352
353
|
const project = setupProject(harness, "latest-supervisor-round-wins", {
|
|
353
354
|
requireSupervisorReview: true
|
|
@@ -391,6 +392,159 @@ runTest("completion audit accepts the latest round section when multiple Design-
|
|
|
391
392
|
result.notes.join("\n"),
|
|
392
393
|
/Detected design-supervisor review status PASS/i
|
|
393
394
|
);
|
|
395
|
+
assert.doesNotMatch(
|
|
396
|
+
result.warnings.join("\n"),
|
|
397
|
+
/legacy\/non-structured format/i
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
runTest("completion audit falls back to the latest structured section when newest review entry is legacy-style", () => {
|
|
402
|
+
const harness = createHarness();
|
|
403
|
+
const project = setupProject(harness, "legacy-supervisor-entry-fallback", {
|
|
404
|
+
requireSupervisorReview: true
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
writeText(
|
|
408
|
+
project.pencilDesignPath,
|
|
409
|
+
[
|
|
410
|
+
"# Pencil Design",
|
|
411
|
+
"",
|
|
412
|
+
"## Design-Supervisor Review",
|
|
413
|
+
"- Configured reviewers: frontend-skill",
|
|
414
|
+
"- Executed reviewers: frontend-skill",
|
|
415
|
+
"- Review source: skill",
|
|
416
|
+
"- Status: PASS",
|
|
417
|
+
"- Issue list: none",
|
|
418
|
+
"- Revision outcome: approved",
|
|
419
|
+
"",
|
|
420
|
+
"## Design-Supervisor Review (Round 2 Attempt)",
|
|
421
|
+
"- Result: BLOCK",
|
|
422
|
+
"- Issue list: style quality not approved",
|
|
423
|
+
"- Revision outcome: pending",
|
|
424
|
+
""
|
|
425
|
+
].join("\n")
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const result = auditProject(project.root, {
|
|
429
|
+
mode: "completion",
|
|
430
|
+
changeId
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
assert.equal(
|
|
434
|
+
result.status,
|
|
435
|
+
"WARN",
|
|
436
|
+
`expected completion audit to warn but not fail when latest entry is legacy:\n${JSON.stringify(result, null, 2)}`
|
|
437
|
+
);
|
|
438
|
+
assert.match(
|
|
439
|
+
result.notes.join("\n"),
|
|
440
|
+
/Detected design-supervisor review status PASS/i
|
|
441
|
+
);
|
|
442
|
+
assert.match(
|
|
443
|
+
result.warnings.join("\n"),
|
|
444
|
+
/legacy\/non-structured format; gate evaluation used ## Design-Supervisor Review/i
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
runTest("completion audit fails when only legacy-style Design-Supervisor Review sections exist", () => {
|
|
449
|
+
const harness = createHarness();
|
|
450
|
+
const project = setupProject(harness, "legacy-supervisor-only", {
|
|
451
|
+
requireSupervisorReview: true
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
writeText(
|
|
455
|
+
project.pencilDesignPath,
|
|
456
|
+
[
|
|
457
|
+
"# Pencil Design",
|
|
458
|
+
"",
|
|
459
|
+
"## Design-Supervisor Review (Round 3 Attempt)",
|
|
460
|
+
"- Result: PASS",
|
|
461
|
+
"- Issue list: none",
|
|
462
|
+
"- Revision outcome: accepted",
|
|
463
|
+
""
|
|
464
|
+
].join("\n")
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const result = auditProject(project.root, {
|
|
468
|
+
mode: "completion",
|
|
469
|
+
changeId
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
assert.equal(result.status, "FAIL");
|
|
473
|
+
assert.match(
|
|
474
|
+
result.failures.join("\n"),
|
|
475
|
+
/missing required field\(s\): Configured reviewers/i
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
runTest("completion audit treats Chinese round-attempt heading as legacy and falls back to latest structured section", () => {
|
|
480
|
+
const harness = createHarness();
|
|
481
|
+
const project = setupProject(harness, "legacy-supervisor-chinese-heading", {
|
|
482
|
+
requireSupervisorReview: true
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
writeText(
|
|
486
|
+
project.pencilDesignPath,
|
|
487
|
+
[
|
|
488
|
+
"# Pencil Design",
|
|
489
|
+
"",
|
|
490
|
+
"## Design-Supervisor Review",
|
|
491
|
+
"- Configured reviewers: frontend-skill",
|
|
492
|
+
"- Executed reviewers: frontend-skill",
|
|
493
|
+
"- Review source: skill",
|
|
494
|
+
"- Status: PASS",
|
|
495
|
+
"- Issue list: none",
|
|
496
|
+
"- Revision outcome: approved",
|
|
497
|
+
"",
|
|
498
|
+
"## Design-Supervisor Review(第2轮尝试)",
|
|
499
|
+
"- Result: BLOCK",
|
|
500
|
+
"- Issue list: 视觉层级仍需调整",
|
|
501
|
+
"- Revision outcome: 待修订",
|
|
502
|
+
""
|
|
503
|
+
].join("\n")
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const result = auditProject(project.root, {
|
|
507
|
+
mode: "completion",
|
|
508
|
+
changeId
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
assert.equal(result.status, "WARN");
|
|
512
|
+
assert.match(
|
|
513
|
+
result.warnings.join("\n"),
|
|
514
|
+
/legacy\/non-structured format; gate evaluation used ## Design-Supervisor Review/i
|
|
515
|
+
);
|
|
516
|
+
assert.match(
|
|
517
|
+
result.notes.join("\n"),
|
|
518
|
+
/Detected design-supervisor review status PASS/i
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
runTest("inspectDesignSupervisorReview marks Chinese round-attempt suffix as legacy", () => {
|
|
523
|
+
const review = inspectDesignSupervisorReview(
|
|
524
|
+
[
|
|
525
|
+
"# Pencil Design",
|
|
526
|
+
"",
|
|
527
|
+
"## Design-Supervisor Review",
|
|
528
|
+
"- Configured reviewers: frontend-skill",
|
|
529
|
+
"- Executed reviewers: frontend-skill",
|
|
530
|
+
"- Review source: skill",
|
|
531
|
+
"- Status: PASS",
|
|
532
|
+
"- Issue list: none",
|
|
533
|
+
"- Revision outcome: approved",
|
|
534
|
+
"",
|
|
535
|
+
"## Design-Supervisor Review(第3轮尝试)",
|
|
536
|
+
"- Result: BLOCK",
|
|
537
|
+
"- Issue list: 继续迭代",
|
|
538
|
+
"- Revision outcome: pending",
|
|
539
|
+
""
|
|
540
|
+
].join("\n")
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
assert.equal(review.found, true);
|
|
544
|
+
assert.equal(review.latestSectionLooksLegacyAttempt, true);
|
|
545
|
+
assert.equal(review.usedStructuredSectionFallback, true);
|
|
546
|
+
assert.equal(review.selectedFromLatest, false);
|
|
547
|
+
assert.equal(review.status, "PASS");
|
|
394
548
|
});
|
|
395
549
|
|
|
396
550
|
runTest("completion audit accepts multiline Issue list and Revision outcome fields", () => {
|
|
@@ -283,6 +283,11 @@ runTest("design-supervisor review stays distinct from preferred adapters and is
|
|
|
283
283
|
const content = read(file);
|
|
284
284
|
assert.match(content, /design-supervisor review/, `${file} should instruct design flows to run design-supervisor review when configured`);
|
|
285
285
|
assert.match(content, /Require Supervisor Review: true/, `${file} should explain the hard-gate branch for supervisor review`);
|
|
286
|
+
assert.doesNotMatch(
|
|
287
|
+
content,
|
|
288
|
+
/^\s*##\s*Design-Supervisor Review\s*\(Round [^)]+Attempt\)\s*$/im,
|
|
289
|
+
`${file} should not provide ad-hoc supervisor-review heading templates`
|
|
290
|
+
);
|
|
286
291
|
}
|
|
287
292
|
});
|
|
288
293
|
|
|
@@ -33,6 +33,10 @@ function createTempDir() {
|
|
|
33
33
|
return fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-pen-persistence-"));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function escapeRegExp(value) {
|
|
37
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
function writePayloadFiles(tempDir, fixture) {
|
|
37
41
|
const nodesFile = path.join(tempDir, "nodes.json");
|
|
38
42
|
const variablesFile = path.join(tempDir, "variables.json");
|
|
@@ -183,6 +187,151 @@ runTest("comparePenSync fails when live payload is newer than disk", () => {
|
|
|
183
187
|
assert.notEqual(result.liveHash, result.persistedHash);
|
|
184
188
|
});
|
|
185
189
|
|
|
190
|
+
runTest("writePenFromPayloadFiles rejects pen-state metadata passed as nodes payload", () => {
|
|
191
|
+
const tempDir = createTempDir();
|
|
192
|
+
const outputPath = path.join(tempDir, "written.pen");
|
|
193
|
+
const nodesFile = path.join(tempDir, "state-like.json");
|
|
194
|
+
|
|
195
|
+
fs.writeFileSync(
|
|
196
|
+
nodesFile,
|
|
197
|
+
JSON.stringify(
|
|
198
|
+
{
|
|
199
|
+
schema: 1,
|
|
200
|
+
penPath: "/tmp/demo.pen",
|
|
201
|
+
snapshotHash: "abc123",
|
|
202
|
+
topLevelIds: [],
|
|
203
|
+
topLevelCount: 0
|
|
204
|
+
},
|
|
205
|
+
null,
|
|
206
|
+
2
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
assert.throws(
|
|
211
|
+
() =>
|
|
212
|
+
writePenFromPayloadFiles({
|
|
213
|
+
outputPath,
|
|
214
|
+
nodesFile
|
|
215
|
+
}),
|
|
216
|
+
/appears to be pen state metadata/i
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
runTest("comparePenSync rejects pen-state metadata passed as nodes payload", () => {
|
|
221
|
+
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
|
|
222
|
+
const tempDir = createTempDir();
|
|
223
|
+
const { nodesFile, variablesFile } = writePayloadFiles(tempDir, fixture);
|
|
224
|
+
const outputPath = path.join(tempDir, "written.pen");
|
|
225
|
+
const invalidNodesFile = path.join(tempDir, "state-like.json");
|
|
226
|
+
|
|
227
|
+
writePenFromPayloadFiles({
|
|
228
|
+
outputPath,
|
|
229
|
+
nodesFile,
|
|
230
|
+
variablesFile,
|
|
231
|
+
version: fixture.version
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
fs.writeFileSync(
|
|
235
|
+
invalidNodesFile,
|
|
236
|
+
JSON.stringify(
|
|
237
|
+
{
|
|
238
|
+
schema: 1,
|
|
239
|
+
penPath: outputPath,
|
|
240
|
+
snapshotHash: "abc123",
|
|
241
|
+
topLevelIds: fixture.children.map((node) => node.id),
|
|
242
|
+
topLevelCount: fixture.children.length
|
|
243
|
+
},
|
|
244
|
+
null,
|
|
245
|
+
2
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
assert.throws(
|
|
250
|
+
() =>
|
|
251
|
+
comparePenSync({
|
|
252
|
+
penPath: outputPath,
|
|
253
|
+
nodesFile: invalidNodesFile,
|
|
254
|
+
version: fixture.version
|
|
255
|
+
}),
|
|
256
|
+
/appears to be pen state metadata/i
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
runTest("writePenFromPayloadFiles accepts nodes payload even when metadata keys are present", () => {
|
|
261
|
+
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
|
|
262
|
+
const tempDir = createTempDir();
|
|
263
|
+
const nodesFile = path.join(tempDir, "nodes-with-metadata.json");
|
|
264
|
+
const variablesFile = path.join(tempDir, "variables.json");
|
|
265
|
+
const outputPath = path.join(tempDir, "written.pen");
|
|
266
|
+
|
|
267
|
+
fs.writeFileSync(
|
|
268
|
+
nodesFile,
|
|
269
|
+
JSON.stringify(
|
|
270
|
+
{
|
|
271
|
+
nodes: fixture.children,
|
|
272
|
+
penPath: "/tmp/demo.pen",
|
|
273
|
+
snapshotHash: "abc123",
|
|
274
|
+
topLevelIds: fixture.children.map((node) => node.id),
|
|
275
|
+
topLevelCount: fixture.children.length
|
|
276
|
+
},
|
|
277
|
+
null,
|
|
278
|
+
2
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
fs.writeFileSync(variablesFile, JSON.stringify({ variables: fixture.variables }, null, 2));
|
|
282
|
+
|
|
283
|
+
writePenFromPayloadFiles({
|
|
284
|
+
outputPath,
|
|
285
|
+
nodesFile,
|
|
286
|
+
variablesFile,
|
|
287
|
+
version: fixture.version
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, "utf8")), fixture);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
runTest("writePenFromPayloadFiles shows relative nodes path in metadata-payload error when file is under cwd", () => {
|
|
294
|
+
const originalCwd = process.cwd();
|
|
295
|
+
const localRoot = fs.mkdtempSync(path.join(originalCwd, ".tmp-da-vinci-pen-path-"));
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
process.chdir(localRoot);
|
|
299
|
+
const outputPath = path.join(localRoot, "written.pen");
|
|
300
|
+
const nodesFile = path.join(localRoot, "state-like.json");
|
|
301
|
+
|
|
302
|
+
fs.writeFileSync(
|
|
303
|
+
nodesFile,
|
|
304
|
+
JSON.stringify(
|
|
305
|
+
{
|
|
306
|
+
schema: 1,
|
|
307
|
+
penPath: "/tmp/demo.pen",
|
|
308
|
+
snapshotHash: "abc123",
|
|
309
|
+
topLevelIds: [],
|
|
310
|
+
topLevelCount: 0
|
|
311
|
+
},
|
|
312
|
+
null,
|
|
313
|
+
2
|
|
314
|
+
)
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
assert.throws(
|
|
318
|
+
() =>
|
|
319
|
+
writePenFromPayloadFiles({
|
|
320
|
+
outputPath,
|
|
321
|
+
nodesFile
|
|
322
|
+
}),
|
|
323
|
+
(error) => {
|
|
324
|
+
assert.match(error.message, /\(state-like\.json\)/i);
|
|
325
|
+
assert.doesNotMatch(error.message, new RegExp(escapeRegExp(localRoot)));
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
} finally {
|
|
330
|
+
process.chdir(originalCwd);
|
|
331
|
+
fs.rmSync(localRoot, { recursive: true, force: true });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
186
335
|
runTest("global Pencil lock serializes projects", () => {
|
|
187
336
|
const tempDir = createTempDir();
|
|
188
337
|
const homeDir = path.join(tempDir, "home");
|