@fission-ai/openspec 1.6.0-beta.1 → 1.7.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/README.md +28 -1
- package/dist/cli/index.js +73 -7
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +90 -50
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +21 -11
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +20 -24
- package/dist/commands/validate.js +18 -3
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +89 -6
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +7 -2
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +96 -33
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
- package/dist/core/artifact-graph/instruction-loader.js +55 -7
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +34 -6
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +6 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- 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/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- 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 +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- 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.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +8 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +7 -1
- package/dist/core/config-schema.js +40 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +21 -3
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/init.d.ts +35 -0
- package/dist/core/init.js +225 -39
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +249 -22
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +71 -1
- package/dist/core/migration.js +294 -1
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +8 -0
- package/dist/core/parsers/requirement-blocks.js +60 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.js +3 -29
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +9 -45
- package/dist/core/project-config.d.ts +25 -6
- package/dist/core/project-config.js +77 -7
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +105 -6
- package/dist/core/specs-apply.d.ts +8 -33
- package/dist/core/specs-apply.js +252 -157
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/workflows/apply-change.js +54 -4
- package/dist/core/templates/workflows/archive-change.js +166 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
- package/dist/core/templates/workflows/continue-change.js +32 -40
- package/dist/core/templates/workflows/explore.js +12 -0
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/templates/workflows/propose.js +50 -32
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +172 -22
- package/dist/core/templates/workflows/update-change.js +20 -12
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +39 -5
- package/dist/core/update.js +288 -71
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +1 -7
- package/dist/core/validation/validator.js +92 -36
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +736 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +55 -33
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +30 -0
- package/dist/utils/change-metadata.js +101 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/command-references.d.ts +80 -6
- package/dist/utils/command-references.js +138 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +102 -0
- package/package.json +29 -30
- package/schemas/spec-driven/schema.yaml +60 -5
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +5 -1
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
|
@@ -2,6 +2,16 @@ import { buildCodeFenceMask } from './requirement-text.js';
|
|
|
2
2
|
export function normalizeRequirementName(name) {
|
|
3
3
|
return name.trim();
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Case- and whitespace-insensitive fold of a requirement name. Requirement
|
|
7
|
+
* matching itself is case-sensitive (normalizeRequirementName); this fold
|
|
8
|
+
* exists only for typo detection - near-miss REMOVED headers and the
|
|
9
|
+
* RENAMED+REMOVED cross-section conflict - where two spellings that differ
|
|
10
|
+
* only in case or interior whitespace mean a mistake, never two requirements.
|
|
11
|
+
*/
|
|
12
|
+
export function foldRequirementName(name) {
|
|
13
|
+
return normalizeRequirementName(name).toLowerCase().replace(/\s+/g, ' ');
|
|
14
|
+
}
|
|
5
15
|
/** The canonical requirement header the delta reader recognizes. */
|
|
6
16
|
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
7
17
|
/**
|
|
@@ -10,7 +20,8 @@ const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
|
10
20
|
export function extractRequirementsSection(content) {
|
|
11
21
|
const normalized = normalizeLineEndings(content);
|
|
12
22
|
const lines = normalized.split('\n');
|
|
13
|
-
const
|
|
23
|
+
const fenceMask = buildCodeFenceMask(lines);
|
|
24
|
+
const reqHeaderIndex = lines.findIndex((l, i) => !fenceMask[i] && /^##\s+Requirements\s*$/i.test(l));
|
|
14
25
|
if (reqHeaderIndex === -1) {
|
|
15
26
|
// No requirements section; create an empty one at the end
|
|
16
27
|
const before = content.trimEnd();
|
|
@@ -26,7 +37,7 @@ export function extractRequirementsSection(content) {
|
|
|
26
37
|
// Find end of this section: next line that starts with '## ' at same or higher level
|
|
27
38
|
let endIndex = lines.length;
|
|
28
39
|
for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
|
|
29
|
-
if (/^##\s+/.test(lines[i])) {
|
|
40
|
+
if (!fenceMask[i] && /^##\s+/.test(lines[i])) {
|
|
30
41
|
endIndex = i;
|
|
31
42
|
break;
|
|
32
43
|
}
|
|
@@ -34,29 +45,31 @@ export function extractRequirementsSection(content) {
|
|
|
34
45
|
const before = lines.slice(0, reqHeaderIndex).join('\n');
|
|
35
46
|
const headerLine = lines[reqHeaderIndex];
|
|
36
47
|
const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
|
|
48
|
+
const sectionBodyMask = fenceMask.slice(reqHeaderIndex + 1, endIndex);
|
|
49
|
+
const isRequirementHeader = (cursor) => !sectionBodyMask[cursor] && REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]);
|
|
50
|
+
const isTopLevelHeader = (cursor) => !sectionBodyMask[cursor] && /^##\s+/.test(sectionBodyLines[cursor]);
|
|
37
51
|
// Parse requirement blocks within section body
|
|
38
52
|
const blocks = [];
|
|
39
53
|
let cursor = 0;
|
|
40
54
|
let preambleLines = [];
|
|
41
55
|
// Collect preamble lines until first requirement header
|
|
42
|
-
while (cursor < sectionBodyLines.length && !
|
|
56
|
+
while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor)) {
|
|
43
57
|
preambleLines.push(sectionBodyLines[cursor]);
|
|
44
58
|
cursor++;
|
|
45
59
|
}
|
|
46
60
|
while (cursor < sectionBodyLines.length) {
|
|
47
|
-
const headerStart = cursor;
|
|
48
61
|
const headerLineCandidate = sectionBodyLines[cursor];
|
|
49
|
-
|
|
50
|
-
if (!headerMatch) {
|
|
62
|
+
if (!isRequirementHeader(cursor)) {
|
|
51
63
|
// Not a requirement header; skip line defensively
|
|
52
64
|
cursor++;
|
|
53
65
|
continue;
|
|
54
66
|
}
|
|
67
|
+
const headerMatch = headerLineCandidate.match(REQUIREMENT_HEADER_REGEX);
|
|
55
68
|
const name = normalizeRequirementName(headerMatch[1]);
|
|
56
69
|
cursor++;
|
|
57
70
|
// Gather lines until next requirement header or end of section
|
|
58
71
|
const bodyLines = [headerLineCandidate];
|
|
59
|
-
while (cursor < sectionBodyLines.length && !
|
|
72
|
+
while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor) && !isTopLevelHeader(cursor)) {
|
|
60
73
|
bodyLines.push(sectionBodyLines[cursor]);
|
|
61
74
|
cursor++;
|
|
62
75
|
}
|
|
@@ -74,14 +87,18 @@ export function extractRequirementsSection(content) {
|
|
|
74
87
|
};
|
|
75
88
|
}
|
|
76
89
|
function normalizeLineEndings(content) {
|
|
77
|
-
|
|
90
|
+
// Strip a UTF-8 BOM: Windows editors and PowerShell redirects prepend one,
|
|
91
|
+
// and it would keep the first line's `## ADDED Requirements` from matching.
|
|
92
|
+
return content.replace(/^/, '').replace(/\r\n?/g, '\n');
|
|
78
93
|
}
|
|
79
94
|
/**
|
|
80
95
|
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
81
96
|
*/
|
|
82
97
|
export function parseDeltaSpec(content) {
|
|
83
98
|
const normalized = normalizeLineEndings(content);
|
|
84
|
-
const
|
|
99
|
+
const lines = normalized.split('\n');
|
|
100
|
+
const fenceMask = buildCodeFenceMask(lines);
|
|
101
|
+
const sections = splitTopLevelSections(lines, fenceMask);
|
|
85
102
|
const addedLookup = getSectionCaseInsensitive(sections, 'ADDED Requirements');
|
|
86
103
|
const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
|
|
87
104
|
const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
|
|
@@ -114,43 +131,45 @@ export function parseDeltaSpec(content) {
|
|
|
114
131
|
},
|
|
115
132
|
};
|
|
116
133
|
}
|
|
117
|
-
function splitTopLevelSections(
|
|
118
|
-
const lines = content.split('\n');
|
|
134
|
+
function splitTopLevelSections(lines, fenceMask) {
|
|
119
135
|
const result = {};
|
|
120
136
|
const indices = [];
|
|
121
137
|
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
if (fenceMask[i])
|
|
139
|
+
continue;
|
|
122
140
|
const m = lines[i].match(/^(##)\s+(.+)$/);
|
|
123
141
|
if (m) {
|
|
124
|
-
|
|
125
|
-
indices.push({ title: m[2].trim(), index: i, level });
|
|
142
|
+
indices.push({ title: m[2].trim(), index: i });
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
for (let i = 0; i < indices.length; i++) {
|
|
129
146
|
const current = indices[i];
|
|
130
147
|
const next = indices[i + 1];
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
const end = next ? next.index : lines.length;
|
|
149
|
+
result[current.title] = {
|
|
150
|
+
lines: lines.slice(current.index + 1, end),
|
|
151
|
+
fenceMask: fenceMask.slice(current.index + 1, end),
|
|
152
|
+
bodyStartLine: current.index + 2,
|
|
153
|
+
};
|
|
134
154
|
}
|
|
135
155
|
return result;
|
|
136
156
|
}
|
|
157
|
+
const EMPTY_SECTION_BODY = { lines: [], fenceMask: [], bodyStartLine: 0 };
|
|
137
158
|
function getSectionCaseInsensitive(sections, desired) {
|
|
138
159
|
const target = desired.toLowerCase();
|
|
139
|
-
for (const [title,
|
|
140
|
-
if (title.toLowerCase() === target)
|
|
141
|
-
return { title, body, bodyStartLine, found: true };
|
|
160
|
+
for (const [title, body] of Object.entries(sections)) {
|
|
161
|
+
if (title.toLowerCase() === target) {
|
|
162
|
+
return { title, body, bodyStartLine: body.bodyStartLine, found: true };
|
|
163
|
+
}
|
|
142
164
|
}
|
|
143
|
-
return { title: desired, body:
|
|
165
|
+
return { title: desired, body: EMPTY_SECTION_BODY, bodyStartLine: 0, found: false };
|
|
144
166
|
}
|
|
145
167
|
function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
146
|
-
|
|
168
|
+
const { lines, fenceMask } = sectionBody;
|
|
169
|
+
if (lines.length === 0)
|
|
147
170
|
return [];
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
// it skips them, so the INFO note describes the reader's real boundaries.
|
|
151
|
-
// Fence-masked lines are excluded: the body reader treats them as fenced
|
|
152
|
-
// content, not as headers.
|
|
153
|
-
const fenceMask = skipped ? buildCodeFenceMask(lines) : undefined;
|
|
171
|
+
const isRequirementHeader = (i) => !fenceMask[i] && REQUIREMENT_HEADER_REGEX.test(lines[i]);
|
|
172
|
+
const isTopLevelHeader = (i) => !fenceMask[i] && /^##\s+/.test(lines[i]);
|
|
154
173
|
const recordIfSkippedHeader = (index) => {
|
|
155
174
|
if (!skipped || fenceMask[index])
|
|
156
175
|
return;
|
|
@@ -167,7 +186,7 @@ function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
|
167
186
|
let i = 0;
|
|
168
187
|
while (i < lines.length) {
|
|
169
188
|
// Seek next requirement header
|
|
170
|
-
while (i < lines.length && !
|
|
189
|
+
while (i < lines.length && !isRequirementHeader(i)) {
|
|
171
190
|
recordIfSkippedHeader(i);
|
|
172
191
|
i++;
|
|
173
192
|
}
|
|
@@ -182,7 +201,7 @@ function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
|
182
201
|
const name = normalizeRequirementName(m[1]);
|
|
183
202
|
const buf = [headerLine];
|
|
184
203
|
i++;
|
|
185
|
-
while (i < lines.length && !
|
|
204
|
+
while (i < lines.length && !isRequirementHeader(i) && !isTopLevelHeader(i)) {
|
|
186
205
|
recordIfSkippedHeader(i);
|
|
187
206
|
buf.push(lines[i]);
|
|
188
207
|
i++;
|
|
@@ -192,11 +211,14 @@ function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
|
192
211
|
return blocks;
|
|
193
212
|
}
|
|
194
213
|
function parseRemovedNames(sectionBody) {
|
|
195
|
-
|
|
214
|
+
const { lines, fenceMask } = sectionBody;
|
|
215
|
+
if (lines.length === 0)
|
|
196
216
|
return [];
|
|
197
217
|
const names = [];
|
|
198
|
-
|
|
199
|
-
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
if (fenceMask[i])
|
|
220
|
+
continue;
|
|
221
|
+
const line = lines[i];
|
|
200
222
|
const m = line.match(REQUIREMENT_HEADER_REGEX);
|
|
201
223
|
if (m) {
|
|
202
224
|
names.push(normalizeRequirementName(m[1]));
|
|
@@ -211,12 +233,15 @@ function parseRemovedNames(sectionBody) {
|
|
|
211
233
|
return names;
|
|
212
234
|
}
|
|
213
235
|
function parseRenamedPairs(sectionBody) {
|
|
214
|
-
|
|
236
|
+
const { lines, fenceMask } = sectionBody;
|
|
237
|
+
if (lines.length === 0)
|
|
215
238
|
return [];
|
|
216
239
|
const pairs = [];
|
|
217
|
-
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
218
240
|
let current = {};
|
|
219
|
-
for (
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
if (fenceMask[i])
|
|
243
|
+
continue;
|
|
244
|
+
const line = lines[i];
|
|
220
245
|
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
221
246
|
const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
222
247
|
if (fromMatch) {
|
|
@@ -8,14 +8,7 @@
|
|
|
8
8
|
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
9
|
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
* Build a per-line mask marking lines that fall inside a fenced code block
|
|
13
|
-
* (``` ``` ``` or ``` ~~~ ```), including the fence lines themselves. Mirrors the
|
|
14
|
-
* fence rules markdown uses: a fence opens on the first ```` ```/~~~ ```` of
|
|
15
|
-
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
-
* opening length, with nothing but whitespace after it.
|
|
17
|
-
*/
|
|
18
|
-
export declare function buildCodeFenceMask(lines: string[]): boolean[];
|
|
11
|
+
export { buildCodeFenceMask } from './code-fence.js';
|
|
19
12
|
/**
|
|
20
13
|
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
21
14
|
* as whole words so the change-delta reader and the schema-based reader accept
|
|
@@ -8,48 +8,10 @@
|
|
|
8
8
|
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
9
|
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
-
* opening length, with nothing but whitespace after it.
|
|
17
|
-
*/
|
|
18
|
-
export function buildCodeFenceMask(lines) {
|
|
19
|
-
const mask = new Array(lines.length).fill(false);
|
|
20
|
-
let activeFence = null;
|
|
21
|
-
for (let i = 0; i < lines.length; i++) {
|
|
22
|
-
const fence = getFenceMarker(lines[i]);
|
|
23
|
-
if (!activeFence) {
|
|
24
|
-
if (fence) {
|
|
25
|
-
activeFence = fence;
|
|
26
|
-
mask[i] = true;
|
|
27
|
-
}
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
mask[i] = true;
|
|
31
|
-
if (isClosingFence(lines[i], activeFence)) {
|
|
32
|
-
activeFence = null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return mask;
|
|
36
|
-
}
|
|
37
|
-
function getFenceMarker(line) {
|
|
38
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
|
|
39
|
-
if (!fenceMatch) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
marker: fenceMatch[1][0],
|
|
44
|
-
length: fenceMatch[1].length,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function isClosingFence(line, activeFence) {
|
|
48
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
49
|
-
return Boolean(fenceMatch &&
|
|
50
|
-
fenceMatch[1][0] === activeFence.marker &&
|
|
51
|
-
fenceMatch[1].length >= activeFence.length);
|
|
52
|
-
}
|
|
11
|
+
// Re-exported so existing importers keep working; the single implementation
|
|
12
|
+
// lives in code-fence.ts.
|
|
13
|
+
export { buildCodeFenceMask } from './code-fence.js';
|
|
14
|
+
import { buildCodeFenceMask } from './code-fence.js';
|
|
53
15
|
/** Lines that look like `**ID**: ...` / `**Priority**: ...` metadata. */
|
|
54
16
|
const METADATA_LINE = /^\*\*[^*]+\*\*:/;
|
|
55
17
|
/** Any markdown header line — the boundary where a requirement body ends. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from './code-fence.js';
|
|
1
2
|
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
2
3
|
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
3
4
|
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
@@ -55,34 +56,7 @@ export function findMainSpecStructureIssues(content) {
|
|
|
55
56
|
}
|
|
56
57
|
export function stripFencedCodeBlocksPreservingLines(content) {
|
|
57
58
|
const lines = content.split('\n');
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
62
|
-
if (!activeFence) {
|
|
63
|
-
if (fenceMatch) {
|
|
64
|
-
activeFence = {
|
|
65
|
-
marker: fenceMatch[1][0],
|
|
66
|
-
length: fenceMatch[1].length,
|
|
67
|
-
};
|
|
68
|
-
output.push('');
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
output.push(line);
|
|
72
|
-
}
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
output.push('');
|
|
76
|
-
if (isClosingFence(line, activeFence)) {
|
|
77
|
-
activeFence = null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return output.join('\n');
|
|
81
|
-
}
|
|
82
|
-
function isClosingFence(line, activeFence) {
|
|
83
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
84
|
-
return Boolean(fenceMatch &&
|
|
85
|
-
fenceMatch[1][0] === activeFence.marker &&
|
|
86
|
-
fenceMatch[1].length >= activeFence.length);
|
|
59
|
+
const mask = buildCodeFenceMask(lines);
|
|
60
|
+
return lines.map((line, i) => (mask[i] ? '' : line)).join('\n');
|
|
87
61
|
}
|
|
88
62
|
//# sourceMappingURL=spec-structure.js.map
|
|
@@ -5,14 +5,6 @@ type WorkflowId = (typeof ALL_WORKFLOWS)[number];
|
|
|
5
5
|
* Maps workflow IDs to their skill directory names.
|
|
6
6
|
*/
|
|
7
7
|
export declare const WORKFLOW_TO_SKILL_DIR: Record<WorkflowId, string>;
|
|
8
|
-
/**
|
|
9
|
-
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
10
|
-
*/
|
|
11
|
-
export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Returns tools with at least one generated command file on disk.
|
|
14
|
-
*/
|
|
15
|
-
export declare function getCommandConfiguredTools(projectPath: string): string[];
|
|
16
8
|
/**
|
|
17
9
|
* Returns tools that are configured via either skills or commands.
|
|
18
10
|
*/
|
|
@@ -3,7 +3,8 @@ import * as fs from 'fs';
|
|
|
3
3
|
import { AI_TOOLS } from './config.js';
|
|
4
4
|
import { ALL_WORKFLOWS } from './profiles.js';
|
|
5
5
|
import { CommandAdapterRegistry } from './command-generation/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getConfiguredTools } from './shared/index.js';
|
|
7
|
+
import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
7
8
|
/**
|
|
8
9
|
* Maps workflow IDs to their skill directory names.
|
|
9
10
|
*/
|
|
@@ -24,48 +25,11 @@ export const WORKFLOW_TO_SKILL_DIR = {
|
|
|
24
25
|
function toKnownWorkflows(workflows) {
|
|
25
26
|
return workflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
26
27
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
29
|
-
*/
|
|
30
|
-
export function toolHasAnyConfiguredCommand(projectPath, toolId) {
|
|
31
|
-
const adapter = CommandAdapterRegistry.get(toolId);
|
|
32
|
-
if (!adapter)
|
|
33
|
-
return false;
|
|
34
|
-
for (const commandId of COMMAND_IDS) {
|
|
35
|
-
const cmdPath = adapter.getFilePath(commandId);
|
|
36
|
-
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
37
|
-
if (fs.existsSync(fullPath)) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Returns tools with at least one generated command file on disk.
|
|
45
|
-
*/
|
|
46
|
-
export function getCommandConfiguredTools(projectPath) {
|
|
47
|
-
return AI_TOOLS
|
|
48
|
-
.filter((tool) => {
|
|
49
|
-
if (!tool.skillsDir)
|
|
50
|
-
return false;
|
|
51
|
-
const toolDir = path.join(projectPath, tool.skillsDir);
|
|
52
|
-
try {
|
|
53
|
-
return fs.statSync(toolDir).isDirectory();
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
.map((tool) => tool.value)
|
|
60
|
-
.filter((toolId) => toolHasAnyConfiguredCommand(projectPath, toolId));
|
|
61
|
-
}
|
|
62
28
|
/**
|
|
63
29
|
* Returns tools that are configured via either skills or commands.
|
|
64
30
|
*/
|
|
65
31
|
export function getConfiguredToolsForProfileSync(projectPath) {
|
|
66
|
-
|
|
67
|
-
const commandConfigured = getCommandConfiguredTools(projectPath);
|
|
68
|
-
return [...new Set([...skillConfigured, ...commandConfigured])];
|
|
32
|
+
return getConfiguredTools(projectPath);
|
|
69
33
|
}
|
|
70
34
|
/**
|
|
71
35
|
* Detects if a single tool has profile/delivery drift against the desired state.
|
|
@@ -83,8 +47,8 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
|
|
|
83
47
|
const desiredWorkflowSet = new Set(knownDesiredWorkflows);
|
|
84
48
|
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
85
49
|
const adapter = CommandAdapterRegistry.get(toolId);
|
|
86
|
-
const shouldGenerateSkills = delivery
|
|
87
|
-
const shouldGenerateCommands = delivery
|
|
50
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(toolId, delivery);
|
|
51
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(toolId, delivery);
|
|
88
52
|
if (shouldGenerateSkills) {
|
|
89
53
|
for (const workflow of knownDesiredWorkflows) {
|
|
90
54
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
@@ -104,7 +68,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
|
|
|
104
68
|
}
|
|
105
69
|
}
|
|
106
70
|
}
|
|
107
|
-
else {
|
|
71
|
+
else if (shouldRemoveSkillsForTool(toolId, delivery)) {
|
|
108
72
|
for (const workflow of ALL_WORKFLOWS) {
|
|
109
73
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
110
74
|
const skillDir = path.join(skillsDir, dirName);
|
|
@@ -132,7 +96,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
|
|
|
132
96
|
}
|
|
133
97
|
}
|
|
134
98
|
}
|
|
135
|
-
else if (
|
|
99
|
+
else if (shouldReconcileCommandFilesForTool(toolId, delivery) && adapter) {
|
|
136
100
|
for (const workflow of ALL_WORKFLOWS) {
|
|
137
101
|
const cmdPath = adapter.getFilePath(workflow);
|
|
138
102
|
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
@@ -188,9 +152,9 @@ export function hasProjectConfigDrift(projectPath, desiredWorkflows, delivery) {
|
|
|
188
152
|
return true;
|
|
189
153
|
}
|
|
190
154
|
const desiredSet = new Set(toKnownWorkflows(desiredWorkflows));
|
|
191
|
-
const includeSkills = delivery !== 'commands';
|
|
192
|
-
const includeCommands = delivery !== 'skills';
|
|
193
155
|
for (const toolId of configuredTools) {
|
|
156
|
+
const includeSkills = shouldGenerateSkillsForTool(toolId, delivery);
|
|
157
|
+
const includeCommands = shouldGenerateCommandsForTool(toolId, delivery);
|
|
194
158
|
const installed = getInstalledWorkflowsForTool(projectPath, toolId, { includeSkills, includeCommands });
|
|
195
159
|
if (installed.some((workflow) => !desiredSet.has(workflow))) {
|
|
196
160
|
return true;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare const OPERATION_IDS: readonly ["apply", "archive"];
|
|
3
|
+
export type OperationId = (typeof OPERATION_IDS)[number];
|
|
4
|
+
export interface OperationConfig {
|
|
5
|
+
guidance?: string[];
|
|
6
|
+
}
|
|
7
|
+
export type OperationsConfig = Partial<Record<OperationId, OperationConfig>>;
|
|
2
8
|
/**
|
|
3
9
|
* Zod schema for project configuration.
|
|
4
10
|
*
|
|
@@ -16,6 +22,14 @@ export declare const ProjectConfigSchema: z.ZodObject<{
|
|
|
16
22
|
schema: z.ZodString;
|
|
17
23
|
context: z.ZodOptional<z.ZodString>;
|
|
18
24
|
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
25
|
+
operations: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
apply: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
archive: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
19
33
|
store: z.ZodOptional<z.ZodString>;
|
|
20
34
|
}, z.core.$strip>;
|
|
21
35
|
/** Normalized in-memory shape of a referenced store declaration. */
|
|
@@ -27,6 +41,11 @@ export interface DeclarationEntry {
|
|
|
27
41
|
export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
|
|
28
42
|
references?: DeclarationEntry[];
|
|
29
43
|
};
|
|
44
|
+
export interface OperationInputs {
|
|
45
|
+
context?: string;
|
|
46
|
+
operationGuidance?: string[];
|
|
47
|
+
}
|
|
48
|
+
export declare function loadOperationInputs(projectConfig: ProjectConfig | null, operationId: OperationId): OperationInputs;
|
|
30
49
|
export declare const MAX_CONTEXT_SIZE: number;
|
|
31
50
|
/**
|
|
32
51
|
* Read and parse openspec/config.yaml from project root.
|
|
@@ -49,16 +68,16 @@ export declare const MAX_CONTEXT_SIZE: number;
|
|
|
49
68
|
*/
|
|
50
69
|
export declare function readProjectConfig(projectRoot: string): ProjectConfig | null;
|
|
51
70
|
/**
|
|
52
|
-
* Validate artifact IDs in rules against
|
|
53
|
-
*
|
|
54
|
-
*
|
|
71
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
72
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
73
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
74
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
55
75
|
*
|
|
56
76
|
* @param rules - The rules object from config
|
|
57
|
-
* @param validArtifactIds - Set of valid artifact IDs
|
|
58
|
-
* @param schemaName - Name of the schema for error messages
|
|
77
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
59
78
|
* @returns Array of warning messages for unknown artifact IDs
|
|
60
79
|
*/
|
|
61
|
-
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string
|
|
80
|
+
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>): string[];
|
|
62
81
|
/**
|
|
63
82
|
* Suggest valid schema names when user provides invalid schema.
|
|
64
83
|
* Uses fuzzy matching to find similar names.
|
|
@@ -2,6 +2,10 @@ import { existsSync, readFileSync, statSync } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
export const OPERATION_IDS = ['apply', 'archive'];
|
|
6
|
+
const OperationConfigSchema = z.object({
|
|
7
|
+
guidance: z.array(z.string()).optional(),
|
|
8
|
+
});
|
|
5
9
|
/**
|
|
6
10
|
* Zod schema for project configuration.
|
|
7
11
|
*
|
|
@@ -34,6 +38,14 @@ export const ProjectConfigSchema = z.object({
|
|
|
34
38
|
)
|
|
35
39
|
.optional()
|
|
36
40
|
.describe('Per-artifact rules, keyed by artifact ID'),
|
|
41
|
+
// Optional: per-operation advisory guidance, kept separate from artifact rules.
|
|
42
|
+
operations: z
|
|
43
|
+
.object({
|
|
44
|
+
apply: OperationConfigSchema.optional(),
|
|
45
|
+
archive: OperationConfigSchema.optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Per-operation advisory guidance'),
|
|
37
49
|
// Note: the `references` field (id strings or {id, remote} maps) is
|
|
38
50
|
// deliberately absent here — readProjectConfig parses and normalizes
|
|
39
51
|
// it by hand (see DeclarationEntry below); a schema entry nothing
|
|
@@ -46,6 +58,60 @@ export const ProjectConfigSchema = z.object({
|
|
|
46
58
|
.optional()
|
|
47
59
|
.describe('Store id used as the OpenSpec root when no local planning shape exists'),
|
|
48
60
|
});
|
|
61
|
+
export function loadOperationInputs(projectConfig, operationId) {
|
|
62
|
+
const context = projectConfig?.context !== undefined && projectConfig.context.trim().length > 0
|
|
63
|
+
? projectConfig.context
|
|
64
|
+
: undefined;
|
|
65
|
+
const guidance = projectConfig?.operations?.[operationId]?.guidance;
|
|
66
|
+
const operationGuidance = guidance && guidance.length > 0 ? guidance : undefined;
|
|
67
|
+
return {
|
|
68
|
+
...(context !== undefined ? { context } : {}),
|
|
69
|
+
...(operationGuidance !== undefined ? { operationGuidance } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function parseOperations(raw) {
|
|
73
|
+
if (raw === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
77
|
+
console.warn(`Invalid 'operations' field in config (must be object)`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const supported = new Set(OPERATION_IDS);
|
|
81
|
+
const operations = {};
|
|
82
|
+
for (const [operationId, value] of Object.entries(raw)) {
|
|
83
|
+
if (!supported.has(operationId)) {
|
|
84
|
+
console.warn(`Unknown operation ID '${operationId}' in config. Supported operation IDs: ${OPERATION_IDS.join(', ')}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const typedOperationId = operationId;
|
|
88
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
89
|
+
console.warn(`Invalid 'operations.${operationId}' field in config (must be object), ignoring this operation`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const operation = value;
|
|
93
|
+
const unknownFields = Object.keys(operation).filter((field) => field !== 'guidance');
|
|
94
|
+
if (unknownFields.length > 0) {
|
|
95
|
+
console.warn(`Unknown field(s) in 'operations.${operationId}': ${unknownFields.join(', ')}. Supported fields: guidance`);
|
|
96
|
+
}
|
|
97
|
+
if (operation.guidance === undefined) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const guidanceResult = z.array(z.string()).safeParse(operation.guidance);
|
|
101
|
+
if (!guidanceResult.success) {
|
|
102
|
+
console.warn(`Guidance for operation '${operationId}' must be an array of strings, ignoring this operation's guidance`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const guidance = guidanceResult.data.filter((entry) => entry.length > 0);
|
|
106
|
+
if (guidance.length < guidanceResult.data.length) {
|
|
107
|
+
console.warn(`Some guidance for operation '${operationId}' are empty strings, ignoring them`);
|
|
108
|
+
}
|
|
109
|
+
if (guidance.length > 0) {
|
|
110
|
+
operations[typedOperationId] = { guidance };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return Object.keys(operations).length > 0 ? operations : undefined;
|
|
114
|
+
}
|
|
49
115
|
/**
|
|
50
116
|
* Parser for `references:` declarations: string entries or
|
|
51
117
|
* {id, remote} maps, normalized to DeclarationEntry[]. Dedup keys on
|
|
@@ -196,6 +262,10 @@ export function readProjectConfig(projectRoot) {
|
|
|
196
262
|
console.warn(`Invalid 'rules' field in config (must be object)`);
|
|
197
263
|
}
|
|
198
264
|
}
|
|
265
|
+
const operations = parseOperations(raw.operations);
|
|
266
|
+
if (operations) {
|
|
267
|
+
config.operations = operations;
|
|
268
|
+
}
|
|
199
269
|
const references = parseDeclarationList(raw.references);
|
|
200
270
|
if (references) {
|
|
201
271
|
config.references = references;
|
|
@@ -223,22 +293,22 @@ function configPathForWarnings(projectRoot) {
|
|
|
223
293
|
return resolveConfigFilePath(projectRoot) ?? path.join(projectRoot, 'openspec', 'config.yaml');
|
|
224
294
|
}
|
|
225
295
|
/**
|
|
226
|
-
* Validate artifact IDs in rules against
|
|
227
|
-
*
|
|
228
|
-
*
|
|
296
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
297
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
298
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
299
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
229
300
|
*
|
|
230
301
|
* @param rules - The rules object from config
|
|
231
|
-
* @param validArtifactIds - Set of valid artifact IDs
|
|
232
|
-
* @param schemaName - Name of the schema for error messages
|
|
302
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
233
303
|
* @returns Array of warning messages for unknown artifact IDs
|
|
234
304
|
*/
|
|
235
|
-
export function validateConfigRules(rules, validArtifactIds
|
|
305
|
+
export function validateConfigRules(rules, validArtifactIds) {
|
|
236
306
|
const warnings = [];
|
|
237
307
|
for (const artifactId of Object.keys(rules)) {
|
|
238
308
|
if (!validArtifactIds.has(artifactId)) {
|
|
239
309
|
const validIds = Array.from(validArtifactIds).sort().join(', ');
|
|
240
310
|
warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
|
|
241
|
-
`
|
|
311
|
+
`It matches no artifact in any available schema. Known artifact IDs: ${validIds}`);
|
|
242
312
|
}
|
|
243
313
|
}
|
|
244
314
|
return warnings;
|