@niujiajie/freespec 0.0.0-stage → 1.15.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/LICENSE +22 -0
- package/README.md +58 -2
- package/bin/freespec.js +5 -0
- package/dist/cli/index.d.ts +25 -0
- package/dist/cli/index.js +359 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/index.d.ts +13 -0
- package/dist/commands/workflow/index.js +10 -0
- package/dist/commands/workflow/instructions.d.ts +39 -0
- package/dist/commands/workflow/instructions.js +565 -0
- package/dist/commands/workflow/new-change.d.ts +19 -0
- package/dist/commands/workflow/new-change.js +135 -0
- package/dist/commands/workflow/shared.d.ts +94 -0
- package/dist/commands/workflow/shared.js +169 -0
- package/dist/commands/workflow/status.d.ts +18 -0
- package/dist/commands/workflow/status.js +201 -0
- package/dist/core/artifact-graph/graph.d.ts +72 -0
- package/dist/core/artifact-graph/graph.js +165 -0
- package/dist/core/artifact-graph/index.d.ts +9 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +208 -0
- package/dist/core/artifact-graph/instruction-loader.js +268 -0
- package/dist/core/artifact-graph/outputs.d.ts +19 -0
- package/dist/core/artifact-graph/outputs.js +351 -0
- package/dist/core/artifact-graph/resolver.d.ts +89 -0
- package/dist/core/artifact-graph/resolver.js +342 -0
- package/dist/core/artifact-graph/schema.d.ts +27 -0
- package/dist/core/artifact-graph/schema.js +160 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +31 -0
- package/dist/core/artifact-graph/types.d.ts +40 -0
- package/dist/core/artifact-graph/types.js +59 -0
- package/dist/core/available-tools.d.ts +18 -0
- package/dist/core/available-tools.js +49 -0
- package/dist/core/change-layout.d.ts +17 -0
- package/dist/core/change-layout.js +30 -0
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +23 -0
- package/dist/core/change-metadata/schema.js +46 -0
- package/dist/core/change-status-policy.d.ts +49 -0
- package/dist/core/change-status-policy.js +50 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +17 -0
- package/dist/core/command-generation/adapters/amazon-q.js +32 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +27 -0
- package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
- package/dist/core/command-generation/adapters/atomcode.js +46 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +28 -0
- package/dist/core/command-generation/adapters/bob.d.ts +18 -0
- package/dist/core/command-generation/adapters/bob.js +33 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +32 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +29 -0
- package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
- package/dist/core/command-generation/adapters/codestudio.js +27 -0
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +33 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +28 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +31 -0
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
- package/dist/core/command-generation/adapters/easycode.js +28 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +28 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +27 -0
- package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
- package/dist/core/command-generation/adapters/gigacode.js +31 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +27 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +30 -0
- package/dist/core/command-generation/adapters/index.d.ts +40 -0
- package/dist/core/command-generation/adapters/index.js +40 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +27 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +14 -0
- package/dist/core/command-generation/adapters/opencode.js +38 -0
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +39 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +17 -0
- package/dist/core/command-generation/adapters/qwen.js +31 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +29 -0
- package/dist/core/command-generation/generator.js +43 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +114 -0
- package/dist/core/command-generation/toml.d.ts +19 -0
- package/dist/core/command-generation/toml.js +39 -0
- package/dist/core/command-generation/types.d.ts +63 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/command-generation/yaml.d.ts +36 -0
- package/dist/core/command-generation/yaml.js +48 -0
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +56 -0
- package/dist/core/config-schema.d.ts +98 -0
- package/dist/core/config-schema.js +308 -0
- package/dist/core/config.d.ts +50 -0
- package/dist/core/config.js +122 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +144 -0
- package/dist/core/file-view.d.ts +13 -0
- package/dist/core/file-view.js +14 -0
- package/dist/core/global-config.d.ts +101 -0
- package/dist/core/global-config.js +182 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.d.ts +57 -0
- package/dist/core/init.js +836 -0
- package/dist/core/list.d.ts +11 -0
- package/dist/core/list.js +197 -0
- package/dist/core/migration.d.ts +121 -0
- package/dist/core/migration.js +540 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.d.ts +47 -0
- package/dist/core/openspec-root.js +196 -0
- package/dist/core/planning-home.d.ts +16 -0
- package/dist/core/planning-home.js +67 -0
- package/dist/core/profile-sync-drift.d.ts +31 -0
- package/dist/core/profile-sync-drift.js +201 -0
- package/dist/core/profiles.d.ts +19 -0
- package/dist/core/profiles.js +33 -0
- package/dist/core/project-config.d.ts +132 -0
- package/dist/core/project-config.js +519 -0
- package/dist/core/references.d.ts +10 -0
- package/dist/core/references.js +46 -0
- package/dist/core/root-selection.d.ts +59 -0
- package/dist/core/root-selection.js +132 -0
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +11 -0
- package/dist/core/shared/index.js +11 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +45 -0
- package/dist/core/shared/skill-files.d.ts +7 -0
- package/dist/core/shared/skill-files.js +43 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +125 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +99 -0
- package/dist/core/shared/tool-detection.js +318 -0
- package/dist/core/shared-skill-target.d.ts +51 -0
- package/dist/core/shared-skill-target.js +239 -0
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/store/errors.js +22 -0
- package/dist/core/store/index.d.ts +2 -0
- package/dist/core/store/index.js +2 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/personal.d.ts +22 -0
- package/dist/core/templates/workflows/personal.js +225 -0
- package/dist/core/update-plan.d.ts +127 -0
- package/dist/core/update-plan.js +390 -0
- package/dist/core/update.d.ts +154 -0
- package/dist/core/update.js +723 -0
- package/dist/core/version-check.d.ts +188 -0
- package/dist/core/version-check.js +906 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +35 -0
- package/dist/prompts/searchable-multi-select.js +188 -0
- package/dist/telemetry/config.d.ts +37 -0
- package/dist/telemetry/config.js +148 -0
- package/dist/telemetry/index.d.ts +39 -0
- package/dist/telemetry/index.js +217 -0
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +18 -0
- package/dist/ui/welcome-screen.js +199 -0
- package/dist/utils/change-metadata.d.ts +116 -0
- package/dist/utils/change-metadata.js +324 -0
- package/dist/utils/change-utils.d.ts +79 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +104 -0
- package/dist/utils/command-references.js +190 -0
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/executable-path.d.ts +5 -0
- package/dist/utils/executable-path.js +44 -0
- package/dist/utils/file-system.d.ts +58 -0
- package/dist/utils/file-system.js +432 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +85 -0
- package/dist/utils/interactive.js +174 -0
- package/dist/utils/item-discovery.d.ts +15 -0
- package/dist/utils/item-discovery.js +33 -0
- package/dist/utils/line-endings.d.ts +34 -0
- package/dist/utils/line-endings.js +46 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +185 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +94 -0
- package/dist/utils/task-progress.d.ts +67 -0
- package/dist/utils/task-progress.js +184 -0
- package/package.json +74 -4
- package/schemas//351/273/230/350/256/244/schema.yaml +68 -0
- package/schemas//351/273/230/350/256/244/templates//344/273/273/345/212/241/346/270/205/345/215/225.md +25 -0
- package/schemas//351/273/230/350/256/244/templates//346/265/213/350/257/225/347/224/250/344/276/213.md +34 -0
- package/schemas//351/273/230/350/256/244/templates//350/256/276/350/256/241/346/226/207/346/241/243.md +172 -0
- package/schemas//351/273/230/350/256/244/templates//351/234/200/346/261/202/346/226/207/346/241/243.md +19 -0
- package/skill-files//344/273/273/345/212/241/references//346/212/244/346/240/217.md +43 -0
- package/skill-files//344/273/273/345/212/241/references//347/262/222/345/272/246.md +23 -0
- package/skill-files//344/273/273/345/212/241/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//344/273/273/345/212/241/scripts/check-tasks.mjs +83 -0
- package/skill-files//345/206/231Java/references/Java/350/247/204/350/214/203.md +25 -0
- package/skill-files//345/206/231Java/references//345/215/261/351/231/251/345/206/231/346/263/225.md +27 -0
- package/skill-files//345/206/231Java/references//345/256/214/346/210/220/345/257/271/347/205/247.md +15 -0
- package/skill-files//345/206/231Java/references//346/212/244/346/240/217.md +35 -0
- package/skill-files//345/206/231Java/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/206/231Java/references//350/270/251/345/235/221.md +17 -0
- package/skill-files//345/256/241Java/references//346/212/244/346/240/217.md +19 -0
- package/skill-files//345/256/241Java/references//347/244/272/344/276/213.md +23 -0
- package/skill-files//345/256/241Java/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//345/256/241Java/references//350/247/204/345/210/231.md +23 -0
- package/skill-files//345/256/241Java/references//351/242/206/345/237/237/346/243/200/351/241/271.md +58 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//346/212/244/346/240/217.md +18 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/273/264/345/272/246.md +17 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//350/270/251/345/235/221.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//344/272/244/346/216/245.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//346/212/244/346/240/217.md +14 -0
- package/skill-files//346/226/260/345/273/272/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//346/267/261/346/200/235/config/prompts.yaml +114 -0
- package/skill-files//346/267/261/346/200/235/references//344/270/211/346/226/271/346/240/270/345/257/271.md +72 -0
- package/skill-files//346/267/261/346/200/235/references//344/276/213/345/255/220.md +39 -0
- package/skill-files//346/267/261/346/200/235/references//346/200/216/344/271/210/346/203/263.md +61 -0
- package/skill-files//346/267/261/346/200/235/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//346/267/261/346/200/235/references//347/202/271/351/200/211/346/217/220/351/227/256.md +66 -0
- package/skill-files//346/267/261/346/200/235/references//347/231/273/350/256/260.md +45 -0
- package/skill-files//346/267/261/346/200/235/references//347/272/246/346/235/237.md +9 -0
- package/skill-files//347/224/250/344/276/213/references//346/200/216/344/271/210/345/206/231.md +13 -0
- package/skill-files//347/224/250/344/276/213/references//346/212/244/346/240/217.md +42 -0
- package/skill-files//347/224/250/344/276/213/references//347/272/246/346/235/237.md +5 -0
- package/skill-files//347/224/250/344/276/213/scripts/check-cases.mjs +70 -0
- package/skill-files//350/256/276/350/256/241/references//345/207/206/345/244/207.md +25 -0
- package/skill-files//350/256/276/350/256/241/references//346/212/244/346/240/217.md +134 -0
- package/skill-files//350/256/276/350/256/241/references//346/226/207/351/243/216.md +17 -0
- package/skill-files//350/256/276/350/256/241/references//347/225/214/351/235/242.md +18 -0
- package/skill-files//350/256/276/350/256/241/references//347/261/273/345/236/213.md +19 -0
- package/skill-files//350/256/276/350/256/241/references//347/272/246/346/235/237.md +25 -0
- package/skill-files//350/256/276/350/256/241/scripts/_smoke.mjs +72 -0
- package/skill-files//350/256/276/350/256/241/scripts/check-design.mjs +331 -0
- package/skill-files//351/234/200/346/261/202/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//351/234/200/346/261/202/references//346/226/207/351/243/216.md +23 -0
- package/skill-files//351/234/200/346/261/202/references//346/267/261/346/200/235/346/221/204/345/205/245.md +21 -0
- package/skill-files//351/234/200/346/261/202/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//351/234/200/346/261/202/references//351/234/200/346/261/202/345/276/205/345/256/232.md +13 -0
- package/skill-files//351/234/200/346/261/202/scripts/check-requirements.mjs +60 -0
- package/skill-files//351/252/214/346/224/266/references//346/212/244/346/240/217.md +27 -0
- package/skill-files//351/252/214/346/224/266/references//346/240/270/345/257/271.md +17 -0
- package/skill-files//351/252/214/346/224/266/references//347/272/246/346/235/237.md +7 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 有页面时的前端页面原型设计
|
|
2
|
+
|
|
3
|
+
没有页面就不要写这一章。
|
|
4
|
+
|
|
5
|
+
哪些页要单写一节,只按护栏里的「改到的页面」。不要凭页面看起来变没变来取舍。这一章放在哪、章号怎么排,只按 `references/类型.md`。
|
|
6
|
+
|
|
7
|
+
每一页按这个顺序。框线简图留在表外。选项、规则、对应接口没有内容的那张表整张删掉,不要留空表。
|
|
8
|
+
|
|
9
|
+
1. 谁从哪个菜单进入、哪几步、哪步可跳过。
|
|
10
|
+
2. 模板里的「本页改动」表。
|
|
11
|
+
3. ASCII 简图。用文本框线,标出这次的区域、字段、按钮、操作区。
|
|
12
|
+
4. 选项表:控件、选项文案、存库的码。枚举挂在这一页下面。
|
|
13
|
+
5. 规则表:编号、规则。
|
|
14
|
+
6. 接口表:操作、方法、路径。查询用问号参数。纯前端调用已有接口时,也写在这张表里。
|
|
15
|
+
|
|
16
|
+
页面长什么样以这一章为准。需求功能点里的草图只说明有哪些块。两处不一致时,先停下来问,不要各写各的。
|
|
17
|
+
|
|
18
|
+
路由和前端文件写在变更方案的改动清单里,不要在这一章再抄一棵树。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 涉及范围
|
|
2
|
+
|
|
3
|
+
标题下先写一行「涉及:」,只留一个:纯前端、前后端、纯后端、仅数据或配置。拿不准就停下来问,不要默认写成前后端。
|
|
4
|
+
|
|
5
|
+
- 纯前端:有页面。不改接口契约,不改服务,不改库和配置。页面调用的已有接口写在页面原型里。
|
|
6
|
+
- 前后端:有页面,并且还改接口、服务、库、文件或配置里的至少一项。
|
|
7
|
+
- 纯后端:没有页面。按实际改动留下数据、接口、服务这几章。
|
|
8
|
+
- 仅数据或配置:只改表、文件、缓存、定时任务、开关、消息或线程池。没有页面,没有新的接口契约,没有要写步骤的服务。
|
|
9
|
+
|
|
10
|
+
章节按这个顺序取,用到的才写。写出来的从第一章连续编号。节号跟着章号走。没用到的章和节整段删掉。
|
|
11
|
+
|
|
12
|
+
1. 概述。每次都有。
|
|
13
|
+
2. 变更方案。每次都有。没有取舍就删掉「取舍」这一节。
|
|
14
|
+
3. 数据与配置。有表、文件、缓存、定时任务、开关、消息或线程池才有。章内只留有内容的节。
|
|
15
|
+
4. 前端页面原型设计。哪些页算改到,只按护栏,不凭页面看起来变没变。一页都没有就整章删掉。放在数据与配置之后、接口设计之前。没有数据章时,紧接在变更方案后面。这一章里面怎么写,只看 `references/界面.md`。
|
|
16
|
+
5. 接口设计。这次改接口契约才有。纯前端不单开这一章。
|
|
17
|
+
6. 服务层设计。这次改服务步骤、定时任务或消息消费才有。纯转发且没有分支的方法不写方法细节。一个方法都不需要写步骤、映射也没有新内容时,整章删掉。
|
|
18
|
+
|
|
19
|
+
不要改这些章名。不要出现缺了第二章却还有第三章。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 约束
|
|
2
|
+
|
|
3
|
+
这里只写尽量做到的事。必须项在 `references/护栏.md`,交卷前读护栏,并跑检查脚本。脚本会读下面的「脚本核对」,没做到只提示,不挡落盘。
|
|
4
|
+
|
|
5
|
+
## 尽量
|
|
6
|
+
|
|
7
|
+
- 影响与测试的每一行对上功能点原句。
|
|
8
|
+
- 取舍写清怎么回滚。
|
|
9
|
+
- 有页面时写清谁从哪个菜单进入、哪几步。
|
|
10
|
+
- 旧数据要进新表时,写出怎么迁。
|
|
11
|
+
|
|
12
|
+
## 脚本核对
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"核对": [
|
|
17
|
+
{
|
|
18
|
+
"说明": "影响与测试里「{列}」是空的",
|
|
19
|
+
"章": "变更方案",
|
|
20
|
+
"节": "影响与测试",
|
|
21
|
+
"列不能空": ["对应功能点"]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const checker = path.join(path.dirname(fileURLToPath(import.meta.url)), 'check-design.mjs');
|
|
8
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-check-'));
|
|
9
|
+
|
|
10
|
+
const tree = `订单\n├── 改 订单接口`;
|
|
11
|
+
const list = (rows) => `### 2.1 改动清单\n\n\`\`\`text\n${tree}\n\`\`\`\n\n| 层 | 文件 | 新/改/不动 | 动什么 | 追溯自 | 对应页面节 |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n${rows}\n`;
|
|
12
|
+
const impact = (point) => `### 2.2 影响与测试关注\n\n| 影响谁 | 代码改没改 | 测试要盯什么 | 对应功能点 |\n| :--- | :--- | :--- | :--- |\n| 下单的人 | 改 | 能下单 | ${point} |\n`;
|
|
13
|
+
const api = `## 第五章 接口设计\n\n### 5.1 接口清单\n\n| 请求方式 | 路径 | 作用 |\n| :--- | :--- | :--- |\n| POST | /order | 下单 |\n\n### 5.2 接口详细设计\n\n#### 接口 1:POST /order(修改)\n\n请求参数:\n\n| 字段 | 位置 |\n| :--- | :--- |\n| 无 | 无 |\n\n成功响应:\n\n| 字段 | 类型 | 说明 |\n| :--- | :--- | :--- |\n| 无 | 无 | 无 |\n\n业务错误:\n\n| 场景 | 错误码 |\n| :--- | :--- |\n| 无 | 无 |\n`;
|
|
14
|
+
const page = `## 第四章 前端页面原型设计\n\n### 4.1 订单页\n\n谁从订单菜单进入,一步提交。\n\n| 本页改动 | 引用接口 | 接口本次变化 |\n| :--- | :--- | :--- |\n| 改提交按钮 | POST /order | 增加备注字段 |\n`;
|
|
15
|
+
|
|
16
|
+
function doc(body) {
|
|
17
|
+
return `# 登录 · 设计说明\n\n涉及:纯后端\n\n## 第一章 概述\n\n### 1.1 技术现状\n\n现在这样。\n\n## 第二章 变更方案\n\n${body}\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cases = [
|
|
21
|
+
{
|
|
22
|
+
name: '纯后端只有控制层,应通过',
|
|
23
|
+
body: doc(`${list('| Controller | OrderController | 改 | 改入参 | 无 | 无 |')}\n${impact('用户可以提交订单')}\n${api}`),
|
|
24
|
+
code: 0,
|
|
25
|
+
absent: ['前端接口层', '组件'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: '纯后端没有控制层,应只报控制层',
|
|
29
|
+
body: doc(`${list('| 服务 | OrderService | 改 | 改入参 | 无 | 无 |')}\n${impact('用户可以提交订单')}\n${api}`),
|
|
30
|
+
code: 1,
|
|
31
|
+
present: ['追溯缺少控制层'],
|
|
32
|
+
absent: ['前端接口层', '组件'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: '有页面却没有前端接口层,应报接口层',
|
|
36
|
+
body: doc(`${list('| Controller | OrderController | 改 | 改入参 | 无 | 无 |\n| 组件 | OrderPage | 改 | 改按钮 | OrderController | 4.1 订单页 |')}\n${impact('用户可以提交订单')}\n${page}\n${api}`),
|
|
37
|
+
code: 1,
|
|
38
|
+
present: ['追溯缺少前端接口层'],
|
|
39
|
+
absent: ['追溯缺少控制层', '追溯缺少引用该接口的组件'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: '没有页面也没有接口时,功能点空着应通过',
|
|
43
|
+
body: doc(`${list('| 配置 | app.yml | 改 | 改开关 | 无 | 无 |')}\n${impact('')}`),
|
|
44
|
+
code: 0,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: '有接口但功能点空着,应拦住',
|
|
48
|
+
body: doc(`${list('| Controller | OrderController | 改 | 改入参 | 无 | 无 |')}\n${impact('')}\n${api}`),
|
|
49
|
+
code: 1,
|
|
50
|
+
present: ['对应功能点'],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let failed = 0;
|
|
55
|
+
for (const item of cases) {
|
|
56
|
+
const file = path.join(dir, `${item.name}.md`);
|
|
57
|
+
fs.writeFileSync(file, item.body);
|
|
58
|
+
const run = spawnSync(process.execPath, [checker, file], { encoding: 'utf8' });
|
|
59
|
+
const output = `${run.stdout}\n${run.stderr}`;
|
|
60
|
+
const problems = [];
|
|
61
|
+
if (run.status !== item.code) problems.push(`退出码 ${run.status},期望 ${item.code}`);
|
|
62
|
+
for (const text of item.present ?? []) if (!output.includes(text)) problems.push(`缺少「${text}」`);
|
|
63
|
+
for (const text of item.absent ?? []) if (output.includes(text)) problems.push(`多报了「${text}」`);
|
|
64
|
+
if (problems.length) {
|
|
65
|
+
failed += 1;
|
|
66
|
+
console.error(`FAIL ${item.name}: ${problems.join(';')}\n${output}`);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(`OK ${item.name}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
if (failed) process.exit(1);
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 通用核对。条文在 references/护栏.md 和 references/约束.md 的 json 块里。
|
|
4
|
+
* 护栏未通过则退出码 1。约束里如果还有核对,只打印,不挡落盘。
|
|
5
|
+
* 用法:node check-design.mjs <设计文档.md>
|
|
6
|
+
*
|
|
7
|
+
* 一条规则可用:说明、仅当有章、章、节、范围内含、范围内不含、匹配则失败、要匹配、
|
|
8
|
+
* 表头要含、列不能空、拆块、每个标题、对账。
|
|
9
|
+
* 「至少各有一行」里的模式可以是字符串,也可以是 { 正则, 说明, 当有章 }。
|
|
10
|
+
* 说明里的 {标题} {文件} {列} {词} 会换成当前这一处。
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const docPath = process.argv[2];
|
|
17
|
+
if (!docPath) {
|
|
18
|
+
console.error('用法:node check-design.mjs <设计文档.md>');
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const references = path.join(here, '..', 'references');
|
|
24
|
+
|
|
25
|
+
function readRules(name) {
|
|
26
|
+
const rulePath = path.join(references, name);
|
|
27
|
+
if (!fs.existsSync(rulePath)) return [];
|
|
28
|
+
const raw = fs.readFileSync(rulePath, 'utf8');
|
|
29
|
+
const fence = raw.match(/```json\s*([\s\S]*?)```/);
|
|
30
|
+
if (!fence) return [];
|
|
31
|
+
try {
|
|
32
|
+
const data = JSON.parse(fence[1]);
|
|
33
|
+
return Array.isArray(data.核对) ? data.核对 : [];
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`${name} 里的 json 读不了:${error.message}`);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const text = fs.readFileSync(docPath, 'utf8');
|
|
41
|
+
|
|
42
|
+
function sliceAt(source, startRe, endRe) {
|
|
43
|
+
const match = startRe.exec(source);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
const rest = source.slice(match.index + match[0].length);
|
|
46
|
+
const next = rest.search(endRe);
|
|
47
|
+
return (next === -1 ? rest : rest.slice(0, next)).trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function chapter(source, name) {
|
|
51
|
+
return sliceAt(source, new RegExp(`^## .*${name}.*$`, 'm'), /^## /m);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function subsection(source, name) {
|
|
55
|
+
return sliceAt(source, new RegExp(`^### .*${name}.*$`, 'm'), /^### /m);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scopeOf(source, rule) {
|
|
59
|
+
let scope = source;
|
|
60
|
+
if (rule.章) {
|
|
61
|
+
scope = chapter(source, rule.章);
|
|
62
|
+
if (scope == null) return null;
|
|
63
|
+
}
|
|
64
|
+
if (rule.节) {
|
|
65
|
+
scope = subsection(scope, rule.节);
|
|
66
|
+
if (scope == null) return null;
|
|
67
|
+
}
|
|
68
|
+
if (rule.范围内含 && !scope.includes(rule.范围内含)) return null;
|
|
69
|
+
if (rule.范围内不含 && scope.includes(rule.范围内不含)) return null;
|
|
70
|
+
return scope;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function tablesIn(section) {
|
|
74
|
+
const tables = [];
|
|
75
|
+
let header = null;
|
|
76
|
+
for (const line of section.split('\n')) {
|
|
77
|
+
if (!line.trim().startsWith('|')) {
|
|
78
|
+
header = null;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
82
|
+
if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;
|
|
83
|
+
if (!header) {
|
|
84
|
+
header = cells;
|
|
85
|
+
tables.push({ header, rows: [] });
|
|
86
|
+
} else {
|
|
87
|
+
tables[tables.length - 1].rows.push(cells);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return tables;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cellOf(header, row, name) {
|
|
94
|
+
const index = header.findIndex((item) => item.includes(name));
|
|
95
|
+
if (index < 0) return null;
|
|
96
|
+
return (row[index] ?? '').trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fill(template, vars) {
|
|
100
|
+
return String(template ?? '未通过').replace(/\{([^}]+)\}/g, (_, key) => vars[key] ?? '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function headings(section, level) {
|
|
104
|
+
const mark = '#'.repeat(level);
|
|
105
|
+
return section.split(new RegExp(`^${mark} `, 'm')).slice(1).map((block) => ({
|
|
106
|
+
title: block.split('\n')[0].trim(),
|
|
107
|
+
body: block,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasTableAfter(block, label) {
|
|
112
|
+
const at = block.indexOf(label);
|
|
113
|
+
if (at === -1) return false;
|
|
114
|
+
return /^\|.*\|/m.test(block.slice(at, at + 1200));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function headingMeta(title) {
|
|
118
|
+
const id = /^(\d+\.\d+)/.exec(title)?.[1] ?? '';
|
|
119
|
+
return { title, id, name: title.replace(/^\d+\.\d+\s*/, '').trim() };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pointsAt(value, head, blanks) {
|
|
123
|
+
if (!value || blanks.includes(value)) return false;
|
|
124
|
+
if (head.id && new RegExp(`(?<![\\d.])${head.id.replace(/\./g, '\\.')}(?!\\d)`).test(value)) return true;
|
|
125
|
+
return Boolean(head.name) && value.includes(head.name);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function checkBlocks(scope, rule, found) {
|
|
129
|
+
const spec = rule.拆块;
|
|
130
|
+
const blocks = headings(scope, spec.级别 ?? 4);
|
|
131
|
+
if (blocks.length === 0) {
|
|
132
|
+
found.push(fill(spec.没有块说明 || rule.说明, {}));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const block of blocks) {
|
|
136
|
+
const skip = spec.跳过当;
|
|
137
|
+
const head = `${block.title}\n${block.body.slice(0, 200)}`;
|
|
138
|
+
if (skip && head.includes(skip.含) && !block.body.includes(skip.且不含)) continue;
|
|
139
|
+
for (const word of spec.词后要有表 ?? []) {
|
|
140
|
+
if (!hasTableAfter(block.body, word)) {
|
|
141
|
+
found.push(fill(rule.说明, { 标题: block.title, 词: word }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function checkHeadings(scope, rule, found) {
|
|
148
|
+
const spec = rule.每个标题;
|
|
149
|
+
const blocks = headings(scope, spec.级别 ?? 3);
|
|
150
|
+
if (blocks.length === 0 && spec.没有标题则失败) {
|
|
151
|
+
found.push(fill(rule.说明, {}));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const block of blocks) {
|
|
155
|
+
if (spec.正文匹配则失败 && new RegExp(spec.正文匹配则失败, 'm').test(block.body)) {
|
|
156
|
+
found.push(fill(rule.说明, { 标题: block.title }));
|
|
157
|
+
}
|
|
158
|
+
const tableSpec = spec.要有表;
|
|
159
|
+
if (!tableSpec) continue;
|
|
160
|
+
const needed = tableSpec.表头含 ?? [];
|
|
161
|
+
const table = tablesIn(block.body).find((item) => needed.every((name) => item.header.some((cell) => cell.includes(name))));
|
|
162
|
+
if (!table || table.rows.length === 0) {
|
|
163
|
+
found.push(fill(tableSpec.缺表说明 || rule.说明, { 标题: block.title }));
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
for (const row of table.rows) {
|
|
167
|
+
for (const column of tableSpec.列不能空 ?? []) {
|
|
168
|
+
const value = cellOf(table.header, row, column);
|
|
169
|
+
if (value == null || value === '') {
|
|
170
|
+
found.push(fill(tableSpec.空列说明 || rule.说明, { 标题: block.title, 列: column }));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const [column, pattern] of Object.entries(tableSpec.列不能整格匹配 ?? {})) {
|
|
174
|
+
const value = cellOf(table.header, row, column) ?? '';
|
|
175
|
+
const source = typeof pattern === 'string' ? pattern : pattern.模式;
|
|
176
|
+
const message = typeof pattern === 'string' ? tableSpec.整格说明 || rule.说明 : pattern.说明 || rule.说明;
|
|
177
|
+
if (new RegExp(source).test(value)) found.push(fill(message, { 标题: block.title, 列: column }));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function leastGate(text, least) {
|
|
184
|
+
if (!least) return false;
|
|
185
|
+
const chapterHit = (least.当有章 ?? []).some((name) => chapter(text, name) != null);
|
|
186
|
+
const textHit = Object.entries(least.或章匹配 ?? {}).some(([name, pattern]) => {
|
|
187
|
+
const body = chapter(text, name);
|
|
188
|
+
return body != null && new RegExp(pattern, 'm').test(body);
|
|
189
|
+
});
|
|
190
|
+
return (least.当有章 || least.或章匹配) ? (chapterHit || textHit) : true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function listTable(text, spec, found, missingMessage) {
|
|
194
|
+
const listChapter = chapter(text, spec.清单章);
|
|
195
|
+
const list = listChapter && subsection(listChapter, spec.清单节);
|
|
196
|
+
if (!list) {
|
|
197
|
+
found.push(fill(missingMessage, {}));
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const needed = spec.表头要含 ?? [];
|
|
201
|
+
const table = tablesIn(list).find((item) => needed.every((name) => item.header.some((cell) => cell.includes(name))));
|
|
202
|
+
if (!table) {
|
|
203
|
+
found.push(fill(spec.缺表说明 || missingMessage, {}));
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return table;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function checkLeast(text, table, least, blanks, found) {
|
|
210
|
+
if (!leastGate(text, least)) return;
|
|
211
|
+
const rows = table.rows.filter((row) => row.some((cell) => cell && !blanks.includes(cell)));
|
|
212
|
+
const values = rows.map((row) => cellOf(table.header, row, least.列) ?? '');
|
|
213
|
+
least.模式.forEach((pattern, index) => {
|
|
214
|
+
const source = typeof pattern === 'string' ? pattern : pattern.正则;
|
|
215
|
+
const needChapters = typeof pattern === 'string' ? null : pattern.当有章;
|
|
216
|
+
if (Array.isArray(needChapters) && !needChapters.some((name) => chapter(text, name) != null)) return;
|
|
217
|
+
if (!values.some((value) => new RegExp(source, 'i').test(value))) {
|
|
218
|
+
const message = typeof pattern === 'object' && pattern.说明
|
|
219
|
+
? pattern.说明
|
|
220
|
+
: (Array.isArray(least.说明) ? least.说明[index] : least.说明);
|
|
221
|
+
found.push(fill(message, { 模式: source }));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function checkRows(text, spec, found) {
|
|
227
|
+
const pages = chapter(text, spec.页面章);
|
|
228
|
+
const least = spec.至少各有一行;
|
|
229
|
+
const leastOn = leastGate(text, least);
|
|
230
|
+
if (pages == null && !leastOn) return;
|
|
231
|
+
|
|
232
|
+
const blanks = spec.空值 ?? ['', '无'];
|
|
233
|
+
const table = listTable(
|
|
234
|
+
text,
|
|
235
|
+
spec,
|
|
236
|
+
found,
|
|
237
|
+
pages == null ? (spec.缺表说明 || '改动清单里还没有追溯要用的表') : (spec.说明 || '清单和页面章对不上')
|
|
238
|
+
);
|
|
239
|
+
if (!table) return;
|
|
240
|
+
|
|
241
|
+
if (pages != null) {
|
|
242
|
+
const heads = headings(pages, spec.标题级别 ?? 3).map((block) => headingMeta(block.title));
|
|
243
|
+
const rows = table.rows.filter((row) => row.some((cell) => cell && !blanks.includes(cell)));
|
|
244
|
+
|
|
245
|
+
for (const rowRule of spec.行 ?? []) {
|
|
246
|
+
const rowBlanks = rowRule.空值 ?? blanks;
|
|
247
|
+
for (const row of rows) {
|
|
248
|
+
const when = rowRule.当列匹配 ?? {};
|
|
249
|
+
const matched = Object.entries(when).every(([column, pattern]) => new RegExp(pattern, 'i').test(cellOf(table.header, row, column) ?? ''));
|
|
250
|
+
if (!matched) continue;
|
|
251
|
+
if (rowRule.列要有字 && !cellOf(table.header, row, rowRule.列要有字)) continue;
|
|
252
|
+
const file = cellOf(table.header, row, rowRule.列要有字 || '文件') ?? '';
|
|
253
|
+
if (rowRule.列要指向标题) {
|
|
254
|
+
const value = cellOf(table.header, row, rowRule.列要指向标题) ?? '';
|
|
255
|
+
if (!heads.some((head) => pointsAt(value, head, rowBlanks))) {
|
|
256
|
+
found.push(fill(rowRule.说明, { 文件: file, 标题: value }));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const [column, banned] of Object.entries(rowRule.列不能是 ?? {})) {
|
|
260
|
+
const value = cellOf(table.header, row, column) ?? '';
|
|
261
|
+
if (banned.includes(value)) found.push(fill(rowRule.说明, { 文件: file, 列: column }));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const link = spec.每个标题要被指向;
|
|
267
|
+
if (link) {
|
|
268
|
+
for (const head of heads) {
|
|
269
|
+
const linked = rows.some((row) => pointsAt(cellOf(table.header, row, link.列) ?? '', head, blanks));
|
|
270
|
+
if (!linked) found.push(fill(link.说明, { 标题: head.title }));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
checkLeast(text, table, least, blanks, found);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function checkColumns(scope, rule, found) {
|
|
279
|
+
const tables = tablesIn(scope).filter((item) => rule.列不能空.every((name) => item.header.some((cell) => cell.includes(name))));
|
|
280
|
+
if (tables.length === 0) {
|
|
281
|
+
if (rule.缺列说明) {
|
|
282
|
+
for (const column of rule.列不能空) found.push(fill(rule.缺列说明, { 列: column }));
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
for (const table of tables) {
|
|
287
|
+
for (const row of table.rows) {
|
|
288
|
+
if (!row.some((cell) => cell)) continue;
|
|
289
|
+
for (const column of rule.列不能空) {
|
|
290
|
+
if (!(cellOf(table.header, row, column) ?? '')) {
|
|
291
|
+
found.push(fill(rule.说明, { 列: column }));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function apply(source, rule, found) {
|
|
299
|
+
if (Array.isArray(rule.仅当有章) && !rule.仅当有章.some((name) => chapter(source, name) != null)) return;
|
|
300
|
+
if (rule.对账) {
|
|
301
|
+
checkRows(source, { ...rule.对账, 说明: rule.对账.说明 || rule.说明 }, found);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const scope = scopeOf(source, rule);
|
|
305
|
+
if (scope == null) return;
|
|
306
|
+
if (rule.匹配则失败 && new RegExp(rule.匹配则失败, 'm').test(scope)) found.push(fill(rule.说明, {}));
|
|
307
|
+
if (rule.要匹配 && !new RegExp(rule.要匹配, 'm').test(scope)) found.push(fill(rule.说明, {}));
|
|
308
|
+
if (rule.表头要含) {
|
|
309
|
+
const ok = tablesIn(scope).some((item) => rule.表头要含.every((name) => item.header.some((cell) => cell.includes(name))));
|
|
310
|
+
if (!ok) found.push(fill(rule.说明, {}));
|
|
311
|
+
}
|
|
312
|
+
if (rule.列不能空) checkColumns(scope, rule, found);
|
|
313
|
+
if (rule.拆块) checkBlocks(scope, rule, found);
|
|
314
|
+
if (rule.每个标题) checkHeadings(scope, rule, found);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const must = [];
|
|
318
|
+
const hints = [];
|
|
319
|
+
for (const rule of readRules('护栏.md')) apply(text, rule, must);
|
|
320
|
+
for (const rule of readRules('约束.md')) apply(text, rule, hints);
|
|
321
|
+
|
|
322
|
+
if (hints.length > 0) {
|
|
323
|
+
console.log(`尽量未做到 ${hints.length} 项:`);
|
|
324
|
+
for (const item of hints) console.log(`- ${item}`);
|
|
325
|
+
}
|
|
326
|
+
if (must.length > 0) {
|
|
327
|
+
console.error(`未通过 ${must.length} 项:`);
|
|
328
|
+
for (const item of must) console.error(`- ${item}`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
console.log('通过');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 护栏
|
|
2
|
+
|
|
3
|
+
护栏挂在动作上,不靠回头翻清单。落盘前运行 `scripts/check-requirements.mjs`,脚本退出码不是 0,就不能交。必须项只在这一份。`references/约束.md` 只是尽量项。
|
|
4
|
+
|
|
5
|
+
## 写每节时查
|
|
6
|
+
|
|
7
|
+
1. 四个二级标题只用模板那四个,顺序不换,不加别的二级标题。脚本查。
|
|
8
|
+
2. 需求背景只写现在谁办不到什么,不超过五句。这次打算怎么改不写进来。说不清时写「现状还没单独说明」,并在需求待定里留一行。句数和「谁看见什么」人看,脚本不查。
|
|
9
|
+
3. 需求描述写了动哪一块、大致做成什么样、旁边这次不动的,不超过五句。句数人看,脚本不查。
|
|
10
|
+
4. 每个功能点动词开头,写了做成什么样、谁看见什么。一点不超过三句,单句不超过 40 个字。写不出的放需求待定,不编结果。动词、句数、字数人看,脚本不查。
|
|
11
|
+
5. 功能点只写人能感知的事。文风里禁止的词不出现。脚本只查围栏和「」外面的禁词。
|
|
12
|
+
|
|
13
|
+
## 遇到这些,按这个走
|
|
14
|
+
|
|
15
|
+
| 遇到什么 | 怎么办 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 深思记录里有未决 | 写进需求待定,不写成已经说清,不替人拍板 |
|
|
18
|
+
| 深思记录里已定的结论 | 写进背景、描述或功能点,不重问 |
|
|
19
|
+
| 功能点缺口会改做法,但还没定 | 写进需求待定,把这份文档写完。不停下来重问 |
|
|
20
|
+
| 返回的 template 说明和文风不一致 | 以文风和需求待定为准 |
|
|
21
|
+
|
|
22
|
+
## 落盘前查
|
|
23
|
+
|
|
24
|
+
1. 跑 `node scripts/check-requirements.mjs <需求文档.md>`,退出码是 0。
|
|
25
|
+
2. 需求待定没有项时写了「无」。
|
|
26
|
+
3. 对话只报路径和需求待定里还要人拍板的条目。不说「接下来写设计」。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 需求文风
|
|
2
|
+
|
|
3
|
+
正文必须是简体中文、白话。四个二级标题按这个顺序,不要多也不要少:需求背景、需求描述、需求功能点拆分、需求待定。
|
|
4
|
+
|
|
5
|
+
会改功能点但还没定的,写进需求待定,把这份文档写完。不要停下来重问,也不要编一句结果填进功能点。
|
|
6
|
+
|
|
7
|
+
需求背景只写现状:现在谁办不到什么。不超过五句。不写目标口号,不写这次打算怎么改。说不清就写「现状还没单独说明」,并在需求待定里留一行。
|
|
8
|
+
|
|
9
|
+
需求描述写三件事:这次动哪一块,大致做成什么样,旁边这次不动的。不超过五句。不动的相邻块写在这里。
|
|
10
|
+
|
|
11
|
+
功能点用动词开头。对得上的就用:新增、修改、查询、删除、启用、停用。一次通知、一次导入也各算一点。一点只写一件用户能感知的事,并写做成什么样、谁看见什么。一个功能点不超过三句。单句不超过 40 个字。
|
|
12
|
+
|
|
13
|
+
校验、落库、接口路径、类名、表名、错误码不写进功能点。
|
|
14
|
+
|
|
15
|
+
某个功能点需要给人看页面或交接时,把简图直接放在该功能点下面。第一行写「草图(附注,不作为依据)」。没有图就不要留空图。句子和图不一致时,以该功能点的句子为准。没有页面就不要画按钮。
|
|
16
|
+
|
|
17
|
+
几个系统之间交接时,用功能点写谁交给谁、交完看见什么,最多六行。配置、开关、定时、修数据,写谁触发、跑完怎样、不跑时怎样。
|
|
18
|
+
|
|
19
|
+
禁止:优化、提升、赋能、闭环、拉通、大概、可能、后续再看、给定-当-则。不要把对话过程写进正文。专业词第一次出现,后面补半句白话。
|
|
20
|
+
|
|
21
|
+
能量化的写数字。不能量化又会改变功能点的,放进需求待定,不要编一个数。
|
|
22
|
+
|
|
23
|
+
「看见什么」必须来自已经说清的话。还写不出结果,就放进需求待定,不要在落文档时补一句。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 有深思记录时怎么写
|
|
2
|
+
|
|
3
|
+
变更目录里如果有 `深思/registry.json`,先读它,也读 `深思/记录.md`。不要把整份 JSON 贴进对话。没有这些文件也可以写需求。
|
|
4
|
+
|
|
5
|
+
若只有上一版的 `追问/`,把它当作 `深思/` 来读。
|
|
6
|
+
|
|
7
|
+
已定、沿用材料:写进需求描述或对应功能点,不要再问。`记录.md` 的答案是材料,按四个标题重写,不要整篇搬过去。
|
|
8
|
+
|
|
9
|
+
未决:写进需求待定。不要重问,也不要在正文里猜成某一种。
|
|
10
|
+
|
|
11
|
+
页面不适用时,不要画草图,也不要单开一节。
|
|
12
|
+
|
|
13
|
+
对照:
|
|
14
|
+
|
|
15
|
+
- 现状和问题写进需求背景,只写已经说清的事实
|
|
16
|
+
- 动哪一块、这次不动的相邻块写进需求描述
|
|
17
|
+
- 要做的每一件事写成一条功能点,动词开头,并写看见什么
|
|
18
|
+
- 会改功能点但还没定的,只放需求待定
|
|
19
|
+
- 三方核对已定的页面、表、接口,按结论写进对应功能点。对不上且还没选定的,放需求待定,不要把原句照抄成已经说清
|
|
20
|
+
|
|
21
|
+
功能点里的「看见什么」必须来自已经说清的话。记录里没有结果,就留在需求待定,不要补一句。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 需求待定
|
|
2
|
+
|
|
3
|
+
这一节不能删。没有项时只写:无。
|
|
4
|
+
|
|
5
|
+
写需求时,会改功能点但还没定的放在这里,然后把文档写完。不要为了这些项停下来问,也不要在功能点里猜一个结果来躲开这一节。
|
|
6
|
+
|
|
7
|
+
只写会改功能点、还没接上的事。例如:有启用没有停用,失败时看见什么还没定,两个功能点对不上,历史数据要不要改还没定。
|
|
8
|
+
|
|
9
|
+
库表、接口路径、错误码、字段怎么摆,不写在这一节,留给设计。
|
|
10
|
+
|
|
11
|
+
写设计、写任务、写代码时读到这些项,停下来问,不要补。
|
|
12
|
+
|
|
13
|
+
用户确认之后,把结论写进需求描述或对应功能点,删掉这一行。
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 核对需求文档的结构。句子长短和「谁看见什么」仍由 references/护栏.md 人工对。
|
|
4
|
+
* 用法:node check-requirements.mjs <需求文档.md>
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const docPath = process.argv[2];
|
|
9
|
+
if (!docPath) {
|
|
10
|
+
console.error('用法:node check-requirements.mjs <需求文档.md>');
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const text = fs.readFileSync(docPath, 'utf8');
|
|
15
|
+
const prose = text.replace(/```[\s\S]*?```/g, '').replace(/「[^」]*」/g, '');
|
|
16
|
+
const errors = [];
|
|
17
|
+
const expected = ['需求背景', '需求描述', '需求功能点拆分', '需求待定'];
|
|
18
|
+
const headings = [...text.matchAll(/^##\s+(.+)$/gm)].map((match) => match[1].trim());
|
|
19
|
+
|
|
20
|
+
if (headings.join('\n') !== expected.join('\n')) {
|
|
21
|
+
const now = headings.length ? headings.join('、') : '(没有)';
|
|
22
|
+
errors.push(`二级标题必须按顺序只有这四个:${expected.join('、')}。现在是:${now}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function section(name) {
|
|
26
|
+
const start = text.search(new RegExp(`^##\\s+${name}\\s*$`, 'm'));
|
|
27
|
+
if (start < 0) return '';
|
|
28
|
+
const rest = text.slice(start).split('\n').slice(1).join('\n');
|
|
29
|
+
const next = rest.search(/^##\s+/m);
|
|
30
|
+
return (next < 0 ? rest : rest.slice(0, next)).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (headings.includes('需求待定') && !section('需求待定')) {
|
|
34
|
+
errors.push('需求待定是空的。没有项就写「无」。');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const points = section('需求功能点拆分');
|
|
38
|
+
if (headings.includes('需求功能点拆分') && !/^\s*\d+[.、.]/m.test(points)) {
|
|
39
|
+
errors.push('功能点拆分里没有编号的功能点。不要用一段话代替。');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const banned = ['本变更不涉及', 'SHALL', '赋能', '闭环', '拉通', '后续再看', '给定-当-则'];
|
|
43
|
+
for (const word of banned) {
|
|
44
|
+
if (prose.includes(word)) errors.push(`正文里有不能出现的「${word}」。`);
|
|
45
|
+
}
|
|
46
|
+
if (/\bGiven\b|\bWhen\b|\bThen\b/.test(prose)) {
|
|
47
|
+
errors.push('正文里有 Given、When 或 Then。');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const leftovers = ['(谁)', '(旁边哪些)', '(一件事)', '(什么)', '(什么样)', '(哪里)', '(现在谁办不到什么'];
|
|
51
|
+
for (const token of leftovers) {
|
|
52
|
+
if (text.includes(token)) errors.push(`模板提示还在:${token}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (errors.length) {
|
|
56
|
+
for (const error of errors) console.error(error);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('通过');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 护栏
|
|
2
|
+
|
|
3
|
+
护栏挂在动作上,不靠回头翻清单。必须项只在这一份。`references/约束.md` 只是尽量项。
|
|
4
|
+
|
|
5
|
+
## 开工前查
|
|
6
|
+
|
|
7
|
+
1. 只读。缺的文件点名,不当成失败,也不当成已经覆盖。
|
|
8
|
+
2. 核对对象想清楚:审Java 只评代码质量,审文档只评文档质量,验收对照两边。这次问的是哪一种,就做哪一种。
|
|
9
|
+
|
|
10
|
+
## 核对时查
|
|
11
|
+
|
|
12
|
+
1. `references/核对.md` 里的三块都写了:做完的、缺证据的、和文档不一致的。
|
|
13
|
+
2. 任务勾了但代码里找不到改动的,不算做完。
|
|
14
|
+
3. 实现和设计里写明的做法、接口、表或状态相反,才算不一致。设计没写的不算。没有设计就写「没有设计可对照」。
|
|
15
|
+
4. 每条问题写清文件,能定位到行的写行,并写可以怎么改。没有证据不升成严重。
|
|
16
|
+
|
|
17
|
+
## 报结论时查
|
|
18
|
+
|
|
19
|
+
结论只用:通过、有条件通过、不通过。
|
|
20
|
+
|
|
21
|
+
- 不通过:实现和文档里写明的做法、接口、表或状态相反,或者任务勾了但代码里没有改动。
|
|
22
|
+
- 有条件通过:没有和文档相反,但有缺证据。
|
|
23
|
+
- 通过:已有文档都能对上,也没有缺证据。
|
|
24
|
+
|
|
25
|
+
缺了某份文档,点名即可,单独不构成不通过。
|
|
26
|
+
|
|
27
|
+
结论摘要 3 到 6 句,非研发读完能懂风险。先说人话,不贴文档全文,不说「接下来改代码」「接下来归档」。
|