@xulthekl/team-flow 0.54.0 → 0.56.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/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +92 -1
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/agents/prototype-env-scout.md +4 -4
- package/dist/parsing/requirement-blocks.d.ts +26 -0
- package/dist/parsing/requirement-blocks.js +33 -5
- package/dist/validation/validator.js +8 -1
- package/docs/README_en.md +1 -1
- package/gemini-extension.json +1 -1
- package/hooks/session-start +19 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/check-project-config.mjs +84 -0
- package/scripts/design-system-clone.mjs +150 -0
- package/scripts/design-system-import.mjs +102 -14
- package/scripts/gen-primer.mjs +65 -13
- package/scripts/guard/checks/tasks-complete.mjs +9 -4
- package/scripts/guard/design-token-guard.mjs +136 -117
- package/scripts/infer-workflow.mjs +10 -1
- package/scripts/lib/arch-merge.mjs +20 -6
- package/scripts/lib/arch-parse.mjs +5 -11
- package/scripts/lib/ds-inputs.mjs +125 -0
- package/scripts/lib/ds-parse.mjs +124 -12
- package/scripts/lib/execution-recommendation.mjs +10 -1
- package/scripts/lib/glaf4-delegation.mjs +14 -3
- package/scripts/lib/hash.mjs +18 -2
- package/scripts/lib/md-normalize.mjs +108 -0
- package/scripts/lib/prototype-sync.mjs +19 -1
- package/scripts/lib/sdd-overlay.mjs +17 -6
- package/scripts/lib/slug.mjs +68 -0
- package/scripts/lib/solutions-capture.mjs +6 -12
- package/scripts/lib/solutions-index-gen.mjs +2 -2
- package/scripts/lib/solutions-phases.mjs +32 -0
- package/scripts/lib/solutions-promote.mjs +16 -5
- package/scripts/lib/spec-merge.mjs +46 -11
- package/scripts/lib/state-loader.mjs +4 -1
- package/scripts/lib/test-merge.mjs +4 -1
- package/scripts/token-extract.mjs +101 -9
- package/skills/ce-compound/references/three-tier-index.md +6 -2
- package/skills/design-system/SKILL.md +46 -23
- package/skills/design-system/references/agents/design-system-architect.md +25 -12
- package/skills/design-system/references/creation-flow.md +17 -0
- package/skills/design-system/references/creation-modes.md +171 -0
- package/skills/design-system/references/showcase-board-b-end.md +50 -36
- package/skills/design-system/references/showcase-board-c-end.md +59 -36
- package/skills/design-system/references/variant-schema.md +21 -1
- package/skills/prototype/SKILL.md +4 -0
- package/skills/prototype/references/builder-methodology.md +11 -4
- package/skills/prototype/references/layouts.md +10 -0
- package/skills/prototype/references/orchestration-flow.md +8 -0
- package/skills/workflow-bootstrap/SKILL.md +15 -5
- package/src/parsing/requirement-blocks.ts +34 -5
- package/src/validation/validator.ts +8 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// design-token-guard.mjs — 设计系统完整性检查(v0.
|
|
2
|
+
// design-token-guard.mjs — 设计系统完整性检查(v0.55.0)
|
|
3
3
|
//
|
|
4
4
|
// 两部分(v0.54.0 新增六层审计;设计 §4.1.5):
|
|
5
5
|
// A. 硬校验(失败 exit 1):9 段 schema / palette 5 方向 / A1 identity / A2 color-mix / B-slot 别名
|
|
@@ -33,24 +33,28 @@ import {
|
|
|
33
33
|
hasSection,
|
|
34
34
|
sectionBody,
|
|
35
35
|
sectionBodyRaw,
|
|
36
|
+
subsectionBodyRaw,
|
|
36
37
|
parseComponentsTable,
|
|
37
38
|
parseContract,
|
|
39
|
+
parsePageArchetype,
|
|
40
|
+
contractFormatWarning,
|
|
38
41
|
COMPONENT_TYPE_MIN_STATES,
|
|
39
42
|
COMPONENT_COUNT_PASS,
|
|
40
43
|
COMPONENT_COUNT_WARN,
|
|
41
44
|
} from '../lib/ds-parse.mjs';
|
|
45
|
+
import {
|
|
46
|
+
REQUIRED_SECTIONS,
|
|
47
|
+
OPTIONAL_SECTIONS,
|
|
48
|
+
NON_SECTION_FILES,
|
|
49
|
+
KNOWN_VARIANT_RE,
|
|
50
|
+
readFileOrNull,
|
|
51
|
+
designSystemMds,
|
|
52
|
+
variantCandidates,
|
|
53
|
+
resolveInputs,
|
|
54
|
+
} from '../lib/ds-inputs.mjs';
|
|
42
55
|
|
|
43
56
|
// ── Check definitions ──
|
|
44
57
|
|
|
45
|
-
// The 9 required ## sections in design-system.md.
|
|
46
|
-
const REQUIRED_SECTIONS = [
|
|
47
|
-
'color', 'typography', 'spacing', 'layout', 'components',
|
|
48
|
-
'motion', 'voice', 'brand', 'anti-patterns',
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
// Optional ## sections — reported for visibility, never counted as failures.
|
|
52
|
-
const OPTIONAL_SECTIONS = ['palette', 'aliases', 'extensions', 'principles', 'governance'];
|
|
53
|
-
|
|
54
58
|
// The 5 palette directions checked when a palette section exists.
|
|
55
59
|
const PALETTE_DIRECTIONS = ['neutral', 'primary', 'success', 'warning', 'danger'];
|
|
56
60
|
|
|
@@ -73,12 +77,6 @@ const B_SLOT_ALIASES = [
|
|
|
73
77
|
|
|
74
78
|
// ── Helpers ──
|
|
75
79
|
|
|
76
|
-
// Read a file as UTF-8, or return null when it does not exist.
|
|
77
|
-
function readFileOrNull(filePath) {
|
|
78
|
-
if (!filePath || !existsSync(filePath)) return null;
|
|
79
|
-
return readFileSync(filePath, 'utf-8');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
80
|
// Escape a token name for safe use inside a RegExp (names contain "--").
|
|
83
81
|
function escapeRegExp(value) {
|
|
84
82
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -122,84 +120,13 @@ if (!mdPath) {
|
|
|
122
120
|
process.exit(1);
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
// ──
|
|
123
|
+
// ── 输入解析:已下沉至 scripts/lib/ds-inputs.mjs(v0.55.0)──
|
|
126
124
|
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// ① 传入目录 → 以目录内 base.md 为主体;② 传入 base.md → 以它为主体;③ 传入变体文件 → base.md 主体 + 该文件。
|
|
133
|
-
// ④ 主体自身已含全部 9 段(单文件系统 / 转换器产物)→ 单文件模式,不合并(避免卷入同目录无关 md)。
|
|
134
|
-
// ⑤ 主体不含全 9 段 → 合并**约定命名的端变体**(`*-end.md` + `variants/` 子目录内的 md);
|
|
135
|
-
// `--variant <name>` 可显式纳入非约定命名的变体文件(逃生通道)。
|
|
136
|
-
// 含必选段但未被纳入的其它 md → 告警列出(不静默补齐:一个 notes.md 不该把残缺系统伪装成 PASS)。
|
|
137
|
-
// 合并按「base 在前」顺序,组件表按组件名去重(base 优先),端特有差异行不会重复计数。
|
|
138
|
-
const NON_SECTION_FILES = new Set(['primer.md', 'pending.md', 'readme.md', 'index.md']);
|
|
139
|
-
|
|
140
|
-
// 端变体命名约定:`*-end.md`(b-end / c-end / mobile-end …)+ `variants/` 子目录内全部 md(如 dark.md)。
|
|
141
|
-
// 收紧为约定集而非「同目录任意 md」——否则一个含 `## spacing` 的 notes.md 就能补齐缺失段,
|
|
142
|
-
// 把残缺系统伪装成 PASS(v0.54.0 P4 复验观察 B 实证)。非约定名可用 `--variant <name>` 显式纳入。
|
|
143
|
-
const KNOWN_VARIANT_RE = /-end\.md$/i;
|
|
144
|
-
|
|
145
|
-
/** 设计系统目录内的全部候选 md(dir 与 variants/ 两层,排除 base 与非段落文件)。 */
|
|
146
|
-
function designSystemMds(dir) {
|
|
147
|
-
const out = [];
|
|
148
|
-
for (const d of [dir, join(dir, 'variants')]) {
|
|
149
|
-
if (!existsSync(d)) continue;
|
|
150
|
-
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
151
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
152
|
-
if (entry.name === 'base.md' || NON_SECTION_FILES.has(entry.name.toLowerCase())) continue;
|
|
153
|
-
out.push(join(d, entry.name));
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return out.sort();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function variantCandidates(dir) {
|
|
160
|
-
return designSystemMds(dir).filter(f => KNOWN_VARIANT_RE.test(basename(f)) || f.includes(`${sep}variants${sep}`));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function resolveInputs(input, variantName) {
|
|
164
|
-
const isDir = existsSync(input) && statSync(input).isDirectory();
|
|
165
|
-
const dir = isDir ? input : dirname(input);
|
|
166
|
-
const parts = [];
|
|
167
|
-
|
|
168
|
-
if (isDir) {
|
|
169
|
-
parts.push(join(dir, 'base.md'));
|
|
170
|
-
} else if (basename(input) === 'base.md') {
|
|
171
|
-
parts.push(input);
|
|
172
|
-
} else {
|
|
173
|
-
parts.push(join(dir, 'base.md')); // 传入变体文件时仍以 base 为主体(base 优先)
|
|
174
|
-
parts.push(input);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const present = parts.filter(f => existsSync(f) && readFileOrNull(f) !== null);
|
|
178
|
-
const subject = present[0] ? readFileOrNull(present[0]) : null;
|
|
179
|
-
const subjectComplete = subject !== null
|
|
180
|
-
&& REQUIRED_SECTIONS.every(s => hasSection(extractH2Headers(subject), s));
|
|
181
|
-
|
|
182
|
-
let merged = [...present];
|
|
183
|
-
let ignored = [];
|
|
184
|
-
if (!subjectComplete) {
|
|
185
|
-
// --variant <name> 为显式指定:在**全部**候选 md 中按名匹配(含非约定命名),作为逃生通道
|
|
186
|
-
const variants = variantName
|
|
187
|
-
? designSystemMds(dir).filter(f => basename(f).includes(variantName))
|
|
188
|
-
: variantCandidates(dir);
|
|
189
|
-
for (const v of variants) if (!merged.includes(v)) merged.push(v);
|
|
190
|
-
// 观察 B 兜底:未被纳入合并、但含必选段的非约定 md —— 显式告警(不静默补齐,也不静默忽略)
|
|
191
|
-
if (!variantName) {
|
|
192
|
-
const mergedSet = new Set(merged);
|
|
193
|
-
ignored = designSystemMds(dir).filter(f => {
|
|
194
|
-
if (mergedSet.has(f)) return false;
|
|
195
|
-
const text = readFileOrNull(f) || '';
|
|
196
|
-
const headers = extractH2Headers(text);
|
|
197
|
-
return REQUIRED_SECTIONS.some(s => hasSection(headers, s));
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return { dir, files: merged, singleFile: subjectComplete, ignored };
|
|
202
|
-
}
|
|
125
|
+
// 动机:gen-primer.mjs 的 digest 必须覆盖参与合并的**全部**文件(否则变体变更不判 STALE
|
|
126
|
+
// → prototype Step 0 gate 放行 → 原型静默用旧页面范式),它需要与 guard 同一套变体发现逻辑。
|
|
127
|
+
// 但 guard 是**顶层副作用模块**(顶层 argv 解析 / process.exit / 打印整份报告),
|
|
128
|
+
// 不能被子脚本 import(import 即先打印报告再退出)→ 纯逻辑下沉,两处 import。
|
|
129
|
+
// 输入规则 ①–⑤、单文件模式与 ignored 告警语义,见 ds-inputs.mjs 头注。
|
|
203
130
|
|
|
204
131
|
const { dir: dsDir, files: mdFiles, singleFile, ignored: ignoredMds } = resolveInputs(mdPath, VARIANT);
|
|
205
132
|
const markdown = mdFiles.length
|
|
@@ -207,14 +134,20 @@ const markdown = mdFiles.length
|
|
|
207
134
|
: null;
|
|
208
135
|
const css = cssPath ? readFileOrNull(cssPath) : null;
|
|
209
136
|
|
|
210
|
-
line('Design Token Guard v0.
|
|
137
|
+
line('Design Token Guard v0.55.0');
|
|
211
138
|
line('==========================');
|
|
212
139
|
line(`design-system.md: ${mdPath}`);
|
|
213
140
|
line(`design-tokens.css: ${cssPath || 'not provided'}`);
|
|
214
141
|
if (mdFiles.length > 1) line(`合并输入(base + 端变体):${mdFiles.map(f => basename(f)).join(' + ')}`);
|
|
215
142
|
if (ignoredMds.length) {
|
|
216
|
-
|
|
217
|
-
|
|
143
|
+
if (singleFile) {
|
|
144
|
+
// v0.55.0:单文件模式原先整块跳过 → 同目录变体静默不参与判据(P1.5 一轮 C1)
|
|
145
|
+
line(` ⚠️ 主体已含全 9 段(单文件模式),同目录变体**未参与判据**:${ignoredMds.map(f => basename(f)).join(' / ')}`);
|
|
146
|
+
line(' 若该变体属于本系统 → 改走拆分布局(base.md 只留品牌层 + 端变体),或用 `--variant <name>` 显式纳入');
|
|
147
|
+
} else {
|
|
148
|
+
line(` ⚠️ 未纳入合并(非约定命名的端变体命名:\`*-end.md\` 或 \`variants/\`):${ignoredMds.map(f => basename(f)).join(' / ')}`);
|
|
149
|
+
line(' 若其中某个确是本系统的端变体 → 改名为 <端>-end.md 或移入 variants/,或用 `--variant <name>` 显式纳入');
|
|
150
|
+
}
|
|
218
151
|
}
|
|
219
152
|
else if (singleFile) line('输入模式: 单文件(主体已含全部 9 段)');
|
|
220
153
|
if (VARIANT) line(`variant 过滤: ${VARIANT}`);
|
|
@@ -322,15 +255,33 @@ if (css === null) {
|
|
|
322
255
|
}
|
|
323
256
|
line();
|
|
324
257
|
|
|
325
|
-
// ── 六层审计报告(advisory;v0.54.0 新增,§4.1.5
|
|
258
|
+
// ── 六层审计报告(advisory;v0.54.0 新增,§4.1.5;v0.55.0 三态 + L3/L4 改造)──
|
|
326
259
|
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
260
|
+
// 标签规则(v0.55.0 三态):
|
|
261
|
+
// `true` → ✅ / `false` → ❌(v1)或 ⚠️(legacy 降级)/ **`'warn'` → 恒 ⚠️,绕过降级逻辑**。
|
|
262
|
+
// `'warn'` 专用于"新增可选内容缺失":不得让既有合规系统回归 FAIL(D-15)。
|
|
263
|
+
// 原实现只有二值 + 按 contract 降级 → 现场 `contract: v1` 系统缺页面范式声明会打 ❌,
|
|
264
|
+
// 与 D-15 直接冲突;且只改明细行不改汇总行会**同一次运行出现相反标签**(P1.5 二轮实证)。
|
|
329
265
|
// 报告中所有标签均不阻断(不影响 exit code)。
|
|
330
266
|
|
|
331
267
|
const contract = markdown ? parseContract(markdown) : null;
|
|
332
|
-
|
|
268
|
+
|
|
269
|
+
// contract 值域(v0.55.0):已知 v1 / legacy;**未知值(如 v2)按最严档(等同 v1)** + WARN——
|
|
270
|
+
// 不再静默归入降级。未知意味着"更新的规范",从严比从宽安全。
|
|
271
|
+
const CONTRACT_KNOWN = ['v1', 'legacy'];
|
|
272
|
+
const contractUnknown = contract !== null && !CONTRACT_KNOWN.includes(contract);
|
|
273
|
+
if (contractUnknown) {
|
|
274
|
+
warnLine(`检测到未识别的 contract 取值 \`${contract}\`,按最严档(等同 v1)判定`);
|
|
275
|
+
}
|
|
276
|
+
// 格式诊断(v0.55.0):段存在 + 提到 contract + 解析不出 → 显式告警(原为静默 unset)
|
|
277
|
+
const contractFormatIssue = markdown ? contractFormatWarning(markdown) : null;
|
|
278
|
+
if (contractFormatIssue) warnLine(contractFormatIssue);
|
|
279
|
+
|
|
280
|
+
const downgrade = !STRICT && !contractUnknown && contract !== 'v1';
|
|
281
|
+
|
|
333
282
|
const labelOf = (ok) => ok ? '✅' : (downgrade ? '⚠️' : '❌');
|
|
283
|
+
/** 三态渲染:`'warn'` 绕过 labelOf 恒输出 ⚠️。 */
|
|
284
|
+
const badge = (v) => (v === 'warn' ? '⚠️' : (v === true ? '✅' : labelOf(false)));
|
|
334
285
|
|
|
335
286
|
const layerResults = {};
|
|
336
287
|
|
|
@@ -405,19 +356,79 @@ if (markdown === null) {
|
|
|
405
356
|
}
|
|
406
357
|
}
|
|
407
358
|
|
|
408
|
-
// L3
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
//
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
359
|
+
// ── L3 业务模式 / L4 页面范式(v0.55.0 改造,设计 §8.2.5)──
|
|
360
|
+
//
|
|
361
|
+
// 检测面 = **设计系统源文本,按端分别求值**。两个理由均为 P1.5 实证:
|
|
362
|
+
// ① **不再读 primer.md**:原判据 `/layouts|页面范式|页面节奏/` 对 markdown + primer 匹配,
|
|
363
|
+
// 而 primer 里 gen-primer 硬编码的 `## 页面范式` 段**必然命中** → L3 恒通过,
|
|
364
|
+
// 等价于重复断言 L5,且**用派生物测源 = 自证**。
|
|
365
|
+
// ② **不用合并文本**:`sectionBodyRaw` 用 `findIndex` **只取首个** `## layout`,
|
|
366
|
+
// 合并顺序恒为 base → b-end → c-end → **c-end 的声明永不解析**。
|
|
367
|
+
// `both` 是默认 target;既有纪律见 `creation-flow.md`("合并断言掩盖单端残缺")。
|
|
368
|
+
const endFiles = mdFiles.filter(f =>
|
|
369
|
+
KNOWN_VARIANT_RE.test(basename(f)) || f.includes(`${sep}variants${sep}`));
|
|
370
|
+
const scopes = endFiles.length >= 2
|
|
371
|
+
? endFiles.map(f => ({ name: basename(f).replace(/-end\.md$/i, ''), text: readFileOrNull(f) || '' }))
|
|
372
|
+
: [{ name: null, text: markdown }];
|
|
373
|
+
const arche = scopes.map(s => ({ ...s, ...parsePageArchetype(s.text) }));
|
|
374
|
+
|
|
375
|
+
// L3 = 来源声明层(值域三值;未声明 → 'warn',**不走 FAIL** —— 新增可选内容不得让存量系统回归,D-15)
|
|
376
|
+
const l3vals = arche.map(a => {
|
|
377
|
+
if (!a.declared) return 'warn';
|
|
378
|
+
const s = a.source || '';
|
|
379
|
+
return (/^引用内置$/.test(s) || /^项目自有/.test(s) || /^同/.test(s)) ? true : 'warn';
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// L4a = 布局基础(栅格/断点)。v0.55.0 **收紧 + 三形态**:
|
|
383
|
+
// 原裸子串 `/断点|breakpoint|sm|md|lg|栅格|grid/` 被 `.md` 路径文本保证命中
|
|
384
|
+
// (`layouts.md` ⊃ `md`,`b-end.md` 同理)→ 空检测。
|
|
385
|
+
// 现要求"名 + 值"配对,接受三种真实写法:
|
|
386
|
+
// ① 键形态:`栅格:12 列` / `断点:sm 640`
|
|
387
|
+
// ② 值形态:`sm 640` / `md 1024px`
|
|
388
|
+
// ③ **表格形态**:`| sm | 640px |` —— 现场 b-end.md 用 4 列表格声明断点,
|
|
389
|
+
// 只认前两种会把它判成"缺断点"(P1.5 三轮预警的假阴性,实施期实测确认)
|
|
390
|
+
const L4A_KEY_RE = /(栅格|断点)\s*[::]/i;
|
|
391
|
+
// 值形态:`sm 640` / `sm(640)` / `` `sm` `≥ 768px` ``(后接数字,容 24 字符间隔)
|
|
392
|
+
const L4A_VALUE_RE = /\b(xs|sm|md|lg|xl)\b[^\n]{0,24}?\d{3,4}/i;
|
|
393
|
+
// 表格形态:`| \`sm\` | \`≥ 768px\` |` —— 现场 b-end.md 的实际写法(反引号包裹 + ≥ 符号)
|
|
394
|
+
const L4A_TABLE_RE = /\|\s*`?\s*(xs|sm|md|lg|xl)\s*`?\s*\|[^|\n]*\d{3,4}/i;
|
|
395
|
+
const l4avals = arche.map(a => {
|
|
396
|
+
const body = sectionBodyRaw(a.text, 'layout');
|
|
397
|
+
if (body === null) return false; // 缺 layout 段是真问题(必填段)→ 走降级规则
|
|
398
|
+
return L4A_KEY_RE.test(body) || L4A_VALUE_RE.test(body) || L4A_TABLE_RE.test(body);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// L4b = 页面范式内容层:引用内置 / 同 <端> → 免检;项目自有 → **子块内**页面类型表行数 ≥3
|
|
402
|
+
const l4bvals = arche.map(a => {
|
|
403
|
+
if (!a.declared) return 'warn';
|
|
404
|
+
const s = a.source || '';
|
|
405
|
+
if (/^引用内置$/.test(s) || /^同/.test(s)) return true;
|
|
406
|
+
if (/^项目自有/.test(s)) return a.tableRows >= 3 ? true : 'warn';
|
|
407
|
+
return 'warn';
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const agg = (vals) => vals.every(v => v === true) ? true
|
|
411
|
+
: (vals.some(v => v === 'warn') ? 'warn' : false);
|
|
412
|
+
const perEnd = (vals) => arche.map((a, i) => `${a.name} ${vals[i] === true ? '✅' : '⚠️'}`).join(' / ');
|
|
413
|
+
|
|
414
|
+
const l3 = agg(l3vals);
|
|
415
|
+
const l4a = agg(l4avals);
|
|
416
|
+
const l4b = agg(l4bvals);
|
|
417
|
+
const l4 = (l4a === true && l4b === true) ? true : ((l4a === 'warn' || l4b === 'warn') ? 'warn' : false);
|
|
418
|
+
|
|
419
|
+
layerResults['L3-业务模式'] = l3;
|
|
420
|
+
layerResults['L4-页面范式'] = l4;
|
|
421
|
+
if (arche.length > 1) {
|
|
422
|
+
line(`L3 业务模式 ${badge(l3)} ${perEnd(l3vals)}(按端求值)`);
|
|
423
|
+
line(`L4 页面范式 ${badge(l4)} 布局 ${perEnd(l4avals)} | 范式 ${perEnd(l4bvals)}`);
|
|
424
|
+
} else {
|
|
425
|
+
const a0 = arche[0];
|
|
426
|
+
line(`L3 业务模式 ${badge(l3)} ${l3 === true
|
|
427
|
+
? `页面范式来源已声明(${a0.source})`
|
|
428
|
+
: '未声明页面范式来源 —— 可在 layout 段补 `页面范式来源:引用内置`,或经 iterate 补写(不阻断)'}`);
|
|
429
|
+
const rowInfo = /^项目自有/.test(a0.source || '') ? `(页面类型 ${a0.tableRows} 行)` : '';
|
|
430
|
+
line(`L4 页面范式 ${badge(l4)} 布局基础 ${badge(l4a)} | 页面范式内容 ${badge(l4b)}${rowInfo}`);
|
|
431
|
+
}
|
|
421
432
|
|
|
422
433
|
// L5 Primer(同目录 primer.md 存在)
|
|
423
434
|
const primerExists = existsSync(join(dsDir, 'primer.md'));
|
|
@@ -425,14 +436,19 @@ if (markdown === null) {
|
|
|
425
436
|
line(`L5 Primer ${labelOf(primerExists)} ${primerExists ? 'primer.md 存在' : '未生成 primer.md'}`);
|
|
426
437
|
|
|
427
438
|
line('------------------------------------------');
|
|
428
|
-
|
|
429
|
-
const
|
|
439
|
+
// v0.55.0:计数用**严格比较** `=== true`——原 `filter(Boolean)` 会把 `'warn'`(truthy)也算达标。
|
|
440
|
+
const vals = Object.values(layerResults);
|
|
441
|
+
const passed = vals.filter(v => v === true).length;
|
|
442
|
+
const warnCount = vals.filter(v => v === 'warn').length;
|
|
443
|
+
const total = vals.length;
|
|
430
444
|
const mark = (k, v) => {
|
|
431
445
|
if (k === 'L1-双主题' && !v) return '⏭️';
|
|
446
|
+
if (v === 'warn') return '⚠️'; // 三态:与明细行同源(否则同一次运行出现相反标签)
|
|
432
447
|
return v ? '✅' : (downgrade ? '⚠️' : '❌');
|
|
433
448
|
};
|
|
434
449
|
line(`合规层:${Object.entries(layerResults).map(([k, v]) => `${k} ${mark(k, v)}`).join(' | ')}`);
|
|
435
|
-
line(`(${passed}/${total}
|
|
450
|
+
line(`(${passed}/${total} 层达标${warnCount ? `;${warnCount} 项待补写` : ''};contract=${contract || 'unset'}`
|
|
451
|
+
+ `${downgrade ? ' → 不达标降级 WARN' : ''}${contractUnknown ? '(未识别取值,按最严档)' : ''};不输出总分)`);
|
|
436
452
|
}
|
|
437
453
|
line();
|
|
438
454
|
|
|
@@ -440,10 +456,13 @@ line();
|
|
|
440
456
|
|
|
441
457
|
if (JSON_OUT) {
|
|
442
458
|
const report = {
|
|
443
|
-
|
|
459
|
+
// v0.55.0:`layers` 值域由 boolean 升为 `true | false | 'warn'` → schemaVersion 升版。
|
|
460
|
+
// 按端求值时(both)各层值为聚合结果,逐端明细见 stdout。
|
|
461
|
+
schemaVersion: '0.21.0',
|
|
444
462
|
hardChecks: { passed: issues.length === 0, issues },
|
|
445
463
|
layers: layerResults,
|
|
446
464
|
contract: contract || null,
|
|
465
|
+
contractUnknown,
|
|
447
466
|
strict: STRICT,
|
|
448
467
|
};
|
|
449
468
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { readState } from './lib/state-loader.mjs';
|
|
6
|
+
import { parseTaskLine } from './lib/md-normalize.mjs';
|
|
6
7
|
|
|
7
8
|
const CODE_EXTS = [
|
|
8
9
|
'mjs', 'js', 'ts', 'jsx', 'tsx', 'cjs',
|
|
@@ -30,8 +31,16 @@ function readText(dir, name) {
|
|
|
30
31
|
return existsSync(p) ? readFileSync(p, 'utf-8') : '';
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* tasks.md 任务行计数——**直接决定 hotfix(≤2) / tweak(≤4) / full 档位**。
|
|
36
|
+
*
|
|
37
|
+
* v0.55.0 §8.4.3 横展(FB-4,约束③ 高风险点):识别改用共享原语 `parseTaskLine`。
|
|
38
|
+
* 原 `/^- \[([ x])\]/gm` 的三个差异面:**只认小写 `x`**(`[X]` 漏计)、**不容缩进**、
|
|
39
|
+
* **不认 `*` bullet**。三者都会让 taskCount 变小 → 档位被低估(full 误判为 hotfix/tweak)。
|
|
40
|
+
* 差异对照见 tests/lib/checkbox-consistency.test.mjs。
|
|
41
|
+
*/
|
|
33
42
|
function countTasks(tasks) {
|
|
34
|
-
return
|
|
43
|
+
return tasks.split('\n').map(line => parseTaskLine(line)).filter(Boolean).length;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
function collectFiles(text) {
|
|
@@ -207,25 +207,39 @@ function buildCurrentStateSection(aggregates) {
|
|
|
207
207
|
* 4. 块范围 = 锚行 → 下一个块起点,块起点 ∈ {`^### change:`、`^## ` / `^# `、`^<!-- arch:`、
|
|
208
208
|
* 文件末尾}。边界之外的内容(含 change 级模板残留表)由本 change 块独占,随替换清理。
|
|
209
209
|
* 5. 删除死变量 `anchorRe`(`/^#{3,4}\s*.*change[:\s].*$/mi`,从未被引用)。
|
|
210
|
+
*
|
|
211
|
+
* v0.55.0 §8.4.3 横展(FB-4):锚行与日志标题容忍加粗/全角冒号等**纯格式变体**
|
|
212
|
+
* (`### **change:foo**`、`## **演进日志**`)。放宽前这类锚行**识别不到** →
|
|
213
|
+
* 走"追加到标题后"分支 → 同一 change 的块被**重复追加**(与 R11 缺陷同型的膨胀形态,
|
|
214
|
+
* 只是触发条件是格式变体)。提示:`change:` 关键词与 `###` 层级、change 名本身不放宽。
|
|
215
|
+
* 检测到同 change 锚块 >1(历史重复)时 WARN,只替换首个,不静默。
|
|
210
216
|
*/
|
|
217
|
+
const EVOLUTION_LOG_HEADING_RE = /^#{2,3}\s*\d*\.?\s*\**\s*(演进日志|Evolution Log)\**/m;
|
|
218
|
+
|
|
211
219
|
function upsertEvolutionLog(globalContent, srcContent, changeName) {
|
|
212
220
|
// 从 change 源提取演进日志条目(标题 + 正文)
|
|
213
|
-
const logStart = srcContent.search(
|
|
221
|
+
const logStart = srcContent.search(EVOLUTION_LOG_HEADING_RE);
|
|
214
222
|
if (logStart < 0) return globalContent;
|
|
215
223
|
const logSection = srcContent.slice(logStart).trim();
|
|
216
224
|
const anchor = `### change:${changeName}`;
|
|
217
|
-
const logBlock = `${anchor}\n${logSection.replace(
|
|
225
|
+
const logBlock = `${anchor}\n${logSection.replace(EVOLUTION_LOG_HEADING_RE, '').trim()}`;
|
|
218
226
|
|
|
219
227
|
// 已存在 → 替换整块;否则追加到演进日志段末尾
|
|
220
|
-
const logHeadingRe =
|
|
221
|
-
|
|
228
|
+
const logHeadingRe = EVOLUTION_LOG_HEADING_RE;
|
|
229
|
+
// 锚行:`### change:<name>` 的宽容形态(容忍 `**` 包裹、全角冒号、冒号后空格)
|
|
230
|
+
const anchorSrc = `^###\\s*\\**\\s*change\\s*[::]\\s*\\**\\s*${escapeRegExp(changeName)}\\s*\\**\\s*$`;
|
|
231
|
+
const anchorRe = new RegExp(anchorSrc, 'm');
|
|
222
232
|
if (anchorRe.test(globalContent)) {
|
|
223
233
|
// 块起点:下一个 change 块 / 下一个一二级标题 / arch marker / 文件末尾
|
|
224
234
|
const blockRe = new RegExp(
|
|
225
|
-
|
|
226
|
-
+ `(
|
|
235
|
+
`${anchorSrc}[\\s\\S]*?`
|
|
236
|
+
+ `(?=^###\\s*\\**\\s*change\\s*[::]|^#{1,2}\\s|^<!-- arch:|$(?![\\s\\S]))`,
|
|
227
237
|
'm'
|
|
228
238
|
);
|
|
239
|
+
const duplicates = (globalContent.match(new RegExp(anchorSrc, 'gm')) || []).length;
|
|
240
|
+
if (duplicates > 1) {
|
|
241
|
+
console.warn(` [WARN] 演进日志存在 ${duplicates} 个 change:${changeName} 锚块(历史重复追加),本次只替换第一个,请人工清理其余块`);
|
|
242
|
+
}
|
|
229
243
|
const matched = globalContent.match(blockRe);
|
|
230
244
|
if (matched) {
|
|
231
245
|
// 被替换段内若含疑似人工内容(非标题/非表格行/非空行/非注释)→ 告警不静默
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
// → 两重失配 → 列语义整体错位 → 全局台账被写入 `上下文=extend`、
|
|
18
18
|
// `根实体=SysFrontSystem(不变)`。**这是污染而非缺失**。改判据为表头定位。
|
|
19
19
|
|
|
20
|
+
// v0.55.0 §8.4.3 横展(FB-4):强调剥离改为引用共享层 `md-normalize.mjs`
|
|
21
|
+
// (单一真相源)——本文件 v0.53.0 的实现已提升为全仓库约定,此处删除本地副本,
|
|
22
|
+
// 避免「两份实现各自漂移」的复发形态。
|
|
23
|
+
import { stripEmphasis } from './md-normalize.mjs';
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* HTTP 方法词表——**三个方法正则的唯一事实源**。
|
|
22
27
|
*
|
|
@@ -54,17 +59,6 @@ const PATH_PREFIX_RE = /^\/[\w\-/{},.]*/;
|
|
|
54
59
|
*/
|
|
55
60
|
export const ENDPOINT_HEADERS = new Set(['端点', 'Endpoint', 'API', '接口']);
|
|
56
61
|
|
|
57
|
-
/** 强调标记剥离:`**x**` / `__x__` / `*x*` / `_x_` → `x`(首尾成对才剥离;长的优先)。 */
|
|
58
|
-
function stripEmphasis(s) {
|
|
59
|
-
let out = s;
|
|
60
|
-
for (const [open, close] of [['**', '**'], ['__', '__'], ['*', '*'], ['_', '_']]) {
|
|
61
|
-
if (out.length > open.length + close.length && out.startsWith(open) && out.endsWith(close)) {
|
|
62
|
-
out = out.slice(open.length, -close.length).trim();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
62
|
/**
|
|
69
63
|
* 解析一行 markdown 表格为单元格数组(去反引号 + 剥离强调标记 + trim)。非表格行返回 null。
|
|
70
64
|
*
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// ds-inputs.mjs — 设计系统输入解析共享层(v0.55.0)
|
|
2
|
+
//
|
|
3
|
+
// ── 为什么存在(设计 §8.2.2 g)────────────────────────────────────────────────
|
|
4
|
+
// `resolveInputs` 原在 design-token-guard.mjs 内,gen-primer.mjs 需要同一套"从 base 路径
|
|
5
|
+
// 发现端变体"的逻辑(primer 的 digest 必须覆盖参与合并的全部文件,否则变体变更不判 STALE
|
|
6
|
+
// → prototype Step 0 gate 放行 → 原型静默用旧页面范式)。
|
|
7
|
+
//
|
|
8
|
+
// 但 guard **不能直接 import**:它是顶层副作用模块(顶层解析 argv、顶层 process.exit、
|
|
9
|
+
// 顶层打印整份报告)——import 它会让 gen-primer 先打印 guard 报告然后提前退出。
|
|
10
|
+
// 故把纯逻辑下沉到本模块,guard 与 gen-primer 双双 import。
|
|
11
|
+
//
|
|
12
|
+
// ── 输入规则(原 guard 注释,语义不变)──────────────────────────────────────
|
|
13
|
+
// ① 传入目录 → 以目录内 base.md 为主体;② 传入 base.md → 以它为主体;
|
|
14
|
+
// ③ 传入变体文件 → base.md 主体 + 该文件。
|
|
15
|
+
// ④ 主体自身已含全部 9 段(单文件系统 / 转换器产物)→ 单文件模式,不合并
|
|
16
|
+
// (避免卷入同目录无关 md)。
|
|
17
|
+
// ⑤ 主体不含全 9 段 → 合并**约定命名的端变体**(`*-end.md` + `variants/` 子目录);
|
|
18
|
+
// `--variant <name>` 可显式纳入非约定命名的变体(逃生通道)。
|
|
19
|
+
// 含必选段但未被纳入的其它 md → 告警列出(不静默补齐:一个 notes.md 不该把
|
|
20
|
+
// 残缺系统伪装成 PASS,也不该被静默忽略)。
|
|
21
|
+
//
|
|
22
|
+
// v0.55.0 新增:**单文件模式也执行 ignored 计算**(原实现在 `!subjectComplete` 分支内,
|
|
23
|
+
// 单文件模式整块被跳过 → 同目录变体静默不参与判据)。
|
|
24
|
+
|
|
25
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
26
|
+
import { dirname, join, basename, sep } from 'node:path';
|
|
27
|
+
import { extractH2Headers, hasSection } from './ds-parse.mjs';
|
|
28
|
+
|
|
29
|
+
/** 9 个必选 ## 段(design-system.md schema,设计 §4.1.1)。 */
|
|
30
|
+
export const REQUIRED_SECTIONS = [
|
|
31
|
+
'color', 'typography', 'spacing', 'layout', 'components',
|
|
32
|
+
'motion', 'voice', 'brand', 'anti-patterns',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** 可选 ## 段——仅作可见性报告,永不计入失败。 */
|
|
36
|
+
export const OPTIONAL_SECTIONS = [
|
|
37
|
+
'palette', 'aliases', 'extensions', 'principles', 'governance',
|
|
38
|
+
'来源与裁决记录', // v0.55.0:导入类创建条件必填(审计层 advisory,设计 §8.2.1)
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** 非段落文件:不参与合并,也不算"未纳入"告警。 */
|
|
42
|
+
export const NON_SECTION_FILES = new Set(['primer.md', 'pending.md', 'readme.md', 'index.md']);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 端变体命名约定:`*-end.md`(b-end / c-end / mobile-end …)+ `variants/` 子目录内全部 md
|
|
46
|
+
* (如 dark.md)。收紧为约定集而非「同目录任意 md」——否则一个含 `## spacing` 的 notes.md
|
|
47
|
+
* 就能补齐缺失段,把残缺系统伪装成 PASS。非约定名可用 `--variant <name>` 显式纳入。
|
|
48
|
+
*/
|
|
49
|
+
export const KNOWN_VARIANT_RE = /-end\.md$/i;
|
|
50
|
+
|
|
51
|
+
/** Read a file as UTF-8, or return null when it does not exist. */
|
|
52
|
+
export function readFileOrNull(filePath) {
|
|
53
|
+
if (!filePath || !existsSync(filePath)) return null;
|
|
54
|
+
return readFileSync(filePath, 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 设计系统目录内的全部候选 md(dir 与 variants/ 两层,排除 base 与非段落文件)。 */
|
|
58
|
+
export function designSystemMds(dir) {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const d of [dir, join(dir, 'variants')]) {
|
|
61
|
+
if (!existsSync(d)) continue;
|
|
62
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
64
|
+
if (entry.name === 'base.md' || NON_SECTION_FILES.has(entry.name.toLowerCase())) continue;
|
|
65
|
+
out.push(join(d, entry.name));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out.sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 约定命名的端变体候选。 */
|
|
72
|
+
export function variantCandidates(dir) {
|
|
73
|
+
return designSystemMds(dir).filter(f => KNOWN_VARIANT_RE.test(basename(f)) || f.includes(`${sep}variants${sep}`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 解析设计系统输入,返回 `{ dir, files, singleFile, ignored }`。
|
|
78
|
+
*
|
|
79
|
+
* @param {string} input 目录 / base.md / 变体文件路径
|
|
80
|
+
* @param {string|null} variantName `--variant <name>` 显式指定的变体名(逃生通道)
|
|
81
|
+
*/
|
|
82
|
+
export function resolveInputs(input, variantName) {
|
|
83
|
+
const isDir = existsSync(input) && statSync(input).isDirectory();
|
|
84
|
+
const dir = isDir ? input : dirname(input);
|
|
85
|
+
const parts = [];
|
|
86
|
+
|
|
87
|
+
if (isDir) {
|
|
88
|
+
parts.push(join(dir, 'base.md'));
|
|
89
|
+
} else if (basename(input) === 'base.md') {
|
|
90
|
+
parts.push(input);
|
|
91
|
+
} else {
|
|
92
|
+
parts.push(join(dir, 'base.md')); // 传入变体文件时仍以 base 为主体(base 优先)
|
|
93
|
+
parts.push(input);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const present = parts.filter(f => existsSync(f) && readFileOrNull(f) !== null);
|
|
97
|
+
const subject = present[0] ? readFileOrNull(present[0]) : null;
|
|
98
|
+
const subjectComplete = subject !== null
|
|
99
|
+
&& REQUIRED_SECTIONS.every(s => hasSection(extractH2Headers(subject), s));
|
|
100
|
+
|
|
101
|
+
let merged = [...present];
|
|
102
|
+
let ignored = [];
|
|
103
|
+
if (!subjectComplete) {
|
|
104
|
+
// --variant <name> 为显式指定:在**全部**候选 md 中按名匹配(含非约定命名),作为逃生通道
|
|
105
|
+
const variants = variantName
|
|
106
|
+
? designSystemMds(dir).filter(f => basename(f).includes(variantName))
|
|
107
|
+
: variantCandidates(dir);
|
|
108
|
+
for (const v of variants) if (!merged.includes(v)) merged.push(v);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// v0.55.0:ignored 计算**移出** `!subjectComplete` 分支——单文件模式下同目录变体
|
|
112
|
+
// 同样不参与判据,同样必须告警(原实现整块跳过 → 静默)。
|
|
113
|
+
{
|
|
114
|
+
const mergedSet = new Set(merged);
|
|
115
|
+
const scope = variantName ? [] : designSystemMds(dir);
|
|
116
|
+
for (const f of scope) {
|
|
117
|
+
if (mergedSet.has(f)) continue;
|
|
118
|
+
const text = readFileOrNull(f) || '';
|
|
119
|
+
const headers = extractH2Headers(text);
|
|
120
|
+
if (REQUIRED_SECTIONS.some(s => hasSection(headers, s))) ignored.push(f);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { dir, files: merged, singleFile: subjectComplete, ignored };
|
|
125
|
+
}
|