@gordon.gan/specflow 1.4.2-beta → 1.4.4-beta

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/README.md CHANGED
@@ -126,7 +126,7 @@ npm install -g @gordon.gan/specflow
126
126
  npm install -g github:Gordon-Gan-Jiang/specflow
127
127
 
128
128
  # 验证
129
- specflow --version # 以 npm / package.json 为准(当前 1.4.2-beta)
129
+ specflow --version # 以 npm / package.json 为准(当前 1.4.4-beta)
130
130
  specflow --help
131
131
  ```
132
132
 
@@ -6,6 +6,9 @@ export interface NormalizedReference {
6
6
  readonly id: string;
7
7
  readonly remote?: string;
8
8
  }
9
+ export declare const CONVENTION_TOPICS: readonly ["architecture", "api", "database", "frontend"];
10
+ export type ConventionTopic = (typeof CONVENTION_TOPICS)[number];
11
+ export type ProjectConventions = Readonly<Partial<Record<ConventionTopic, readonly string[]>>>;
9
12
  export interface ParsedProjectConfig {
10
13
  readonly schema: string;
11
14
  readonly context?: string;
@@ -14,6 +17,7 @@ export interface ParsedProjectConfig {
14
17
  readonly workflowUpstream?: string;
15
18
  readonly store?: string;
16
19
  readonly references: readonly NormalizedReference[];
20
+ readonly conventions: ProjectConventions;
17
21
  readonly diagnostics: readonly Diagnostic[];
18
22
  }
19
23
  export declare function parseProjectConfig(raw: unknown): ParsedProjectConfig;
@@ -12,6 +12,12 @@ const ReferenceObjectSchema = z
12
12
  remote: z.string().url().optional(),
13
13
  })
14
14
  .strict();
15
+ export const CONVENTION_TOPICS = [
16
+ 'architecture',
17
+ 'api',
18
+ 'database',
19
+ 'frontend',
20
+ ];
15
21
  function referenceDiagnostic(message, target) {
16
22
  return {
17
23
  severity: 'warning',
@@ -155,6 +161,48 @@ export function parseProjectConfig(raw) {
155
161
  fix: `Add '${workflowUpstream}' to specflow/config.yaml references.`,
156
162
  });
157
163
  }
164
+ const conventions = {};
165
+ if (record.conventions !== undefined) {
166
+ const conventionsRaw = typeof record.conventions === 'object' &&
167
+ record.conventions !== null &&
168
+ !Array.isArray(record.conventions)
169
+ ? record.conventions
170
+ : undefined;
171
+ if (!conventionsRaw) {
172
+ diagnostics.push({
173
+ severity: 'error',
174
+ code: 'invalid_conventions_config',
175
+ message: 'Project conventions must be an object of topic → path arrays.',
176
+ target: 'config.conventions',
177
+ fix: 'Use conventions.architecture|api|database|frontend: [relative/path.md].',
178
+ });
179
+ }
180
+ else {
181
+ for (const topic of CONVENTION_TOPICS) {
182
+ const value = conventionsRaw[topic];
183
+ if (value === undefined) {
184
+ continue;
185
+ }
186
+ if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
187
+ diagnostics.push({
188
+ severity: 'error',
189
+ code: 'invalid_conventions_topic',
190
+ message: `conventions.${topic} must be an array of relative file path strings.`,
191
+ target: `config.conventions.${topic}`,
192
+ fix: `Set conventions.${topic} to e.g. ['docs/engineering/${topic}.md'].`,
193
+ });
194
+ continue;
195
+ }
196
+ const paths = value
197
+ .map((item) => item.trim())
198
+ .filter((item) => item.length > 0)
199
+ .slice(0, 3);
200
+ if (paths.length > 0) {
201
+ conventions[topic] = paths;
202
+ }
203
+ }
204
+ }
205
+ }
158
206
  return {
159
207
  schema,
160
208
  context,
@@ -163,6 +211,7 @@ export function parseProjectConfig(raw) {
163
211
  workflowUpstream,
164
212
  store,
165
213
  references,
214
+ conventions,
166
215
  diagnostics,
167
216
  };
168
217
  }
@@ -0,0 +1,15 @@
1
+ import { type ConventionTopic, type ProjectConventions } from './project-config.js';
2
+ export type IdeFlavor = 'cursor' | 'claude' | 'codex';
3
+ /**
4
+ * Resolves project convention files for an approval topic.
5
+ * Priority: config conventions.<topic> → repo-neutral docs.
6
+ * IDE-specific rule/skill globs are documented for the agent to scan; this helper
7
+ * covers the deterministic, config + neutral paths used in tests and tooling.
8
+ */
9
+ export declare function resolveProjectConventionPaths(input: {
10
+ readonly projectRoot: string;
11
+ readonly topic: ConventionTopic;
12
+ readonly conventions?: ProjectConventions;
13
+ }): readonly string[];
14
+ export declare function listConventionTopics(): readonly ConventionTopic[];
15
+ export declare function neutralCandidatesFor(topic: ConventionTopic): readonly string[];
@@ -0,0 +1,66 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { CONVENTION_TOPICS, } from './project-config.js';
4
+ const NEUTRAL_CANDIDATES = {
5
+ architecture: [
6
+ 'docs/engineering/architecture.md',
7
+ '.specflow/conventions/architecture.md',
8
+ 'ARCHITECTURE.md',
9
+ ],
10
+ api: [
11
+ 'docs/api/guidelines.md',
12
+ 'docs/engineering/api.md',
13
+ '.specflow/conventions/api.md',
14
+ ],
15
+ database: [
16
+ 'docs/db/conventions.md',
17
+ 'docs/engineering/database.md',
18
+ '.specflow/conventions/database.md',
19
+ ],
20
+ frontend: [
21
+ 'docs/frontend/conventions.md',
22
+ 'docs/frontend/patterns.md',
23
+ 'docs/frontend/testing.md',
24
+ 'docs/engineering/frontend.md',
25
+ '.specflow/conventions/frontend.md',
26
+ 'agent_docs/tech_stack.md',
27
+ 'agent_docs/code_patterns.md',
28
+ 'agent_docs/testing.md',
29
+ ],
30
+ };
31
+ const MAX_FILES_PER_TOPIC = 3;
32
+ function existingRelative(projectRoot, relativePaths) {
33
+ const found = [];
34
+ for (const relative of relativePaths) {
35
+ const normalized = relative.replace(/^\.\//, '').replace(/\\/g, '/');
36
+ if (!normalized || normalized.includes('..')) {
37
+ continue;
38
+ }
39
+ if (existsSync(join(projectRoot, ...normalized.split('/')))) {
40
+ found.push(normalized);
41
+ }
42
+ if (found.length >= MAX_FILES_PER_TOPIC) {
43
+ break;
44
+ }
45
+ }
46
+ return found;
47
+ }
48
+ /**
49
+ * Resolves project convention files for an approval topic.
50
+ * Priority: config conventions.<topic> → repo-neutral docs.
51
+ * IDE-specific rule/skill globs are documented for the agent to scan; this helper
52
+ * covers the deterministic, config + neutral paths used in tests and tooling.
53
+ */
54
+ export function resolveProjectConventionPaths(input) {
55
+ const configured = input.conventions?.[input.topic];
56
+ if (configured && configured.length > 0) {
57
+ return existingRelative(input.projectRoot, configured.slice(0, MAX_FILES_PER_TOPIC));
58
+ }
59
+ return existingRelative(input.projectRoot, NEUTRAL_CANDIDATES[input.topic]);
60
+ }
61
+ export function listConventionTopics() {
62
+ return CONVENTION_TOPICS;
63
+ }
64
+ export function neutralCandidatesFor(topic) {
65
+ return NEUTRAL_CANDIDATES[topic];
66
+ }
@@ -69,6 +69,7 @@ async function inspectClaude(projectRoot) {
69
69
  const promptsDir = join(projectRoot, '.claude', 'specflow', 'prompts');
70
70
  const schemasDir = join(projectRoot, '.claude', 'specflow', 'schemas');
71
71
  const templatesDir = join(projectRoot, '.claude', 'specflow', 'templates');
72
+ const guidanceDir = join(projectRoot, '.claude', 'specflow', 'guidance');
72
73
  const supported = [];
73
74
  const skillContentsByCommand = {};
74
75
  for (const command of COMMAND_CATALOG) {
@@ -98,6 +99,7 @@ async function inspectClaude(projectRoot) {
98
99
  runtimePromptsDir: { exists: await directoryExists(promptsDir), hash: await hashDirectoryTree(promptsDir) },
99
100
  runtimeSchemasDir: { exists: await directoryExists(schemasDir), hash: await hashDirectoryTree(schemasDir) },
100
101
  runtimeTemplatesDir: { exists: await directoryExists(templatesDir), hash: await hashDirectoryTree(templatesDir) },
102
+ runtimeGuidanceDir: { exists: await directoryExists(guidanceDir), hash: await hashDirectoryTree(guidanceDir) },
101
103
  },
102
104
  };
103
105
  }
@@ -40,6 +40,7 @@ function renderCodexAgentsBlock() {
40
40
  '- Use artifacts under specflow/changes/<change>/ before coding',
41
41
  '- Workflow skills: $specflow-explore, $specflow-propose, $specflow-refine, $specflow-apply, $specflow-review, $specflow-test, $specflow-verify, $specflow-archive, $specflow-fix, $specflow-snap',
42
42
  '- Runtime prompts live under .agents/specflow/prompts/',
43
+ '- Guidance packs (non-IDE skills) live under .agents/specflow/guidance/',
43
44
  '',
44
45
  ].join('\n');
45
46
  }
@@ -96,6 +97,7 @@ async function inspectCodex(projectRoot) {
96
97
  const promptsDir = join(projectRoot, '.agents', 'specflow', 'prompts');
97
98
  const schemasDir = join(projectRoot, '.agents', 'specflow', 'schemas');
98
99
  const templatesDir = join(projectRoot, '.agents', 'specflow', 'templates');
100
+ const guidanceDir = join(projectRoot, '.agents', 'specflow', 'guidance');
99
101
  let agentsFileExists = false;
100
102
  try {
101
103
  const agentsContent = await fs.readFile(agentsFile, 'utf-8');
@@ -134,6 +136,7 @@ async function inspectCodex(projectRoot) {
134
136
  runtimePromptsDir: { exists: await directoryExists(promptsDir), hash: await hashDirectoryTree(promptsDir) },
135
137
  runtimeSchemasDir: { exists: await directoryExists(schemasDir), hash: await hashDirectoryTree(schemasDir) },
136
138
  runtimeTemplatesDir: { exists: await directoryExists(templatesDir), hash: await hashDirectoryTree(templatesDir) },
139
+ runtimeGuidanceDir: { exists: await directoryExists(guidanceDir), hash: await hashDirectoryTree(guidanceDir) },
137
140
  },
138
141
  };
139
142
  }
@@ -89,6 +89,7 @@ async function inspectCursor(projectRoot) {
89
89
  const promptsDir = join(projectRoot, '.cursor', 'specflow', 'prompts');
90
90
  const schemasDir = join(projectRoot, '.cursor', 'specflow', 'schemas');
91
91
  const templatesDir = join(projectRoot, '.cursor', 'specflow', 'templates');
92
+ const guidanceDir = join(projectRoot, '.cursor', 'specflow', 'guidance');
92
93
  const supported = [];
93
94
  const skillContentsByCommand = {};
94
95
  for (const command of COMMAND_CATALOG) {
@@ -119,6 +120,7 @@ async function inspectCursor(projectRoot) {
119
120
  runtimePromptsDir: { exists: await directoryExists(promptsDir), hash: await hashDirectoryTree(promptsDir) },
120
121
  runtimeSchemasDir: { exists: await directoryExists(schemasDir), hash: await hashDirectoryTree(schemasDir) },
121
122
  runtimeTemplatesDir: { exists: await directoryExists(templatesDir), hash: await hashDirectoryTree(templatesDir) },
123
+ runtimeGuidanceDir: { exists: await directoryExists(guidanceDir), hash: await hashDirectoryTree(guidanceDir) },
122
124
  },
123
125
  };
124
126
  }
@@ -4,6 +4,7 @@ import { CAPABILITY_MANIFEST, IDE_ASSET_MANIFEST } from './parity-manifest.js';
4
4
  const PARITY_COMPARABLE_ASSET_KEYS = [
5
5
  'runtimeSchemasDir',
6
6
  'runtimeTemplatesDir',
7
+ 'runtimeGuidanceDir',
7
8
  ];
8
9
  export function validateReportAgainstManifest(report) {
9
10
  const deltas = [];
@@ -42,6 +42,7 @@ export const IDE_ASSET_MANIFEST = [
42
42
  'runtimePromptsDir',
43
43
  'runtimeSchemasDir',
44
44
  'runtimeTemplatesDir',
45
+ 'runtimeGuidanceDir',
45
46
  ],
46
47
  },
47
48
  {
@@ -53,6 +54,7 @@ export const IDE_ASSET_MANIFEST = [
53
54
  'runtimePromptsDir',
54
55
  'runtimeSchemasDir',
55
56
  'runtimeTemplatesDir',
57
+ 'runtimeGuidanceDir',
56
58
  ],
57
59
  },
58
60
  {
@@ -63,6 +65,7 @@ export const IDE_ASSET_MANIFEST = [
63
65
  'runtimePromptsDir',
64
66
  'runtimeSchemasDir',
65
67
  'runtimeTemplatesDir',
68
+ 'runtimeGuidanceDir',
66
69
  ],
67
70
  },
68
71
  ];
@@ -1,2 +1,15 @@
1
1
  import type { IdeTarget } from './types.js';
2
+ export declare function ideRootDir(ide: IdeTarget): string;
3
+ interface GuidancePackSpec {
4
+ readonly id: string;
5
+ readonly source: string;
6
+ readonly installAs: string;
7
+ readonly consumers?: readonly string[];
8
+ }
9
+ /**
10
+ * Loads guidance pack registry from skills/guidance-packs.yaml.
11
+ * Missing or empty registry → no packs (non-fatal).
12
+ */
13
+ export declare function loadGuidancePacks(packageRoot: string): Promise<readonly GuidancePackSpec[]>;
2
14
  export declare function copyRuntimeAssets(packageRoot: string, projectRoot: string, ide: IdeTarget): Promise<readonly string[]>;
15
+ export {};
@@ -1,7 +1,9 @@
1
+ import { promises as fs } from 'node:fs';
1
2
  import { join } from 'node:path';
3
+ import yaml from 'js-yaml';
2
4
  import { copyDirRecursive, copyDirRecursiveRendered } from './asset-copy.js';
3
5
  import { renderIdeContent } from './skill-renderer.js';
4
- function ideRootDir(ide) {
6
+ export function ideRootDir(ide) {
5
7
  if (ide === 'claude') {
6
8
  return '.claude';
7
9
  }
@@ -10,12 +12,48 @@ function ideRootDir(ide) {
10
12
  }
11
13
  return '.agents';
12
14
  }
15
+ /**
16
+ * Loads guidance pack registry from skills/guidance-packs.yaml.
17
+ * Missing or empty registry → no packs (non-fatal).
18
+ */
19
+ export async function loadGuidancePacks(packageRoot) {
20
+ const registryPath = join(packageRoot, 'skills', 'guidance-packs.yaml');
21
+ let content;
22
+ try {
23
+ content = await fs.readFile(registryPath, 'utf-8');
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ const parsed = yaml.load(content);
29
+ if (!parsed || !Array.isArray(parsed.packs)) {
30
+ return [];
31
+ }
32
+ return parsed.packs.filter((pack) => typeof pack?.id === 'string' &&
33
+ typeof pack?.source === 'string' &&
34
+ typeof pack?.installAs === 'string');
35
+ }
36
+ async function copyGuidancePacks(packageRoot, specflowRoot) {
37
+ const packs = await loadGuidancePacks(packageRoot);
38
+ for (const pack of packs) {
39
+ const src = join(packageRoot, ...pack.source.split('/'));
40
+ const dest = join(specflowRoot, ...pack.installAs.split('/'));
41
+ await copyDirRecursive(src, dest);
42
+ }
43
+ }
13
44
  export async function copyRuntimeAssets(packageRoot, projectRoot, ide) {
14
- const root = join(projectRoot, ideRootDir(ide), 'specflow');
45
+ const ideRoot = ideRootDir(ide);
46
+ const root = join(projectRoot, ideRoot, 'specflow');
15
47
  const promptsSrc = join(packageRoot, 'prompts');
16
48
  const promptsDest = join(root, 'prompts');
17
49
  await copyDirRecursiveRendered(promptsSrc, promptsDest, (content) => renderIdeContent(content, ide));
18
50
  await copyDirRecursive(join(packageRoot, 'schemas'), join(root, 'schemas'));
19
51
  await copyDirRecursive(join(packageRoot, 'templates'), join(root, 'templates'));
20
- return [`${ideRootDir(ide)}/specflow/prompts`, `${ideRootDir(ide)}/specflow/schemas`, `${ideRootDir(ide)}/specflow/templates`];
52
+ await copyGuidancePacks(packageRoot, root);
53
+ return [
54
+ `${ideRoot}/specflow/prompts`,
55
+ `${ideRoot}/specflow/schemas`,
56
+ `${ideRoot}/specflow/templates`,
57
+ `${ideRoot}/specflow/guidance`,
58
+ ];
21
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gordon.gan/specflow",
3
- "version": "1.4.2-beta",
3
+ "version": "1.4.4-beta",
4
4
  "type": "module",
5
5
  "description": "SpecFlow — unified spec-driven development: OpenSpec planning + Superpowers execution in one CLI and cross-IDE workflow",
6
6
  "keywords": [
@@ -1,9 +1,9 @@
1
1
  # Approval · Database Guidance Router
2
2
 
3
- > Used by `/specflow:approval` when generating §4.4 (数据结构 / 数据模型).
4
- > **本地技能**: `skills/database/`(本仓库长期维护)。
5
- > **禁止**依赖远程 skill 仓库(禁止 `npx skills add` / clone / 运行时 fetch)。
6
- > **Not** an MCP tool on hit, `Read` local matched skill files; on miss, LLM-only §4.4.
3
+ > **本地技能**: SpecFlow pack `{ide}/specflow/guidance/database/`( `skills/guidance-packs.yaml`)
4
+ > **项目约定**: 写 §4.4 **之前**还须执行 `prompts/approval/project-conventions-guidance.md`(`topic=database`)。
5
+ > **优先级**: 项目约定 + 现网 DDL **>** 本文件 SpecFlow guidance **>** LLM-fallback。
6
+ > **禁止**依赖远程 skill 仓库; **Not** an MCP tool; 禁止「invoke `/mysql` skill」。
7
7
 
8
8
  ---
9
9
 
@@ -28,18 +28,38 @@ Scan the **project under review** (change's repo root), not SpecFlow itself. Col
28
28
  **Multi-hit**: pick primary OLTP store for §4.4 (prefer `mysql` / `postgresql` / `oracle` over
29
29
  `redis` / `elasticsearch`). Mention secondary stores in §4.7/架构 if relevant.
30
30
 
31
- **Announce** after detection:
31
+ ---
32
+
33
+ ## 2. Resolve guidance root (path priority)
34
+
35
+ Before reading pack files, resolve `guidanceRoot` for stack `<stack>`:
36
+
37
+ | Priority | Path | When |
38
+ |----------|------|------|
39
+ | 1 | `.cursor/specflow/guidance/database/<stack>/` | Cursor IDE assets present |
40
+ | 1 | `.claude/specflow/guidance/database/<stack>/` | Claude IDE assets present |
41
+ | 1 | `.agents/specflow/guidance/database/<stack>/` | Codex IDE assets present |
42
+ | 2 | `skills/database/<stack>/` | SpecFlow package / self-repo fallback (relative to SpecFlow package root) |
43
+ | 3 | *(missing)* | `LLM-fallback` |
44
+
45
+ If multiple IDE roots exist, prefer the IDE the user is running in; otherwise the first existing path among cursor → claude → agents.
46
+
47
+ **Announce** after detection + resolve:
32
48
 
33
49
  ```text
34
50
  DB stack for approval: <mysql|postgresql|oracle|redis|elasticsearch|none>
35
- Guidance: skills/database/<stack>/SKILL.md | LLM-fallback
51
+ Guidance: <.cursor|claude|agents>/specflow/guidance/database/<stack>/SKILL.md
52
+ | skills/database/<stack>/SKILL.md | LLM-fallback
36
53
  ```
37
54
 
38
55
  ---
39
56
 
40
- ## 2. On hit — load **local** guidance (Read, do not invent, do not fetch)
57
+ ## 3. On hit — load **local** guidance (Read, do not invent, do not fetch)
41
58
 
42
- Base: `skills/database/<stack>/`(相对 SpecFlow 仓库根;离线可读)
59
+ > 先完成 `project-conventions-guidance.md` 的 `topic=database` 解析并 Read 项目约定(若有)。
60
+ > 本表仅加载 **SpecFlow** guidance pack,用于补强 DDL 写法;不得覆盖项目禁令。
61
+
62
+ Base = resolved `guidanceRoot` above. From that directory:
43
63
 
44
64
  | Stack | Must Read | Also Read when §4.4 needs it |
45
65
  |-------|-----------|------------------------------|
@@ -49,31 +69,37 @@ Base: `skills/database/<stack>/`(相对 SpecFlow 仓库根;离线可读)
49
69
  | `redis` | `SKILL.md` | Only if change is cache/key design — **not** a substitute for relational DDL |
50
70
  | `elasticsearch` | `SKILL.md` | Only if change is index/mapping — **not** a substitute for relational DDL |
51
71
 
72
+ Point to files **from the router entry** (no reference→reference chains).
73
+
52
74
  Apply guidance to §4.4 output:
53
75
 
54
76
  1. Prefer engine/charset/collation idioms from the local skill (MySQL: InnoDB + utf8mb4…).
55
77
  2. Prefer type/index gotchas from the skill (e.g. DECIMAL for money, no FLOAT amounts).
56
78
  3. Keep SpecFlow hard rules (ER, full CREATE TABLE, 本迭代用法, G3/G4) — skill **supplements**, does not replace.
57
79
 
58
- Cite in §4.4 总则, e.g. `DB 技能: skills/database/mysql (本地)`.
80
+ Cite in §4.4 总则 the **actual** paths used:
81
+ - `项目约定: <path…> | 未发现`
82
+ - `DB 技能: .cursor/specflow/guidance/database/mysql` 或 `skills/database/mysql (package fallback)` 或 `LLM-fallback`
59
83
 
60
84
  ---
61
85
 
62
- ## 3. On miss — LLM fallback
86
+ ## 4. On miss — LLM fallback
63
87
 
64
- If `DB stack = none` (no durable store signals, or only unknown/custom DB):
88
+ If `DB stack = none`, or stack set but **no** guidance root resolves:
65
89
 
66
90
  1. **Do not** force-load mysql/postgresql skills.
67
91
  2. Generate §4.4 with SpecFlow rules only (`generate.md` §4.4 + Quality Gates G3/G4).
68
92
  3. If design/tasks claim a DB but signals are absent → `WARNING` in §8:
69
93
  `design 声称落库但未检测到 DB 栈信号 — 已用 LLM 通用 DDL;建议 refine 标明引擎`.
70
- 4. Pure CLI/config changeskeep `不涉及数据库变更(...)`.
94
+ 4. If stack detected but guidance files missing `WARNING` in §8:
95
+ `dbStack=<stack> 但未找到 guidance/database — 已用 LLM-fallback;建议 specflow init --force-assets`.
96
+ 5. Pure CLI/config changes → keep `不涉及数据库变更(...)`.
71
97
 
72
98
  ---
73
99
 
74
- ## 4. Why not remote skills / MCP tools?
100
+ ## 5. Why not remote skills / MCP / IDE skills?
75
101
 
76
- - **Remote**: approval must work offline and must not drift with upstream installs; maintain
77
- `skills/database/` in this repo.
78
- - **MCP tools**: these packs are markdown guidance, not APIs; routing chooses which local
79
- files to `Read`.
102
+ - **Remote**: approval must work offline; maintain pack in this repo + init sync.
103
+ - **MCP tools**: markdown guidance, not APIs; routing chooses which local files to `Read`.
104
+ - **IDE skills**: packs must **not** appear under `.cursor/skills` / `.claude/skills` / `.agents/skills`
105
+ (noise + false discoverability). They live only under `{ide}/specflow/guidance/`.