@gordon.gan/specflow 1.7.0-beta → 1.8.0-alpha

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.
Files changed (151) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/commands/document-run.d.ts +84 -0
  3. package/dist/cli/commands/document-run.js +539 -0
  4. package/dist/cli/index.js +2 -2
  5. package/dist/core/artifact-language.js +11 -1
  6. package/dist/core/document/chapters.d.ts +6 -0
  7. package/dist/core/document/chapters.js +21 -0
  8. package/dist/core/document/coverage.d.ts +25 -0
  9. package/dist/core/document/coverage.js +49 -0
  10. package/dist/core/document/digests.d.ts +41 -0
  11. package/dist/core/document/digests.js +136 -0
  12. package/dist/core/document/engine.d.ts +92 -0
  13. package/dist/core/document/engine.js +487 -0
  14. package/dist/core/{approval → document}/forbidden-patterns.d.ts +4 -2
  15. package/dist/core/document/forbidden-patterns.js +33 -0
  16. package/dist/core/document/gates.d.ts +27 -0
  17. package/dist/core/document/gates.js +33 -0
  18. package/dist/core/document/index.d.ts +7 -0
  19. package/dist/core/document/index.js +7 -0
  20. package/dist/core/document/input-digest.d.ts +55 -0
  21. package/dist/core/document/input-digest.js +121 -0
  22. package/dist/core/document/input-features.d.ts +24 -0
  23. package/dist/core/document/input-features.js +112 -0
  24. package/dist/core/document/lint.d.ts +22 -0
  25. package/dist/core/document/lint.js +29 -0
  26. package/dist/core/document/llm.d.ts +35 -0
  27. package/dist/core/document/llm.js +27 -0
  28. package/dist/core/document/map.d.ts +46 -0
  29. package/dist/core/document/map.js +212 -0
  30. package/dist/core/document/outline.d.ts +26 -0
  31. package/dist/core/document/outline.js +128 -0
  32. package/dist/core/document/paths.d.ts +23 -0
  33. package/dist/core/document/paths.js +31 -0
  34. package/dist/core/document/profile-validator.d.ts +23 -0
  35. package/dist/core/document/profile-validator.js +127 -0
  36. package/dist/core/document/profiles.d.ts +23 -0
  37. package/dist/core/document/profiles.js +69 -0
  38. package/dist/core/document/render.d.ts +19 -0
  39. package/dist/core/document/render.js +97 -0
  40. package/dist/core/document/review.d.ts +42 -0
  41. package/dist/core/document/review.js +157 -0
  42. package/dist/core/document/scene-detect.d.ts +25 -0
  43. package/dist/core/document/scene-detect.js +121 -0
  44. package/dist/core/document/schemas.d.ts +949 -0
  45. package/dist/core/document/schemas.js +158 -0
  46. package/dist/integrations/shared/capability-evidence.js +4 -8
  47. package/dist/integrations/shared/command-catalog.js +1 -1
  48. package/dist/integrations/shared/parity-manifest.js +4 -8
  49. package/package.json +1 -1
  50. package/prompts/apply/phase-a-plan.md +1 -1
  51. package/prompts/document/map/acceptance.md +8 -0
  52. package/prompts/document/map/api-design.md +17 -0
  53. package/prompts/document/map/architecture.md +7 -0
  54. package/prompts/document/map/closed-loop.md +8 -0
  55. package/prompts/document/map/compat-migration.md +8 -0
  56. package/prompts/document/map/config-runtime.md +8 -0
  57. package/prompts/document/map/core-logic.md +8 -0
  58. package/prompts/document/map/data-model.md +17 -0
  59. package/prompts/document/map/deploy.md +8 -0
  60. package/prompts/document/map/fix.md +7 -0
  61. package/prompts/document/map/goal.md +7 -0
  62. package/prompts/document/map/impact.md +7 -0
  63. package/prompts/document/map/implementability.md +8 -0
  64. package/prompts/document/map/mvp-boundary.md +7 -0
  65. package/prompts/document/map/non-goals.md +7 -0
  66. package/prompts/document/map/regression.md +7 -0
  67. package/prompts/document/map/reproduce.md +7 -0
  68. package/prompts/document/map/requirement.md +7 -0
  69. package/prompts/document/map/root-cause.md +7 -0
  70. package/prompts/document/map/signoff.md +8 -0
  71. package/prompts/document/map/tech-selection.md +17 -0
  72. package/prompts/document/map/test-strategy.md +7 -0
  73. package/prompts/document/map/ui-design.md +18 -0
  74. package/prompts/document/outline/general.md +37 -0
  75. package/prompts/document/review/ai-review.md +56 -0
  76. package/prompts/explore/explore-session.md +1 -1
  77. package/prompts/propose/design-draft.md +1 -1
  78. package/prompts/propose/proposal.md +1 -1
  79. package/prompts/propose/specs.md +1 -1
  80. package/prompts/propose/tasks-draft.md +1 -1
  81. package/prompts/refine/design-output.md +1 -1
  82. package/prompts/refine/update-artifacts.md +2 -0
  83. package/prompts/shared/artifact-language.md +13 -0
  84. package/skills/specflow-document/SKILL.md +124 -0
  85. package/templates/document/chapters/acceptance.yaml +16 -0
  86. package/templates/document/chapters/api-design.yaml +24 -0
  87. package/templates/document/chapters/architecture.yaml +18 -0
  88. package/templates/document/chapters/closed-loop.yaml +22 -0
  89. package/templates/document/chapters/compat-migration.yaml +17 -0
  90. package/templates/document/chapters/config-runtime.yaml +16 -0
  91. package/templates/document/chapters/core-logic.yaml +16 -0
  92. package/templates/document/chapters/data-model.yaml +25 -0
  93. package/templates/document/chapters/deploy.yaml +18 -0
  94. package/templates/document/chapters/fix.yaml +18 -0
  95. package/templates/document/chapters/goal.yaml +17 -0
  96. package/templates/document/chapters/impact.yaml +16 -0
  97. package/templates/document/chapters/implementability.yaml +22 -0
  98. package/templates/document/chapters/mvp-boundary.yaml +20 -0
  99. package/templates/document/chapters/non-goals.yaml +17 -0
  100. package/templates/document/chapters/regression.yaml +16 -0
  101. package/templates/document/chapters/reproduce.yaml +16 -0
  102. package/templates/document/chapters/requirement.yaml +18 -0
  103. package/templates/document/chapters/root-cause.yaml +18 -0
  104. package/templates/document/chapters/signoff.yaml +15 -0
  105. package/templates/document/chapters/tech-selection.yaml +20 -0
  106. package/templates/document/chapters/test-strategy.yaml +20 -0
  107. package/templates/document/chapters/ui-design.yaml +22 -0
  108. package/templates/document/profiles/0to1.yaml +12 -0
  109. package/templates/document/profiles/approve.yaml +22 -0
  110. package/templates/document/profiles/bugfix.yaml +10 -0
  111. package/templates/document/profiles/feature.yaml +11 -0
  112. package/dist/cli/commands/approval-assemble.d.ts +0 -64
  113. package/dist/cli/commands/approval-assemble.js +0 -408
  114. package/dist/core/approval/assemble.d.ts +0 -10
  115. package/dist/core/approval/assemble.js +0 -384
  116. package/dist/core/approval/bundle.d.ts +0 -9
  117. package/dist/core/approval/bundle.js +0 -172
  118. package/dist/core/approval/forbidden-patterns.js +0 -37
  119. package/dist/core/approval/index-schema.d.ts +0 -676
  120. package/dist/core/approval/index-schema.js +0 -193
  121. package/dist/core/approval/index.d.ts +0 -12
  122. package/dist/core/approval/index.js +0 -9
  123. package/dist/core/approval/lint.d.ts +0 -10
  124. package/dist/core/approval/lint.js +0 -302
  125. package/dist/core/approval/paths.d.ts +0 -13
  126. package/dist/core/approval/paths.js +0 -43
  127. package/dist/core/approval/pipeline.d.ts +0 -28
  128. package/dist/core/approval/pipeline.js +0 -146
  129. package/dist/core/approval/playbook-schema.d.ts +0 -182
  130. package/dist/core/approval/playbook-schema.js +0 -51
  131. package/dist/core/approval/render.d.ts +0 -20
  132. package/dist/core/approval/render.js +0 -210
  133. package/dist/core/approval/review-pack.d.ts +0 -26
  134. package/dist/core/approval/review-pack.js +0 -205
  135. package/dist/core/approval/types.d.ts +0 -223
  136. package/dist/core/approval/types.js +0 -1
  137. package/prompts/approval/acp-pipeline.md +0 -104
  138. package/prompts/approval/ai-review.md +0 -145
  139. package/prompts/approval/api-guidance.md +0 -179
  140. package/prompts/approval/database-guidance.md +0 -105
  141. package/prompts/approval/frontend-guidance.md +0 -249
  142. package/prompts/approval/generate.md +0 -1583
  143. package/prompts/approval/multi-repo-guidance.md +0 -238
  144. package/prompts/approval/project-conventions-guidance.md +0 -171
  145. package/prompts/approval/runtime-guidance.md +0 -64
  146. package/prompts/approval/segmented-generation.md +0 -157
  147. package/skills/specflow-approval/SKILL.md +0 -663
  148. package/templates/approval-index.yaml +0 -93
  149. package/templates/approval-part.md +0 -15
  150. package/templates/approval-playbook.yaml +0 -28
  151. package/templates/approval.md +0 -261
@@ -1,193 +0,0 @@
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
- repo: z.string().min(1).optional(),
8
- });
9
- const interfaceFrozenSchema = z.object({
10
- rpc: z.string().min(1).optional(),
11
- http_path: z.string().min(1).optional(),
12
- status_enum: z.array(z.string().min(1)).optional(),
13
- });
14
- const interfaceRefSchema = z.object({
15
- id: z.string().min(1),
16
- short: z.string().min(1),
17
- change: z.string().min(1),
18
- part: z.string().min(1),
19
- repo: z.string().min(1).optional(),
20
- layers: z.array(z.string().min(1)).optional(),
21
- pairs_with: z.string().min(1).optional(),
22
- feature_slice: z.string().min(1).optional(),
23
- frozen: interfaceFrozenSchema.optional(),
24
- });
25
- const pageRefSchema = z.object({
26
- id: z.string().min(1),
27
- route: z.string().min(1),
28
- apis: z.array(z.string()).default([]),
29
- part: z.string().min(1),
30
- repo: z.string().min(1).optional(),
31
- });
32
- const capabilityRefSchema = z.object({
33
- id: z.string().min(1),
34
- part: z.string().min(1),
35
- });
36
- const outputRefSchema = z.object({
37
- repo: z.string().min(1),
38
- change: z.string().min(1),
39
- path: z.string().min(1),
40
- });
41
- const multiRepoSchema = z
42
- .object({
43
- enabled: z.boolean(),
44
- document_mode: z.enum(['unified', 'per_repo']).optional(),
45
- primary_repo: z.string().min(1).optional(),
46
- repos: z
47
- .array(z.object({
48
- id: z.string().min(1),
49
- label: z.string().min(1),
50
- role: z.enum(['platform', 'web', 'worker', 'hub', 'other']).optional(),
51
- change: z.string().min(1),
52
- root_hint: z.string().min(1).optional(),
53
- }))
54
- .min(1),
55
- outputs: z
56
- .object({
57
- unified: outputRefSchema.optional(),
58
- per_repo: z.array(outputRefSchema).optional(),
59
- })
60
- .optional(),
61
- bundle: z
62
- .object({
63
- enabled: z.boolean().optional(),
64
- playbook: z.string().min(1).optional(),
65
- output: z.string().min(1).optional(),
66
- readonly: z.boolean().optional(),
67
- })
68
- .optional(),
69
- })
70
- .superRefine((value, ctx) => {
71
- if (!value.enabled) {
72
- return;
73
- }
74
- if (value.repos.length < 2) {
75
- ctx.addIssue({
76
- code: z.ZodIssueCode.custom,
77
- message: 'multi_repo.repos must have at least 2 entries when enabled',
78
- path: ['repos'],
79
- });
80
- }
81
- if (value.document_mode === 'unified' && !value.primary_repo) {
82
- ctx.addIssue({
83
- code: z.ZodIssueCode.custom,
84
- message: 'multi_repo.primary_repo is required when document_mode is unified',
85
- path: ['primary_repo'],
86
- });
87
- }
88
- if (value.primary_repo && !value.repos.some((r) => r.id === value.primary_repo)) {
89
- ctx.addIssue({
90
- code: z.ZodIssueCode.custom,
91
- message: 'multi_repo.primary_repo must match a repos[].id',
92
- path: ['primary_repo'],
93
- });
94
- }
95
- if (value.bundle?.enabled === true && !value.primary_repo) {
96
- ctx.addIssue({
97
- code: z.ZodIssueCode.custom,
98
- message: 'multi_repo.primary_repo is required when bundle.enabled is true',
99
- path: ['primary_repo'],
100
- });
101
- }
102
- });
103
- export const approvalIndexSchema = z.object({
104
- schema: z.literal('specflow.approval.index/v1'),
105
- change: z.string().min(1),
106
- generated_at: z.preprocess((val) => (val instanceof Date ? val.toISOString() : val), z.string().min(1)),
107
- mode: z.enum(['segmented', 'monolithic']).default('segmented'),
108
- ui_in_scope: z.boolean().default(false),
109
- db_in_scope: z.boolean().default(false),
110
- api_in_scope: z.boolean().default(false),
111
- optional: z
112
- .object({
113
- s5: z.boolean().default(false),
114
- s7: z.boolean().default(false),
115
- s8: z.boolean().default(false),
116
- })
117
- .default({ s5: false, s7: false, s8: false }),
118
- parts_order: z.array(z.string().min(1)).min(1),
119
- design_points: z.array(z.string()).default([]),
120
- decisions: z.array(z.string()).default([]),
121
- tables: z.array(tableRefSchema).default([]),
122
- interfaces: z.array(interfaceRefSchema).default([]),
123
- pages: z.array(pageRefSchema).default([]),
124
- capabilities: z.array(capabilityRefSchema).default([]),
125
- batching: z
126
- .object({
127
- tables_per_call: z.number().int().positive().default(3),
128
- interfaces_per_call: z.number().int().positive().default(3),
129
- pages_per_call: z.number().int().positive().default(3),
130
- capabilities_per_call: z.number().int().positive().default(1),
131
- })
132
- .default({
133
- tables_per_call: 3,
134
- interfaces_per_call: 3,
135
- pages_per_call: 3,
136
- capabilities_per_call: 1,
137
- }),
138
- conventions: z
139
- .object({
140
- architecture: z.array(z.string()).optional(),
141
- database: z.array(z.string()).optional(),
142
- api: z.array(z.string()).optional(),
143
- frontend: z.array(z.string()).optional(),
144
- })
145
- .default({}),
146
- meta: z
147
- .object({
148
- language: z.string().optional(),
149
- tech_stack: z.string().optional(),
150
- project_mode: z.string().optional(),
151
- })
152
- .optional(),
153
- multi_repo: multiRepoSchema.optional(),
154
- acp: z
155
- .object({
156
- pipeline: z.enum(['acp/v1', 'acp/v2']).optional(),
157
- context_budget_tokens: z.number().int().positive().optional(),
158
- output_budget_tokens: z.number().int().positive().optional(),
159
- feature_slices: z
160
- .array(z.object({
161
- id: z.string().min(1),
162
- repos: z.array(z.string()).optional(),
163
- interfaces: z.array(z.string()).optional(),
164
- pages: z.array(z.string()).optional(),
165
- design_points: z.array(z.string()).optional(),
166
- parts: z.array(z.string().min(1)).min(1),
167
- }))
168
- .optional(),
169
- review: z
170
- .object({
171
- enabled: z.boolean().optional(),
172
- required_pass: z.boolean().optional(),
173
- })
174
- .optional(),
175
- })
176
- .optional(),
177
- });
178
- export function parseApprovalIndex(raw) {
179
- return approvalIndexSchema.parse(raw);
180
- }
181
- /** Returns true when segmented fast-path (monolithic) is allowed. */
182
- export function isLightweightApproval(index) {
183
- return (index.tables.length <= 2 &&
184
- index.interfaces.length <= 3 &&
185
- index.pages.length <= 2 &&
186
- !index.optional.s5);
187
- }
188
- export function shouldForceSegmented(index) {
189
- if (index.mode === 'monolithic') {
190
- return false;
191
- }
192
- return !isLightweightApproval(index);
193
- }
@@ -1,12 +0,0 @@
1
- export type { ApprovalAssembleResult, ApprovalDiagnostic, ApprovalIndex, ApprovalManifest, ApprovalGenerationMode, ApprovalMultiRepo, ApprovalDocumentMode, ApprovalPlaybook, ApprovalBundleResult, ApprovalReviewPacket, ApprovalReviewResult, ApprovalAcpBlock, ApprovalFeatureSlice, } from './types.js';
2
- export { parseApprovalIndex, isLightweightApproval, shouldForceSegmented, approvalIndexSchema, } from './index-schema.js';
3
- export { parseApprovalPlaybook, parseApprovalReviewResult } from './playbook-schema.js';
4
- export { getChangeDirectory, getApprovalWorkspaceDir, getApprovalIndexPath, getApprovalManifestPath, getApprovalPartsDir, getApprovalPartPath, getApprovalOutputPath, getApprovalAnalysisPath, getApprovalReviewPacketPath, getApprovalReviewPath, getApprovalPlaybookPath, getApprovalBundleOutputPath, } from './paths.js';
5
- export { assembleApprovalDocument, readApprovalIndex, readApprovalManifest, } from './assemble.js';
6
- export { lintApprovalDocument } from './lint.js';
7
- export { bundleApprovalDocument, resolvePlaybookPath } from './bundle.js';
8
- export { generateReviewPacket, checkApprovalReview, computeReviewPartHashes } from './review-pack.js';
9
- export { finalizeApprovalDocument } from './pipeline.js';
10
- export type { FinalizeApprovalResult, FinalizeApprovalOptions, ApprovalFinalizeStage } from './pipeline.js';
11
- export { renderApprovalDocument, resolveApprovalRenderPaths } from './render.js';
12
- export type { RenderApprovalOptions, RenderApprovalResult } from './render.js';
@@ -1,9 +0,0 @@
1
- export { parseApprovalIndex, isLightweightApproval, shouldForceSegmented, approvalIndexSchema, } from './index-schema.js';
2
- export { parseApprovalPlaybook, parseApprovalReviewResult } from './playbook-schema.js';
3
- export { getChangeDirectory, getApprovalWorkspaceDir, getApprovalIndexPath, getApprovalManifestPath, getApprovalPartsDir, getApprovalPartPath, getApprovalOutputPath, getApprovalAnalysisPath, getApprovalReviewPacketPath, getApprovalReviewPath, getApprovalPlaybookPath, getApprovalBundleOutputPath, } from './paths.js';
4
- export { assembleApprovalDocument, readApprovalIndex, readApprovalManifest, } from './assemble.js';
5
- export { lintApprovalDocument } from './lint.js';
6
- export { bundleApprovalDocument, resolvePlaybookPath } from './bundle.js';
7
- export { generateReviewPacket, checkApprovalReview, computeReviewPartHashes } from './review-pack.js';
8
- export { finalizeApprovalDocument } from './pipeline.js';
9
- export { renderApprovalDocument, resolveApprovalRenderPaths } from './render.js';
@@ -1,10 +0,0 @@
1
- import type { ApprovalDiagnostic } from './types.js';
2
- export interface ApprovalLintOptions {
3
- readonly changeDir: string;
4
- readonly strict?: boolean;
5
- }
6
- export interface ApprovalLintResult {
7
- readonly ok: boolean;
8
- readonly diagnostics: readonly ApprovalDiagnostic[];
9
- }
10
- export declare function lintApprovalDocument(options: ApprovalLintOptions): Promise<ApprovalLintResult>;
@@ -1,302 +0,0 @@
1
- import { promises as fs } from 'node:fs';
2
- import { findForbiddenMatch, findInconsistencyHints } from './forbidden-patterns.js';
3
- import { readApprovalIndex } from './assemble.js';
4
- import { getApprovalAnalysisPath, getApprovalPartPath } from './paths.js';
5
- async function readPartContents(changeDir, index) {
6
- const parts = new Map();
7
- for (const partId of index.parts_order) {
8
- try {
9
- parts.set(partId, await fs.readFile(getApprovalPartPath(changeDir, partId), 'utf-8'));
10
- }
11
- catch {
12
- // missing parts handled by assemble check
13
- }
14
- }
15
- return parts;
16
- }
17
- function allPartsText(parts) {
18
- return [...parts.values()].join('\n');
19
- }
20
- export async function lintApprovalDocument(options) {
21
- const diagnostics = [];
22
- const { changeDir, strict = false } = options;
23
- let index;
24
- try {
25
- index = await readApprovalIndex(changeDir);
26
- }
27
- catch (error) {
28
- const message = error instanceof Error ? error.message : String(error);
29
- return {
30
- ok: false,
31
- diagnostics: [
32
- {
33
- code: 'missing_index',
34
- severity: 'error',
35
- message: `Cannot read approval/index.yaml: ${message}`,
36
- },
37
- ],
38
- };
39
- }
40
- const partContents = await readPartContents(changeDir, index);
41
- const combined = allPartsText(partContents);
42
- // C3: design_points referenced somewhere in parts
43
- for (const point of index.design_points) {
44
- const id = point.replace(/^P/, 'P');
45
- const pattern = new RegExp(`\\b${escapeRegExp(id)}\\b`);
46
- if (!pattern.test(combined)) {
47
- diagnostics.push({
48
- code: 'design_point_unreferenced',
49
- severity: strict ? 'error' : 'warning',
50
- message: `Design point "${id}" listed in index but not referenced in any part`,
51
- });
52
- }
53
- }
54
- // C3b: decisions referenced in parts
55
- for (const decision of index.decisions) {
56
- const id = decision.replace(/^D/, 'D');
57
- const pattern = new RegExp(`\\b${escapeRegExp(id)}\\b`);
58
- if (!pattern.test(combined)) {
59
- diagnostics.push({
60
- code: 'decision_unreferenced',
61
- severity: strict ? 'error' : 'warning',
62
- message: `Decision "${id}" listed in index but not referenced in any part`,
63
- });
64
- }
65
- }
66
- // C3c: tables / capabilities referenced in assigned parts
67
- for (const table of index.tables) {
68
- const partText = partContents.get(table.part) ?? '';
69
- if (partText.length === 0) {
70
- continue;
71
- }
72
- const hasRef = partText.includes(table.id) ||
73
- partText.includes(table.name) ||
74
- new RegExp(`\\b${escapeRegExp(table.id)}\\b`).test(partText);
75
- if (!hasRef) {
76
- diagnostics.push({
77
- code: 'table_unreferenced',
78
- severity: strict ? 'error' : 'warning',
79
- message: `Table "${table.id}" (${table.name}) not referenced in part "${table.part}"`,
80
- part: table.part,
81
- });
82
- }
83
- }
84
- for (const cap of index.capabilities) {
85
- const partText = partContents.get(cap.part) ?? '';
86
- if (partText.length === 0) {
87
- continue;
88
- }
89
- if (!partText.includes(cap.id)) {
90
- diagnostics.push({
91
- code: 'capability_unreferenced',
92
- severity: strict ? 'error' : 'warning',
93
- message: `Capability "${cap.id}" not referenced in part "${cap.part}"`,
94
- part: cap.part,
95
- });
96
- }
97
- }
98
- // C4: page apis exist as interface ids
99
- const interfaceIds = new Set(index.interfaces.map((i) => i.id));
100
- for (const page of index.pages) {
101
- for (const apiId of page.apis) {
102
- if (!interfaceIds.has(apiId)) {
103
- diagnostics.push({
104
- code: 'page_api_missing',
105
- severity: 'error',
106
- message: `Page "${page.id}" references api "${apiId}" not in index.interfaces`,
107
- part: page.part,
108
- });
109
- }
110
- }
111
- }
112
- // C1 + C2: frozen fields and pairs_with
113
- for (const iface of index.interfaces) {
114
- const frozen = iface.frozen;
115
- const partText = partContents.get(iface.part) ?? '';
116
- if (frozen?.rpc) {
117
- const rpcShort = frozen.rpc.split('.').pop() ?? frozen.rpc;
118
- if (partText.length > 0 && !partText.includes(frozen.rpc) && !partText.includes(rpcShort)) {
119
- diagnostics.push({
120
- code: 'frozen_rpc_missing',
121
- severity: 'error',
122
- message: `Interface "${iface.id}" frozen.rpc "${frozen.rpc}" not found in part "${iface.part}"`,
123
- part: iface.part,
124
- });
125
- }
126
- }
127
- if (frozen?.http_path) {
128
- const pathOnly = frozen.http_path.replace(/^(GET|POST|PUT|PATCH|DELETE)\s+/i, '');
129
- if (partText.length > 0 && !partText.includes(pathOnly)) {
130
- diagnostics.push({
131
- code: 'frozen_http_path_missing',
132
- severity: 'error',
133
- message: `Interface "${iface.id}" frozen.http_path "${frozen.http_path}" not found in part "${iface.part}"`,
134
- part: iface.part,
135
- });
136
- }
137
- }
138
- if (frozen?.status_enum && frozen.status_enum.length > 0) {
139
- for (const status of frozen.status_enum) {
140
- if (partText.length > 0 && !partText.includes(status)) {
141
- diagnostics.push({
142
- code: 'frozen_status_enum_missing',
143
- severity: strict ? 'error' : 'warning',
144
- message: `Interface "${iface.id}" frozen status "${status}" not mentioned in part "${iface.part}"`,
145
- part: iface.part,
146
- });
147
- }
148
- }
149
- }
150
- if (iface.pairs_with) {
151
- const pair = index.interfaces.find((i) => i.id === iface.pairs_with);
152
- if (!pair) {
153
- diagnostics.push({
154
- code: 'pairs_with_missing',
155
- severity: 'error',
156
- message: `Interface "${iface.id}" pairs_with "${iface.pairs_with}" not in index.interfaces`,
157
- part: iface.part,
158
- });
159
- }
160
- else if (!partContents.has(pair.part)) {
161
- diagnostics.push({
162
- code: 'pairs_with_part_missing',
163
- severity: 'error',
164
- message: `Interface "${iface.id}" pairs_with part "${pair.part}" file missing`,
165
- part: iface.part,
166
- });
167
- }
168
- else {
169
- const pairText = partContents.get(pair.part) ?? '';
170
- if (partText.length > 0 && !partText.includes(iface.pairs_with)) {
171
- diagnostics.push({
172
- code: 'pairs_with_unref',
173
- severity: 'warning',
174
- message: `Interface "${iface.id}" part does not cross-ref pairs_with "${iface.pairs_with}"`,
175
- part: iface.part,
176
- });
177
- }
178
- if (pairText.length > 0 && !pairText.includes(iface.id)) {
179
- diagnostics.push({
180
- code: 'pairs_with_unref',
181
- severity: 'warning',
182
- message: `Interface "${iface.pairs_with}" part does not cross-ref pair "${iface.id}"`,
183
- part: pair.part,
184
- });
185
- }
186
- }
187
- }
188
- }
189
- // C6: feature_slices — all listed parts exist + depth parity
190
- if (index.acp?.feature_slices) {
191
- for (const slice of index.acp.feature_slices) {
192
- const byteLengths = [];
193
- for (const partId of slice.parts) {
194
- const content = partContents.get(partId);
195
- if (!content) {
196
- diagnostics.push({
197
- code: 'feature_slice_part_missing',
198
- severity: 'error',
199
- message: `Feature slice "${slice.id}" references missing part "${partId}"`,
200
- part: partId,
201
- });
202
- }
203
- else {
204
- byteLengths.push(Buffer.byteLength(content, 'utf8'));
205
- }
206
- }
207
- if (byteLengths.length >= 2) {
208
- const min = Math.min(...byteLengths);
209
- const max = Math.max(...byteLengths);
210
- if (min > 0 && max / min >= 8) {
211
- diagnostics.push({
212
- code: 'feature_slice_depth_imbalance',
213
- severity: strict ? 'error' : 'warning',
214
- message: `Feature slice "${slice.id}" has large depth imbalance across parts (max/min bytes ≥ 8)`,
215
- });
216
- }
217
- }
218
- }
219
- }
220
- // C6b: context budget hint (acp/v2)
221
- const budget = index.acp?.context_budget_tokens;
222
- if (budget && budget > 0) {
223
- const approxTokens = Math.ceil(combined.length / 4);
224
- if (approxTokens > budget * 1.5) {
225
- diagnostics.push({
226
- code: 'context_budget_exceeded',
227
- severity: 'warning',
228
- message: `Combined parts ~${approxTokens} tokens exceed acp.context_budget_tokens (${budget}) — consider smaller Map batches`,
229
- });
230
- }
231
- }
232
- // C7: analysis.json frozen_rpc aligns with index
233
- try {
234
- const analysisRaw = JSON.parse(await fs.readFile(getApprovalAnalysisPath(changeDir), 'utf-8'));
235
- if (analysisRaw.frozen_rpc) {
236
- const rpcInterfaces = index.interfaces.filter((i) => i.frozen?.rpc);
237
- if (rpcInterfaces.length > 0) {
238
- const mismatch = rpcInterfaces.some((i) => i.frozen.rpc !== analysisRaw.frozen_rpc);
239
- if (mismatch) {
240
- diagnostics.push({
241
- code: 'analysis_frozen_rpc_mismatch',
242
- severity: 'warning',
243
- message: `analysis.json frozen_rpc "${analysisRaw.frozen_rpc}" differs from index interface frozen.rpc values`,
244
- });
245
- }
246
- }
247
- }
248
- }
249
- catch {
250
- // analysis optional for lint
251
- }
252
- // C8: multi_repo — each repo has at least one entity or part tagged
253
- if (index.multi_repo?.enabled) {
254
- for (const repo of index.multi_repo.repos) {
255
- const hasEntity = index.interfaces.some((i) => i.repo === repo.id) ||
256
- index.pages.some((p) => p.repo === repo.id) ||
257
- index.tables.some((t) => t.repo === repo.id);
258
- if (!hasEntity && index.multi_repo.document_mode === 'per_repo') {
259
- diagnostics.push({
260
- code: 'multi_repo_repo_unrepresented',
261
- severity: 'warning',
262
- message: `Multi-repo entry "${repo.id}" has no interfaces/pages/tables with repo tag`,
263
- });
264
- }
265
- }
266
- }
267
- // C5: forbidden patterns per part
268
- for (const [partId, content] of partContents) {
269
- const forbidden = findForbiddenMatch(content);
270
- if (forbidden) {
271
- diagnostics.push({
272
- code: 'forbidden_pattern',
273
- severity: 'error',
274
- message: `Part "${partId}": forbidden stub pattern (${forbidden.source})`,
275
- part: partId,
276
- });
277
- }
278
- for (const hint of findInconsistencyHints(content)) {
279
- diagnostics.push({
280
- code: 'inconsistency_hint',
281
- severity: strict ? 'error' : 'warning',
282
- message: `Part "${partId}": possible semantic inconsistency (${hint.source})`,
283
- part: partId,
284
- });
285
- }
286
- }
287
- // C9: appendix duplication in signoff
288
- const signoff = partContents.get('10-signoff') ?? '';
289
- if (/附录\s*A/i.test(signoff)) {
290
- diagnostics.push({
291
- code: 'appendix_in_signoff',
292
- severity: 'warning',
293
- message: 'Part "10-signoff" contains 附录 A; assemble will skip auto-appendix — ensure no duplicate in output',
294
- part: '10-signoff',
295
- });
296
- }
297
- const hasError = diagnostics.some((d) => d.severity === 'error');
298
- return { ok: !hasError, diagnostics };
299
- }
300
- function escapeRegExp(value) {
301
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
302
- }
@@ -1,13 +0,0 @@
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;
9
- export declare function getApprovalReviewPacketPath(changeDir: string): string;
10
- export declare function getApprovalReviewPath(changeDir: string): string;
11
- export declare function getApprovalPlaybookPath(changeDir: string): string;
12
- export declare function getApprovalBundleOutputPath(changeDir: string, relativePath: string): string;
13
- export declare function getApprovalHtmlOutputPath(changeDir: string): string;
@@ -1,43 +0,0 @@
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
- }
29
- export function getApprovalReviewPacketPath(changeDir) {
30
- return join(getApprovalWorkspaceDir(changeDir), 'review-packet.json');
31
- }
32
- export function getApprovalReviewPath(changeDir) {
33
- return join(getApprovalWorkspaceDir(changeDir), 'review-result.json');
34
- }
35
- export function getApprovalPlaybookPath(changeDir) {
36
- return join(getApprovalWorkspaceDir(changeDir), 'approval-playbook.yaml');
37
- }
38
- export function getApprovalBundleOutputPath(changeDir, relativePath) {
39
- return join(changeDir, relativePath);
40
- }
41
- export function getApprovalHtmlOutputPath(changeDir) {
42
- return join(changeDir, 'approval.html');
43
- }
@@ -1,28 +0,0 @@
1
- import type { ApprovalDiagnostic } from './types.js';
2
- export type ApprovalFinalizeStage = 'lint' | 'review-pack' | 'review-required' | 'review-check' | 'check' | 'assemble' | 'bundle' | 'done';
3
- export interface FinalizeApprovalOptions {
4
- readonly changeDir: string;
5
- readonly workspaceRoot?: string;
6
- readonly strict?: boolean;
7
- readonly skipReview?: boolean;
8
- readonly force?: boolean;
9
- readonly bundle?: boolean | 'auto';
10
- readonly playbook?: string;
11
- readonly dryRun?: boolean;
12
- readonly validateLazy?: boolean;
13
- }
14
- export interface FinalizeApprovalResult {
15
- readonly ok: boolean;
16
- readonly paused?: boolean;
17
- readonly stage: ApprovalFinalizeStage;
18
- readonly change: string;
19
- readonly outputPath?: string;
20
- readonly bundlePath?: string;
21
- readonly reviewPacketPath?: string;
22
- readonly diagnostics: readonly ApprovalDiagnostic[];
23
- }
24
- /**
25
- * One-shot ACP finalize: lint → review gate → check → assemble → optional bundle.
26
- * When AI review is required but review-result.json is missing, pauses after review-pack.
27
- */
28
- export declare function finalizeApprovalDocument(options: FinalizeApprovalOptions): Promise<FinalizeApprovalResult>;