@gordon.gan/specflow 1.4.3-beta → 1.4.6-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.
@@ -0,0 +1,244 @@
1
+ import { z } from 'zod';
2
+ import type { ApprovalIndex } from './types.js';
3
+ export declare const approvalIndexSchema: z.ZodObject<{
4
+ schema: z.ZodLiteral<"specflow.approval.index/v1">;
5
+ change: z.ZodString;
6
+ generated_at: z.ZodEffects<z.ZodString, string, unknown>;
7
+ mode: z.ZodDefault<z.ZodEnum<["segmented", "monolithic"]>>;
8
+ ui_in_scope: z.ZodDefault<z.ZodBoolean>;
9
+ db_in_scope: z.ZodDefault<z.ZodBoolean>;
10
+ api_in_scope: z.ZodDefault<z.ZodBoolean>;
11
+ optional: z.ZodDefault<z.ZodObject<{
12
+ s5: z.ZodDefault<z.ZodBoolean>;
13
+ s7: z.ZodDefault<z.ZodBoolean>;
14
+ s8: z.ZodDefault<z.ZodBoolean>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ s5: boolean;
17
+ s7: boolean;
18
+ s8: boolean;
19
+ }, {
20
+ s5?: boolean | undefined;
21
+ s7?: boolean | undefined;
22
+ s8?: boolean | undefined;
23
+ }>>;
24
+ parts_order: z.ZodArray<z.ZodString, "many">;
25
+ design_points: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
26
+ decisions: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
27
+ tables: z.ZodDefault<z.ZodArray<z.ZodObject<{
28
+ id: z.ZodString;
29
+ name: z.ZodString;
30
+ action: z.ZodString;
31
+ part: z.ZodString;
32
+ }, "strip", z.ZodTypeAny, {
33
+ id: string;
34
+ name: string;
35
+ action: string;
36
+ part: string;
37
+ }, {
38
+ id: string;
39
+ name: string;
40
+ action: string;
41
+ part: string;
42
+ }>, "many">>;
43
+ interfaces: z.ZodDefault<z.ZodArray<z.ZodObject<{
44
+ id: z.ZodString;
45
+ short: z.ZodString;
46
+ change: z.ZodString;
47
+ part: z.ZodString;
48
+ }, "strip", z.ZodTypeAny, {
49
+ id: string;
50
+ part: string;
51
+ short: string;
52
+ change: string;
53
+ }, {
54
+ id: string;
55
+ part: string;
56
+ short: string;
57
+ change: string;
58
+ }>, "many">>;
59
+ pages: z.ZodDefault<z.ZodArray<z.ZodObject<{
60
+ id: z.ZodString;
61
+ route: z.ZodString;
62
+ apis: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
63
+ part: z.ZodString;
64
+ }, "strip", z.ZodTypeAny, {
65
+ id: string;
66
+ part: string;
67
+ route: string;
68
+ apis: string[];
69
+ }, {
70
+ id: string;
71
+ part: string;
72
+ route: string;
73
+ apis?: string[] | undefined;
74
+ }>, "many">>;
75
+ capabilities: z.ZodDefault<z.ZodArray<z.ZodObject<{
76
+ id: z.ZodString;
77
+ part: z.ZodString;
78
+ }, "strip", z.ZodTypeAny, {
79
+ id: string;
80
+ part: string;
81
+ }, {
82
+ id: string;
83
+ part: string;
84
+ }>, "many">>;
85
+ batching: z.ZodDefault<z.ZodObject<{
86
+ tables_per_call: z.ZodDefault<z.ZodNumber>;
87
+ interfaces_per_call: z.ZodDefault<z.ZodNumber>;
88
+ pages_per_call: z.ZodDefault<z.ZodNumber>;
89
+ capabilities_per_call: z.ZodDefault<z.ZodNumber>;
90
+ }, "strip", z.ZodTypeAny, {
91
+ tables_per_call: number;
92
+ interfaces_per_call: number;
93
+ pages_per_call: number;
94
+ capabilities_per_call: number;
95
+ }, {
96
+ tables_per_call?: number | undefined;
97
+ interfaces_per_call?: number | undefined;
98
+ pages_per_call?: number | undefined;
99
+ capabilities_per_call?: number | undefined;
100
+ }>>;
101
+ conventions: z.ZodDefault<z.ZodObject<{
102
+ architecture: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
103
+ database: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
104
+ api: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
105
+ frontend: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
106
+ }, "strip", z.ZodTypeAny, {
107
+ architecture?: string[] | undefined;
108
+ api?: string[] | undefined;
109
+ database?: string[] | undefined;
110
+ frontend?: string[] | undefined;
111
+ }, {
112
+ architecture?: string[] | undefined;
113
+ api?: string[] | undefined;
114
+ database?: string[] | undefined;
115
+ frontend?: string[] | undefined;
116
+ }>>;
117
+ meta: z.ZodOptional<z.ZodObject<{
118
+ language: z.ZodOptional<z.ZodString>;
119
+ tech_stack: z.ZodOptional<z.ZodString>;
120
+ project_mode: z.ZodOptional<z.ZodString>;
121
+ }, "strip", z.ZodTypeAny, {
122
+ language?: string | undefined;
123
+ tech_stack?: string | undefined;
124
+ project_mode?: string | undefined;
125
+ }, {
126
+ language?: string | undefined;
127
+ tech_stack?: string | undefined;
128
+ project_mode?: string | undefined;
129
+ }>>;
130
+ }, "strip", z.ZodTypeAny, {
131
+ schema: "specflow.approval.index/v1";
132
+ conventions: {
133
+ architecture?: string[] | undefined;
134
+ api?: string[] | undefined;
135
+ database?: string[] | undefined;
136
+ frontend?: string[] | undefined;
137
+ };
138
+ change: string;
139
+ generated_at: string;
140
+ mode: "segmented" | "monolithic";
141
+ ui_in_scope: boolean;
142
+ db_in_scope: boolean;
143
+ api_in_scope: boolean;
144
+ optional: {
145
+ s5: boolean;
146
+ s7: boolean;
147
+ s8: boolean;
148
+ };
149
+ parts_order: string[];
150
+ design_points: string[];
151
+ decisions: string[];
152
+ tables: {
153
+ id: string;
154
+ name: string;
155
+ action: string;
156
+ part: string;
157
+ }[];
158
+ interfaces: {
159
+ id: string;
160
+ part: string;
161
+ short: string;
162
+ change: string;
163
+ }[];
164
+ pages: {
165
+ id: string;
166
+ part: string;
167
+ route: string;
168
+ apis: string[];
169
+ }[];
170
+ capabilities: {
171
+ id: string;
172
+ part: string;
173
+ }[];
174
+ batching: {
175
+ tables_per_call: number;
176
+ interfaces_per_call: number;
177
+ pages_per_call: number;
178
+ capabilities_per_call: number;
179
+ };
180
+ meta?: {
181
+ language?: string | undefined;
182
+ tech_stack?: string | undefined;
183
+ project_mode?: string | undefined;
184
+ } | undefined;
185
+ }, {
186
+ schema: "specflow.approval.index/v1";
187
+ change: string;
188
+ parts_order: string[];
189
+ conventions?: {
190
+ architecture?: string[] | undefined;
191
+ api?: string[] | undefined;
192
+ database?: string[] | undefined;
193
+ frontend?: string[] | undefined;
194
+ } | undefined;
195
+ generated_at?: unknown;
196
+ mode?: "segmented" | "monolithic" | undefined;
197
+ ui_in_scope?: boolean | undefined;
198
+ db_in_scope?: boolean | undefined;
199
+ api_in_scope?: boolean | undefined;
200
+ optional?: {
201
+ s5?: boolean | undefined;
202
+ s7?: boolean | undefined;
203
+ s8?: boolean | undefined;
204
+ } | undefined;
205
+ design_points?: string[] | undefined;
206
+ decisions?: string[] | undefined;
207
+ tables?: {
208
+ id: string;
209
+ name: string;
210
+ action: string;
211
+ part: string;
212
+ }[] | undefined;
213
+ interfaces?: {
214
+ id: string;
215
+ part: string;
216
+ short: string;
217
+ change: string;
218
+ }[] | undefined;
219
+ pages?: {
220
+ id: string;
221
+ part: string;
222
+ route: string;
223
+ apis?: string[] | undefined;
224
+ }[] | undefined;
225
+ capabilities?: {
226
+ id: string;
227
+ part: string;
228
+ }[] | undefined;
229
+ batching?: {
230
+ tables_per_call?: number | undefined;
231
+ interfaces_per_call?: number | undefined;
232
+ pages_per_call?: number | undefined;
233
+ capabilities_per_call?: number | undefined;
234
+ } | undefined;
235
+ meta?: {
236
+ language?: string | undefined;
237
+ tech_stack?: string | undefined;
238
+ project_mode?: string | undefined;
239
+ } | undefined;
240
+ }>;
241
+ export declare function parseApprovalIndex(raw: unknown): ApprovalIndex;
242
+ /** Returns true when segmented fast-path (monolithic) is allowed. */
243
+ export declare function isLightweightApproval(index: ApprovalIndex): boolean;
244
+ export declare function shouldForceSegmented(index: ApprovalIndex): boolean;
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+ const tableRefSchema = z.object({
3
+ id: z.string().min(1),
4
+ name: z.string().min(1),
5
+ action: z.string().min(1),
6
+ part: z.string().min(1),
7
+ });
8
+ const interfaceRefSchema = z.object({
9
+ id: z.string().min(1),
10
+ short: z.string().min(1),
11
+ change: z.string().min(1),
12
+ part: z.string().min(1),
13
+ });
14
+ const pageRefSchema = z.object({
15
+ id: z.string().min(1),
16
+ route: z.string().min(1),
17
+ apis: z.array(z.string()).default([]),
18
+ part: z.string().min(1),
19
+ });
20
+ const capabilityRefSchema = z.object({
21
+ id: z.string().min(1),
22
+ part: z.string().min(1),
23
+ });
24
+ export const approvalIndexSchema = z.object({
25
+ schema: z.literal('specflow.approval.index/v1'),
26
+ change: z.string().min(1),
27
+ generated_at: z.preprocess((val) => (val instanceof Date ? val.toISOString() : val), z.string().min(1)),
28
+ mode: z.enum(['segmented', 'monolithic']).default('segmented'),
29
+ ui_in_scope: z.boolean().default(false),
30
+ db_in_scope: z.boolean().default(false),
31
+ api_in_scope: z.boolean().default(false),
32
+ optional: z
33
+ .object({
34
+ s5: z.boolean().default(false),
35
+ s7: z.boolean().default(false),
36
+ s8: z.boolean().default(false),
37
+ })
38
+ .default({ s5: false, s7: false, s8: false }),
39
+ parts_order: z.array(z.string().min(1)).min(1),
40
+ design_points: z.array(z.string()).default([]),
41
+ decisions: z.array(z.string()).default([]),
42
+ tables: z.array(tableRefSchema).default([]),
43
+ interfaces: z.array(interfaceRefSchema).default([]),
44
+ pages: z.array(pageRefSchema).default([]),
45
+ capabilities: z.array(capabilityRefSchema).default([]),
46
+ batching: z
47
+ .object({
48
+ tables_per_call: z.number().int().positive().default(3),
49
+ interfaces_per_call: z.number().int().positive().default(3),
50
+ pages_per_call: z.number().int().positive().default(3),
51
+ capabilities_per_call: z.number().int().positive().default(1),
52
+ })
53
+ .default({
54
+ tables_per_call: 3,
55
+ interfaces_per_call: 3,
56
+ pages_per_call: 3,
57
+ capabilities_per_call: 1,
58
+ }),
59
+ conventions: z
60
+ .object({
61
+ architecture: z.array(z.string()).optional(),
62
+ database: z.array(z.string()).optional(),
63
+ api: z.array(z.string()).optional(),
64
+ frontend: z.array(z.string()).optional(),
65
+ })
66
+ .default({}),
67
+ meta: z
68
+ .object({
69
+ language: z.string().optional(),
70
+ tech_stack: z.string().optional(),
71
+ project_mode: z.string().optional(),
72
+ })
73
+ .optional(),
74
+ });
75
+ export function parseApprovalIndex(raw) {
76
+ return approvalIndexSchema.parse(raw);
77
+ }
78
+ /** Returns true when segmented fast-path (monolithic) is allowed. */
79
+ export function isLightweightApproval(index) {
80
+ return (index.tables.length <= 2 &&
81
+ index.interfaces.length <= 3 &&
82
+ index.pages.length <= 2 &&
83
+ !index.optional.s5);
84
+ }
85
+ export function shouldForceSegmented(index) {
86
+ if (index.mode === 'monolithic') {
87
+ return false;
88
+ }
89
+ return !isLightweightApproval(index);
90
+ }
@@ -0,0 +1,4 @@
1
+ export type { ApprovalAssembleResult, ApprovalDiagnostic, ApprovalIndex, ApprovalManifest, ApprovalGenerationMode, } from './types.js';
2
+ export { parseApprovalIndex, isLightweightApproval, shouldForceSegmented, approvalIndexSchema, } from './index-schema.js';
3
+ export { getChangeDirectory, getApprovalWorkspaceDir, getApprovalIndexPath, getApprovalManifestPath, getApprovalPartsDir, getApprovalPartPath, getApprovalOutputPath, getApprovalAnalysisPath, } from './paths.js';
4
+ export { assembleApprovalDocument, readApprovalIndex, readApprovalManifest, } from './assemble.js';
@@ -0,0 +1,3 @@
1
+ export { parseApprovalIndex, isLightweightApproval, shouldForceSegmented, approvalIndexSchema, } from './index-schema.js';
2
+ export { getChangeDirectory, getApprovalWorkspaceDir, getApprovalIndexPath, getApprovalManifestPath, getApprovalPartsDir, getApprovalPartPath, getApprovalOutputPath, getApprovalAnalysisPath, } from './paths.js';
3
+ export { assembleApprovalDocument, readApprovalIndex, readApprovalManifest, } from './assemble.js';
@@ -0,0 +1,8 @@
1
+ export declare function getChangeDirectory(projectRoot: string, changeName: string): string;
2
+ export declare function getApprovalWorkspaceDir(changeDir: string): string;
3
+ export declare function getApprovalIndexPath(changeDir: string): string;
4
+ export declare function getApprovalManifestPath(changeDir: string): string;
5
+ export declare function getApprovalPartsDir(changeDir: string): string;
6
+ export declare function getApprovalPartPath(changeDir: string, partId: string): string;
7
+ export declare function getApprovalOutputPath(changeDir: string): string;
8
+ export declare function getApprovalAnalysisPath(changeDir: string): string;
@@ -0,0 +1,28 @@
1
+ import { join } from 'node:path';
2
+ import { validateChangeName } from '../../utils/change-utils.js';
3
+ const CHANGES_REL = 'specflow/changes';
4
+ export function getChangeDirectory(projectRoot, changeName) {
5
+ validateChangeName(changeName);
6
+ return join(projectRoot, CHANGES_REL, changeName);
7
+ }
8
+ export function getApprovalWorkspaceDir(changeDir) {
9
+ return join(changeDir, 'approval');
10
+ }
11
+ export function getApprovalIndexPath(changeDir) {
12
+ return join(getApprovalWorkspaceDir(changeDir), 'index.yaml');
13
+ }
14
+ export function getApprovalManifestPath(changeDir) {
15
+ return join(getApprovalWorkspaceDir(changeDir), 'manifest.json');
16
+ }
17
+ export function getApprovalPartsDir(changeDir) {
18
+ return join(getApprovalWorkspaceDir(changeDir), 'parts');
19
+ }
20
+ export function getApprovalPartPath(changeDir, partId) {
21
+ return join(getApprovalPartsDir(changeDir), `${partId}.md`);
22
+ }
23
+ export function getApprovalOutputPath(changeDir) {
24
+ return join(changeDir, 'approval.md');
25
+ }
26
+ export function getApprovalAnalysisPath(changeDir) {
27
+ return join(getApprovalWorkspaceDir(changeDir), 'analysis.json');
28
+ }
@@ -0,0 +1,92 @@
1
+ export type ApprovalGenerationMode = 'segmented' | 'monolithic';
2
+ export type ApprovalPartStatus = 'draft' | 'ok' | 'retry';
3
+ export interface ApprovalOptionalChapters {
4
+ readonly s5: boolean;
5
+ readonly s7: boolean;
6
+ readonly s8: boolean;
7
+ }
8
+ export interface ApprovalBatching {
9
+ readonly tables_per_call: number;
10
+ readonly interfaces_per_call: number;
11
+ readonly pages_per_call: number;
12
+ readonly capabilities_per_call: number;
13
+ }
14
+ export interface ApprovalTableRef {
15
+ readonly id: string;
16
+ readonly name: string;
17
+ readonly action: string;
18
+ readonly part: string;
19
+ }
20
+ export interface ApprovalInterfaceRef {
21
+ readonly id: string;
22
+ readonly short: string;
23
+ readonly change: string;
24
+ readonly part: string;
25
+ }
26
+ export interface ApprovalPageRef {
27
+ readonly id: string;
28
+ readonly route: string;
29
+ readonly apis: readonly string[];
30
+ readonly part: string;
31
+ }
32
+ export interface ApprovalCapabilityRef {
33
+ readonly id: string;
34
+ readonly part: string;
35
+ }
36
+ export interface ApprovalConventionsResolved {
37
+ readonly architecture?: readonly string[];
38
+ readonly database?: readonly string[];
39
+ readonly api?: readonly string[];
40
+ readonly frontend?: readonly string[];
41
+ }
42
+ export interface ApprovalIndex {
43
+ readonly schema: 'specflow.approval.index/v1';
44
+ readonly change: string;
45
+ readonly generated_at: string;
46
+ readonly mode: ApprovalGenerationMode;
47
+ readonly ui_in_scope: boolean;
48
+ readonly db_in_scope: boolean;
49
+ readonly api_in_scope: boolean;
50
+ readonly optional: ApprovalOptionalChapters;
51
+ readonly parts_order: readonly string[];
52
+ readonly design_points: readonly string[];
53
+ readonly decisions: readonly string[];
54
+ readonly tables: readonly ApprovalTableRef[];
55
+ readonly interfaces: readonly ApprovalInterfaceRef[];
56
+ readonly pages: readonly ApprovalPageRef[];
57
+ readonly capabilities: readonly ApprovalCapabilityRef[];
58
+ readonly batching: ApprovalBatching;
59
+ readonly conventions: ApprovalConventionsResolved;
60
+ readonly meta?: {
61
+ readonly language?: string;
62
+ readonly tech_stack?: string;
63
+ readonly project_mode?: string;
64
+ };
65
+ }
66
+ export interface ApprovalManifestPart {
67
+ readonly id: string;
68
+ readonly path: string;
69
+ readonly status: ApprovalPartStatus;
70
+ readonly sha256?: string;
71
+ readonly updated_at?: string;
72
+ }
73
+ export interface ApprovalManifest {
74
+ readonly schema: 'specflow.approval.manifest/v1';
75
+ readonly change: string;
76
+ readonly parts: readonly ApprovalManifestPart[];
77
+ readonly assembled_at?: string;
78
+ readonly approval_sha256?: string;
79
+ }
80
+ export type ApprovalDiagnosticSeverity = 'error' | 'warning';
81
+ export interface ApprovalDiagnostic {
82
+ readonly code: string;
83
+ readonly severity: ApprovalDiagnosticSeverity;
84
+ readonly message: string;
85
+ readonly part?: string;
86
+ }
87
+ export interface ApprovalAssembleResult {
88
+ readonly markdown?: string;
89
+ readonly outputPath?: string;
90
+ readonly diagnostics: readonly ApprovalDiagnostic[];
91
+ readonly ok: boolean;
92
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gordon.gan/specflow",
3
- "version": "1.4.3-beta",
3
+ "version": "1.4.6-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": [