@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
package/dist/core/archive.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { formatLocalDate } from '../utils/date.js';
|
|
3
4
|
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
5
|
import { Validator } from './validation/validator.js';
|
|
5
6
|
import chalk from 'chalk';
|
|
6
7
|
import { emitStoreRootBanner, isRootSelectionError, resolveOpenSpecRoot, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from './root-selection.js';
|
|
7
8
|
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
9
|
+
import { discoverSpecFiles, hasAnyFileUnder } from '../utils/spec-discovery.js';
|
|
10
|
+
import { readSkipSpecsMarker } from '../utils/change-metadata.js';
|
|
8
11
|
function isMissingPathError(error) {
|
|
9
12
|
return (typeof error === 'object' &&
|
|
10
13
|
error !== null &&
|
|
11
14
|
'code' in error &&
|
|
12
15
|
error.code === 'ENOENT');
|
|
13
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Matches the `YYYY-MM-DD-` prefix that archiving prepends to a change name.
|
|
19
|
+
* A change whose name already starts with one (a common authoring convention)
|
|
20
|
+
* is archived under its existing name so the prefix is never stacked (#1309).
|
|
21
|
+
*/
|
|
22
|
+
const ARCHIVE_DATE_PREFIX_PATTERN = /^\d{4}-\d{2}-\d{2}-/;
|
|
14
23
|
async function listActiveChangeNames(changesDir) {
|
|
15
24
|
try {
|
|
16
25
|
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
@@ -181,10 +190,21 @@ export class ArchiveCommand {
|
|
|
181
190
|
try {
|
|
182
191
|
await fs.access(changeFile);
|
|
183
192
|
const changeReport = await validator.validateChange(changeFile);
|
|
184
|
-
// Proposal validation is informative only (do not block archive)
|
|
185
|
-
|
|
193
|
+
// Proposal validation is informative only (do not block archive).
|
|
194
|
+
// `validateChange` parses the change together with its delta specs,
|
|
195
|
+
// so it also raises requirement-level issues under
|
|
196
|
+
// `deltas.<n>.requirement(s)`. Those
|
|
197
|
+
// are not proposal problems, and reporting them here was noisy and
|
|
198
|
+
// sometimes wrong (#498): the change parser records every requirement
|
|
199
|
+
// under both `requirement` and `requirements`, so each defect was
|
|
200
|
+
// printed twice, and REMOVED requirements — names-only by design —
|
|
201
|
+
// produced a "missing scenario" warning for a correct removal.
|
|
202
|
+
// Genuine delta defects are still caught below, by the delta spec
|
|
203
|
+
// validation and by the rebuilt-spec check that runs before any write.
|
|
204
|
+
const proposalIssues = changeReport.issues.filter((issue) => !/^deltas\.\d+\.requirements?\./.test(issue.path));
|
|
205
|
+
if (!changeReport.valid && proposalIssues.length > 0) {
|
|
186
206
|
console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
|
|
187
|
-
for (const issue of
|
|
207
|
+
for (const issue of proposalIssues) {
|
|
188
208
|
const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
|
|
189
209
|
console.log(chalk.yellow(` ${symbol} ${issue.message}`));
|
|
190
210
|
}
|
|
@@ -196,25 +216,52 @@ export class ArchiveCommand {
|
|
|
196
216
|
}
|
|
197
217
|
// Validate delta-formatted spec files under the change directory if present
|
|
198
218
|
const changeSpecsDir = path.join(changeDir, 'specs');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
// A spec.md at the specs/ root is never merged, so archiving a change
|
|
220
|
+
// that has one drops its content whether or not it carries delta headers
|
|
221
|
+
// (#1385). Its existence alone must run validation, which reports it and
|
|
222
|
+
// blocks the archive. A directory named spec.md is a normal capability
|
|
223
|
+
// folder, so only a regular file counts.
|
|
224
|
+
const rootSpecStat = await fs.stat(path.join(changeSpecsDir, 'spec.md')).catch(() => null);
|
|
225
|
+
let hasDeltaSpecs = rootSpecStat?.isFile() === true;
|
|
226
|
+
// A change that declares skip_specs must not carry any file under
|
|
227
|
+
// specs/ — validate reports that as a conflict, so archive has to run
|
|
228
|
+
// the same check instead of skipping validation because the files
|
|
229
|
+
// happen to have no delta headers. A marker that cannot be honored
|
|
230
|
+
// (skip_specs mentioned but the metadata fails the shared shape, or
|
|
231
|
+
// names a schema that does not resolve) also
|
|
232
|
+
// forces validation, so archive and validate always agree about the
|
|
233
|
+
// marker. Unreadable specs/ fails closed into validation too. (An
|
|
234
|
+
// UNMARKED zero-delta change still archives with only non-blocking
|
|
235
|
+
// proposal warnings — a gap that predates the marker and is left
|
|
236
|
+
// unchanged here.)
|
|
237
|
+
if (!hasDeltaSpecs) {
|
|
238
|
+
const marker = readSkipSpecsMarker(changeDir);
|
|
239
|
+
if (marker.invalidReason) {
|
|
240
|
+
hasDeltaSpecs = true;
|
|
241
|
+
}
|
|
242
|
+
else if (marker.declared) {
|
|
243
|
+
let specsDirHasFiles = true;
|
|
244
|
+
try {
|
|
245
|
+
specsDirHasFiles = await hasAnyFileUnder(changeSpecsDir);
|
|
214
246
|
}
|
|
247
|
+
catch {
|
|
248
|
+
// fall through with true: let validation surface the conflict
|
|
249
|
+
}
|
|
250
|
+
hasDeltaSpecs = specsDirHasFiles;
|
|
215
251
|
}
|
|
216
252
|
}
|
|
217
|
-
|
|
253
|
+
for (const { specFile } of hasDeltaSpecs ? [] : await discoverSpecFiles(changeSpecsDir)) {
|
|
254
|
+
try {
|
|
255
|
+
const content = await fs.readFile(specFile, 'utf-8');
|
|
256
|
+
// Case-insensitive to match the delta parser, so a lowercase header
|
|
257
|
+
// routes through the same delta validation that validate runs.
|
|
258
|
+
if (/^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements/im.test(content)) {
|
|
259
|
+
hasDeltaSpecs = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch { }
|
|
264
|
+
}
|
|
218
265
|
if (hasDeltaSpecs) {
|
|
219
266
|
const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
|
|
220
267
|
if (!deltaReport.valid) {
|
|
@@ -298,6 +345,7 @@ export class ArchiveCommand {
|
|
|
298
345
|
// Handle spec updates unless skipSpecs flag is set
|
|
299
346
|
let specsUpdated = false;
|
|
300
347
|
let totals;
|
|
348
|
+
const specWarnings = [];
|
|
301
349
|
if (options.skipSpecs) {
|
|
302
350
|
if (!json) {
|
|
303
351
|
console.log('Skipping spec updates (--skip-specs flag provided).');
|
|
@@ -311,7 +359,7 @@ export class ArchiveCommand {
|
|
|
311
359
|
console.log('\nSpecs to update:');
|
|
312
360
|
for (const update of specUpdates) {
|
|
313
361
|
const status = update.exists ? 'update' : 'create';
|
|
314
|
-
const capability =
|
|
362
|
+
const capability = update.id;
|
|
315
363
|
console.log(` ${capability}: ${status}`);
|
|
316
364
|
}
|
|
317
365
|
}
|
|
@@ -336,6 +384,9 @@ export class ArchiveCommand {
|
|
|
336
384
|
for (const update of specUpdates) {
|
|
337
385
|
const built = await buildUpdatedSpec(update, changeName, { silent: json });
|
|
338
386
|
prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
|
|
387
|
+
// Carried into the result so JSON mode (where nothing was
|
|
388
|
+
// printed) still surfaces them; human mode discards the result.
|
|
389
|
+
specWarnings.push(...built.warnings);
|
|
339
390
|
}
|
|
340
391
|
}
|
|
341
392
|
catch (err) {
|
|
@@ -351,7 +402,7 @@ export class ArchiveCommand {
|
|
|
351
402
|
// late validation failure really does leave all targets unchanged.
|
|
352
403
|
if (!skipValidation) {
|
|
353
404
|
for (const p of prepared) {
|
|
354
|
-
const specName =
|
|
405
|
+
const specName = p.update.id;
|
|
355
406
|
const report = await new Validator().validateSpecContent(specName, p.rebuilt);
|
|
356
407
|
if (!report.valid) {
|
|
357
408
|
if (json) {
|
|
@@ -372,28 +423,43 @@ export class ArchiveCommand {
|
|
|
372
423
|
}
|
|
373
424
|
// All validations passed; write files and display counts
|
|
374
425
|
const writeTotals = { added: 0, modified: 0, removed: 0, renamed: 0 };
|
|
426
|
+
let wroteAny = false;
|
|
375
427
|
for (const p of prepared) {
|
|
428
|
+
const { added, modified, removed, renamed } = p.counts;
|
|
429
|
+
if (added + modified + removed + renamed === 0) {
|
|
430
|
+
// Every operation was already synced: rewriting the file would
|
|
431
|
+
// only churn normalization differences into it.
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
376
434
|
await writeUpdatedSpec(p.update, p.rebuilt, p.counts, {
|
|
377
435
|
silent: json,
|
|
378
436
|
// Cross-root paths must be absolute when a store is selected.
|
|
379
437
|
...(isStoreSelectedRoot(root) ? { displayPath: p.update.target } : {}),
|
|
380
438
|
});
|
|
381
|
-
|
|
382
|
-
writeTotals.
|
|
383
|
-
writeTotals.
|
|
384
|
-
writeTotals.
|
|
439
|
+
wroteAny = true;
|
|
440
|
+
writeTotals.added += added;
|
|
441
|
+
writeTotals.modified += modified;
|
|
442
|
+
writeTotals.removed += removed;
|
|
443
|
+
writeTotals.renamed += renamed;
|
|
385
444
|
}
|
|
386
|
-
specsUpdated =
|
|
445
|
+
specsUpdated = wroteAny;
|
|
387
446
|
totals = writeTotals;
|
|
388
447
|
if (!json) {
|
|
389
448
|
console.log(`Totals: + ${writeTotals.added}, ~ ${writeTotals.modified}, - ${writeTotals.removed}, → ${writeTotals.renamed}`);
|
|
390
|
-
console.log(
|
|
449
|
+
console.log(wroteAny
|
|
450
|
+
? 'Specs updated successfully.'
|
|
451
|
+
: 'Specs already in sync; no files changed.');
|
|
391
452
|
}
|
|
392
453
|
}
|
|
393
454
|
}
|
|
394
455
|
}
|
|
395
|
-
// Create archive directory with date prefix
|
|
396
|
-
|
|
456
|
+
// Create archive directory with date prefix. Names that already carry
|
|
457
|
+
// one keep it: re-prefixing would stutter the name, and when the archive
|
|
458
|
+
// runs on a later day the folder would sort under a day on which the
|
|
459
|
+
// change did not happen (#1309).
|
|
460
|
+
const archiveName = ARCHIVE_DATE_PREFIX_PATTERN.test(changeName)
|
|
461
|
+
? changeName
|
|
462
|
+
: `${formatLocalDate()}-${changeName}`;
|
|
397
463
|
const archivePath = path.join(archiveDir, archiveName);
|
|
398
464
|
// Check if archive already exists
|
|
399
465
|
let archiveExists = false;
|
|
@@ -422,6 +488,7 @@ export class ArchiveCommand {
|
|
|
422
488
|
path: archivePath,
|
|
423
489
|
specsUpdated,
|
|
424
490
|
...(totals ? { totals } : {}),
|
|
491
|
+
...(specWarnings.length > 0 ? { warnings: specWarnings } : {}),
|
|
425
492
|
};
|
|
426
493
|
}
|
|
427
494
|
async selectChange(changesDir) {
|
|
@@ -462,9 +529,5 @@ export class ArchiveCommand {
|
|
|
462
529
|
return null;
|
|
463
530
|
}
|
|
464
531
|
}
|
|
465
|
-
getArchiveDate() {
|
|
466
|
-
// Returns date in YYYY-MM-DD format
|
|
467
|
-
return new Date().toISOString().split('T')[0];
|
|
468
|
-
}
|
|
469
532
|
}
|
|
470
533
|
//# sourceMappingURL=archive.js.map
|
|
@@ -6,7 +6,23 @@ import type { Artifact, SchemaYaml, CompletedSet, BlockedArtifacts } from './typ
|
|
|
6
6
|
export declare class ArtifactGraph {
|
|
7
7
|
private artifacts;
|
|
8
8
|
private schema;
|
|
9
|
+
/** Artifact id -> its position in the schema's `artifacts:` list. */
|
|
10
|
+
private declarationOrder;
|
|
9
11
|
private constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Orders artifact ids by where the schema declares them.
|
|
14
|
+
*
|
|
15
|
+
* The dependency graph leaves siblings tied - spec-driven's `specs` and
|
|
16
|
+
* `design` both require only `proposal`, so both become ready at the same
|
|
17
|
+
* time. Ties used to be broken alphabetically, which put `design` ahead of
|
|
18
|
+
* `specs` and made the CLI recommend the artifacts in an order that
|
|
19
|
+
* contradicted the schema's own documented sequence
|
|
20
|
+
* (proposal -> specs -> design -> tasks). Breaking ties by declaration order
|
|
21
|
+
* follows the sequence the schema author wrote, for built-in and custom
|
|
22
|
+
* schemas alike, and stays just as deterministic. Ids not in the schema sort
|
|
23
|
+
* last so the comparator stays total.
|
|
24
|
+
*/
|
|
25
|
+
private compareByDeclarationOrder;
|
|
10
26
|
/**
|
|
11
27
|
* Creates an ArtifactGraph from a YAML file path.
|
|
12
28
|
*/
|
|
@@ -6,9 +6,29 @@ import { loadSchema, parseSchema } from './schema.js';
|
|
|
6
6
|
export class ArtifactGraph {
|
|
7
7
|
artifacts;
|
|
8
8
|
schema;
|
|
9
|
+
/** Artifact id -> its position in the schema's `artifacts:` list. */
|
|
10
|
+
declarationOrder;
|
|
9
11
|
constructor(schema) {
|
|
10
12
|
this.schema = schema;
|
|
11
13
|
this.artifacts = new Map(schema.artifacts.map(a => [a.id, a]));
|
|
14
|
+
this.declarationOrder = new Map(schema.artifacts.map((a, index) => [a.id, index]));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Orders artifact ids by where the schema declares them.
|
|
18
|
+
*
|
|
19
|
+
* The dependency graph leaves siblings tied - spec-driven's `specs` and
|
|
20
|
+
* `design` both require only `proposal`, so both become ready at the same
|
|
21
|
+
* time. Ties used to be broken alphabetically, which put `design` ahead of
|
|
22
|
+
* `specs` and made the CLI recommend the artifacts in an order that
|
|
23
|
+
* contradicted the schema's own documented sequence
|
|
24
|
+
* (proposal -> specs -> design -> tasks). Breaking ties by declaration order
|
|
25
|
+
* follows the sequence the schema author wrote, for built-in and custom
|
|
26
|
+
* schemas alike, and stays just as deterministic. Ids not in the schema sort
|
|
27
|
+
* last so the comparator stays total.
|
|
28
|
+
*/
|
|
29
|
+
compareByDeclarationOrder(a, b) {
|
|
30
|
+
return ((this.declarationOrder.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
31
|
+
(this.declarationOrder.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
12
32
|
}
|
|
13
33
|
/**
|
|
14
34
|
* Creates an ArtifactGraph from a YAML file path.
|
|
@@ -72,10 +92,10 @@ export class ArtifactGraph {
|
|
|
72
92
|
dependents.get(req).push(artifact.id);
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
|
-
// Start with roots (in-degree 0),
|
|
95
|
+
// Start with roots (in-degree 0), in declaration order for determinism
|
|
76
96
|
const queue = [...this.artifacts.keys()]
|
|
77
97
|
.filter(id => inDegree.get(id) === 0)
|
|
78
|
-
.sort();
|
|
98
|
+
.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
79
99
|
const result = [];
|
|
80
100
|
while (queue.length > 0) {
|
|
81
101
|
const current = queue.shift();
|
|
@@ -89,7 +109,10 @@ export class ArtifactGraph {
|
|
|
89
109
|
newlyReady.push(dep);
|
|
90
110
|
}
|
|
91
111
|
}
|
|
92
|
-
queue
|
|
112
|
+
// Re-sort the whole queue, not just the new arrivals: an artifact that
|
|
113
|
+
// has been waiting can be declared after one that just became ready.
|
|
114
|
+
queue.push(...newlyReady);
|
|
115
|
+
queue.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
93
116
|
}
|
|
94
117
|
return result;
|
|
95
118
|
}
|
|
@@ -107,8 +130,9 @@ export class ArtifactGraph {
|
|
|
107
130
|
ready.push(artifact.id);
|
|
108
131
|
}
|
|
109
132
|
}
|
|
110
|
-
//
|
|
111
|
-
|
|
133
|
+
// Declaration order: deterministic, and the first entry is the artifact the
|
|
134
|
+
// schema wants written next.
|
|
135
|
+
return ready.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
112
136
|
}
|
|
113
137
|
/**
|
|
114
138
|
* Checks if all artifacts in the graph are completed.
|
|
@@ -132,7 +156,7 @@ export class ArtifactGraph {
|
|
|
132
156
|
}
|
|
133
157
|
const unmetDeps = artifact.requires.filter(req => !completed.has(req));
|
|
134
158
|
if (unmetDeps.length > 0) {
|
|
135
|
-
blocked[artifact.id] = unmetDeps.sort();
|
|
159
|
+
blocked[artifact.id] = unmetDeps.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
136
160
|
}
|
|
137
161
|
}
|
|
138
162
|
return blocked;
|
|
@@ -32,10 +32,18 @@ export interface ChangeContext {
|
|
|
32
32
|
planningHome?: PlanningHome;
|
|
33
33
|
/** Parsed change metadata, when present */
|
|
34
34
|
metadata?: ChangeMetadata;
|
|
35
|
+
/**
|
|
36
|
+
* Artifact IDs counted as complete only because the change declares
|
|
37
|
+
* skip_specs, not because their files exist. Kept separate so status can
|
|
38
|
+
* render them as skipped rather than done.
|
|
39
|
+
*/
|
|
40
|
+
skippedArtifacts?: Set<string>;
|
|
35
41
|
}
|
|
36
42
|
export interface LoadChangeContextOptions {
|
|
37
43
|
changeDir?: string;
|
|
38
44
|
planningHome?: PlanningHome;
|
|
45
|
+
/** Pre-read project config; suppresses schema resolution's fallback config read. */
|
|
46
|
+
projectConfig?: ProjectConfig | null;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Enriched instructions for creating an artifact.
|
|
@@ -73,7 +81,17 @@ export interface ArtifactInstructions {
|
|
|
73
81
|
dependencies: DependencyInfo[];
|
|
74
82
|
/** Artifacts that become available after completing this one */
|
|
75
83
|
unlocks: string[];
|
|
84
|
+
/** True when the change declares skip_specs and this artifact is skipped */
|
|
85
|
+
skipped?: boolean;
|
|
86
|
+
/** Present only when skipped: tells the consumer not to create the artifact */
|
|
87
|
+
warning?: string;
|
|
76
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Warning attached to instructions for an artifact skipped via skip_specs.
|
|
91
|
+
* Carried in the JSON payload too, so agents driving the CLI with --json see
|
|
92
|
+
* the same do-not-create signal as the text output.
|
|
93
|
+
*/
|
|
94
|
+
export declare const SKIP_SPECS_INSTRUCTIONS_WARNING: string;
|
|
77
95
|
/**
|
|
78
96
|
* Dependency information including path and description.
|
|
79
97
|
*/
|
|
@@ -86,6 +104,8 @@ export interface DependencyInfo {
|
|
|
86
104
|
path: string;
|
|
87
105
|
/** Description of the dependency artifact */
|
|
88
106
|
description: string;
|
|
107
|
+
/** True when the dependency is satisfied via skip_specs - no files exist to read */
|
|
108
|
+
skipped?: boolean;
|
|
89
109
|
}
|
|
90
110
|
/**
|
|
91
111
|
* Status of a single artifact in the workflow.
|
|
@@ -95,8 +115,13 @@ export interface ArtifactStatus {
|
|
|
95
115
|
id: string;
|
|
96
116
|
/** Output path pattern */
|
|
97
117
|
outputPath: string;
|
|
98
|
-
/** Status: done, ready, or blocked */
|
|
99
|
-
status: 'done' | 'ready' | 'blocked';
|
|
118
|
+
/** Status: done, skipped (via skip_specs), ready, or blocked */
|
|
119
|
+
status: 'done' | 'skipped' | 'ready' | 'blocked';
|
|
120
|
+
/** Artifact IDs this artifact directly requires (its `requires` edges).
|
|
121
|
+
* Present for every status so callers can compute the transitive required
|
|
122
|
+
* set even when the artifact is already `done` (file-existence status does
|
|
123
|
+
* not imply its dependencies exist). */
|
|
124
|
+
requires: string[];
|
|
100
125
|
/** Missing dependencies (only for blocked) */
|
|
101
126
|
missingDeps?: string[];
|
|
102
127
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { getSchemaDir, resolveSchema } from './resolver.js';
|
|
3
|
+
import { getSchemaDir, resolveSchema, listSchemasWithInfo } from './resolver.js';
|
|
4
4
|
import { ArtifactGraph } from './graph.js';
|
|
5
5
|
import { detectCompleted } from './state.js';
|
|
6
6
|
import { resolveArtifactOutputs } from './outputs.js';
|
|
@@ -21,6 +21,13 @@ export class TemplateLoadError extends Error {
|
|
|
21
21
|
this.name = 'TemplateLoadError';
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Warning attached to instructions for an artifact skipped via skip_specs.
|
|
26
|
+
* Carried in the JSON payload too, so agents driving the CLI with --json see
|
|
27
|
+
* the same do-not-create signal as the text output.
|
|
28
|
+
*/
|
|
29
|
+
export const SKIP_SPECS_INSTRUCTIONS_WARNING = 'This change declares skip_specs: true in .openspec.yaml (no spec-level behavior changes), so this artifact is skipped.\n' +
|
|
30
|
+
'Do not create spec files - they will conflict with that marker. If requirements now change, remove skip_specs from .openspec.yaml and rerun this command.';
|
|
24
31
|
/**
|
|
25
32
|
* Loads a template from a schema's templates directory.
|
|
26
33
|
*
|
|
@@ -66,10 +73,29 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
|
|
|
66
73
|
const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
|
|
67
74
|
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
|
|
68
75
|
metadata: metadata ?? null,
|
|
76
|
+
projectConfig: options.projectConfig,
|
|
69
77
|
});
|
|
70
78
|
const schema = resolveSchema(resolvedSchemaName, projectRoot);
|
|
71
79
|
const graph = ArtifactGraph.fromSchema(schema);
|
|
72
80
|
const completed = detectCompleted(graph, changeDir);
|
|
81
|
+
// A change that declares skip_specs has no spec deltas by design, so
|
|
82
|
+
// artifacts generating into specs/ count as complete; otherwise the graph
|
|
83
|
+
// would block their dependents (e.g. tasks) on files that must not exist.
|
|
84
|
+
// Tracked separately so status renders them as skipped, not done.
|
|
85
|
+
const skippedArtifacts = new Set();
|
|
86
|
+
if (metadata?.skip_specs) {
|
|
87
|
+
for (const artifact of graph.getAllArtifacts()) {
|
|
88
|
+
// A schema may write generates as './specs/...' - the globs treat that
|
|
89
|
+
// identically to 'specs/...', so the skip set must too, or validate
|
|
90
|
+
// would honor the marker while instructions tell the agent to create
|
|
91
|
+
// the very files the conflict gate polices.
|
|
92
|
+
const generates = artifact.generates.replace(/^(?:\.\/)+/, '');
|
|
93
|
+
if (generates.startsWith('specs/') && !completed.has(artifact.id)) {
|
|
94
|
+
completed.add(artifact.id);
|
|
95
|
+
skippedArtifacts.add(artifact.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
73
99
|
return {
|
|
74
100
|
graph,
|
|
75
101
|
completed,
|
|
@@ -79,6 +105,7 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
|
|
|
79
105
|
projectRoot,
|
|
80
106
|
...(options.planningHome ? { planningHome: options.planningHome } : {}),
|
|
81
107
|
...(metadata ? { metadata } : {}),
|
|
108
|
+
...(skippedArtifacts.size > 0 ? { skippedArtifacts } : {}),
|
|
82
109
|
};
|
|
83
110
|
}
|
|
84
111
|
export function generateInstructions(context, artifactId, projectRoot, options = {}) {
|
|
@@ -87,7 +114,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
87
114
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
|
|
88
115
|
}
|
|
89
116
|
const templateContent = loadTemplate(context.schemaName, artifact.template, context.projectRoot);
|
|
90
|
-
const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
|
|
117
|
+
const dependencies = getDependencyInfo(artifact, context.graph, context.completed, context.skippedArtifacts);
|
|
91
118
|
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
|
92
119
|
// Use projectRoot from context if not explicitly provided
|
|
93
120
|
const effectiveProjectRoot = projectRoot ?? context.projectRoot;
|
|
@@ -101,10 +128,12 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
101
128
|
// If config read fails, continue without config
|
|
102
129
|
}
|
|
103
130
|
}
|
|
104
|
-
// Validate rules artifact IDs if config has rules (only once per session)
|
|
131
|
+
// Validate rules artifact IDs if config has rules (only once per session).
|
|
132
|
+
// The rules map is global while each change can use a different schema, so a
|
|
133
|
+
// key is only "unknown" when it matches no artifact in ANY available schema.
|
|
105
134
|
if (projectConfig?.rules) {
|
|
106
|
-
const validArtifactIds = new Set(
|
|
107
|
-
const warnings = validateConfigRules(projectConfig.rules, validArtifactIds
|
|
135
|
+
const validArtifactIds = new Set(listSchemasWithInfo(effectiveProjectRoot ?? undefined).flatMap((s) => s.artifacts));
|
|
136
|
+
const warnings = validateConfigRules(projectConfig.rules, validArtifactIds);
|
|
108
137
|
// Show each unique warning only once per session
|
|
109
138
|
for (const warning of warnings) {
|
|
110
139
|
if (!shownWarnings.has(warning)) {
|
|
@@ -131,6 +160,9 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
131
160
|
context: configContext,
|
|
132
161
|
rules: configRules,
|
|
133
162
|
...(options.references !== undefined ? { references: options.references } : {}),
|
|
163
|
+
...(context.skippedArtifacts?.has(artifact.id)
|
|
164
|
+
? { skipped: true, warning: SKIP_SPECS_INSTRUCTIONS_WARNING }
|
|
165
|
+
: {}),
|
|
134
166
|
template: templateContent,
|
|
135
167
|
dependencies,
|
|
136
168
|
unlocks,
|
|
@@ -139,7 +171,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
139
171
|
/**
|
|
140
172
|
* Gets dependency info including paths and descriptions.
|
|
141
173
|
*/
|
|
142
|
-
function getDependencyInfo(artifact, graph, completed) {
|
|
174
|
+
function getDependencyInfo(artifact, graph, completed, skippedArtifacts) {
|
|
143
175
|
return artifact.requires.map(id => {
|
|
144
176
|
const depArtifact = graph.getArtifact(id);
|
|
145
177
|
return {
|
|
@@ -147,11 +179,16 @@ function getDependencyInfo(artifact, graph, completed) {
|
|
|
147
179
|
done: completed.has(id),
|
|
148
180
|
path: depArtifact?.generates ?? id,
|
|
149
181
|
description: depArtifact?.description ?? '',
|
|
182
|
+
...(skippedArtifacts?.has(id) ? { skipped: true } : {}),
|
|
150
183
|
};
|
|
151
184
|
});
|
|
152
185
|
}
|
|
153
186
|
/**
|
|
154
187
|
* Gets artifacts that become available after completing the given artifact.
|
|
188
|
+
*
|
|
189
|
+
* `getAllArtifacts()` already yields the schema's declaration order, so the list
|
|
190
|
+
* is returned as collected: sorting it alphabetically would have `unlocks` name
|
|
191
|
+
* the artifacts in a different order than `status` recommends them.
|
|
155
192
|
*/
|
|
156
193
|
function getUnlockedArtifacts(graph, artifactId) {
|
|
157
194
|
const unlocks = [];
|
|
@@ -160,7 +197,7 @@ function getUnlockedArtifacts(graph, artifactId) {
|
|
|
160
197
|
unlocks.push(artifact.id);
|
|
161
198
|
}
|
|
162
199
|
}
|
|
163
|
-
return unlocks
|
|
200
|
+
return unlocks;
|
|
164
201
|
}
|
|
165
202
|
/**
|
|
166
203
|
* Formats the status of all artifacts in a change.
|
|
@@ -182,11 +219,20 @@ export function formatChangeStatus(context, options = {}) {
|
|
|
182
219
|
resolvedOutputPath: path.join(context.changeDir, artifact.generates),
|
|
183
220
|
existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
|
|
184
221
|
};
|
|
222
|
+
if (context.skippedArtifacts?.has(artifact.id)) {
|
|
223
|
+
return {
|
|
224
|
+
id: artifact.id,
|
|
225
|
+
outputPath: artifact.generates,
|
|
226
|
+
status: 'skipped',
|
|
227
|
+
requires: artifact.requires,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
185
230
|
if (context.completed.has(artifact.id)) {
|
|
186
231
|
return {
|
|
187
232
|
id: artifact.id,
|
|
188
233
|
outputPath: artifact.generates,
|
|
189
234
|
status: 'done',
|
|
235
|
+
requires: artifact.requires,
|
|
190
236
|
};
|
|
191
237
|
}
|
|
192
238
|
if (ready.has(artifact.id)) {
|
|
@@ -194,12 +240,14 @@ export function formatChangeStatus(context, options = {}) {
|
|
|
194
240
|
id: artifact.id,
|
|
195
241
|
outputPath: artifact.generates,
|
|
196
242
|
status: 'ready',
|
|
243
|
+
requires: artifact.requires,
|
|
197
244
|
};
|
|
198
245
|
}
|
|
199
246
|
return {
|
|
200
247
|
id: artifact.id,
|
|
201
248
|
outputPath: artifact.generates,
|
|
202
249
|
status: 'blocked',
|
|
250
|
+
requires: artifact.requires,
|
|
203
251
|
missingDeps: blocked[artifact.id] ?? [],
|
|
204
252
|
};
|
|
205
253
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
1
2
|
import type { SchemaYaml } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Error thrown when loading a schema fails.
|
|
@@ -22,6 +23,19 @@ export declare function getUserSchemasDir(): string;
|
|
|
22
23
|
* @returns The path to the project's schemas directory
|
|
23
24
|
*/
|
|
24
25
|
export declare function getProjectSchemasDir(projectRoot: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Determines whether a directory entry represents a schema directory candidate.
|
|
28
|
+
*
|
|
29
|
+
* Returns true for real directories and for symlinks whose target is a
|
|
30
|
+
* directory. `fs.Dirent.isDirectory()` reports the raw entry type, so a symlink
|
|
31
|
+
* (even one pointing at a directory) has `isDirectory() === false`; we
|
|
32
|
+
* dereference such entries via `fs.statSync` to admit symlinked schema dirs
|
|
33
|
+
* while still rejecting symlinks-to-files and broken/dangling symlinks.
|
|
34
|
+
*
|
|
35
|
+
* @param parentDir - The directory containing the entry
|
|
36
|
+
* @param entry - The directory entry from `fs.readdirSync(..., { withFileTypes: true })`
|
|
37
|
+
*/
|
|
38
|
+
export declare function isSchemaDir(parentDir: string, entry: fs.Dirent): boolean;
|
|
25
39
|
/**
|
|
26
40
|
* Resolves a schema name to its directory path.
|
|
27
41
|
*
|
|
@@ -39,6 +39,34 @@ export function getUserSchemasDir() {
|
|
|
39
39
|
export function getProjectSchemasDir(projectRoot) {
|
|
40
40
|
return path.join(projectRoot, 'openspec', 'schemas');
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Determines whether a directory entry represents a schema directory candidate.
|
|
44
|
+
*
|
|
45
|
+
* Returns true for real directories and for symlinks whose target is a
|
|
46
|
+
* directory. `fs.Dirent.isDirectory()` reports the raw entry type, so a symlink
|
|
47
|
+
* (even one pointing at a directory) has `isDirectory() === false`; we
|
|
48
|
+
* dereference such entries via `fs.statSync` to admit symlinked schema dirs
|
|
49
|
+
* while still rejecting symlinks-to-files and broken/dangling symlinks.
|
|
50
|
+
*
|
|
51
|
+
* @param parentDir - The directory containing the entry
|
|
52
|
+
* @param entry - The directory entry from `fs.readdirSync(..., { withFileTypes: true })`
|
|
53
|
+
*/
|
|
54
|
+
export function isSchemaDir(parentDir, entry) {
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (entry.isSymbolicLink()) {
|
|
59
|
+
try {
|
|
60
|
+
// statSync follows the link; isDirectory() reflects the target type.
|
|
61
|
+
return fs.statSync(path.join(parentDir, entry.name)).isDirectory();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Broken symlink (dangling target) — statSync throws; treat as non-dir.
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
42
70
|
/**
|
|
43
71
|
* Resolves a schema name to its directory path.
|
|
44
72
|
*
|
|
@@ -134,7 +162,7 @@ export function listSchemas(projectRoot) {
|
|
|
134
162
|
const packageDir = getPackageSchemasDir();
|
|
135
163
|
if (fs.existsSync(packageDir)) {
|
|
136
164
|
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
|
|
137
|
-
if (entry
|
|
165
|
+
if (isSchemaDir(packageDir, entry)) {
|
|
138
166
|
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
|
|
139
167
|
if (fs.existsSync(schemaPath)) {
|
|
140
168
|
schemas.add(entry.name);
|
|
@@ -146,7 +174,7 @@ export function listSchemas(projectRoot) {
|
|
|
146
174
|
const userDir = getUserSchemasDir();
|
|
147
175
|
if (fs.existsSync(userDir)) {
|
|
148
176
|
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
|
|
149
|
-
if (entry
|
|
177
|
+
if (isSchemaDir(userDir, entry)) {
|
|
150
178
|
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
|
|
151
179
|
if (fs.existsSync(schemaPath)) {
|
|
152
180
|
schemas.add(entry.name);
|
|
@@ -159,7 +187,7 @@ export function listSchemas(projectRoot) {
|
|
|
159
187
|
const projectDir = getProjectSchemasDir(projectRoot);
|
|
160
188
|
if (fs.existsSync(projectDir)) {
|
|
161
189
|
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
|
|
162
|
-
if (entry
|
|
190
|
+
if (isSchemaDir(projectDir, entry)) {
|
|
163
191
|
const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
|
|
164
192
|
if (fs.existsSync(schemaPath)) {
|
|
165
193
|
schemas.add(entry.name);
|
|
@@ -184,7 +212,7 @@ export function listSchemasWithInfo(projectRoot) {
|
|
|
184
212
|
const projectDir = getProjectSchemasDir(projectRoot);
|
|
185
213
|
if (fs.existsSync(projectDir)) {
|
|
186
214
|
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
|
|
187
|
-
if (entry
|
|
215
|
+
if (isSchemaDir(projectDir, entry)) {
|
|
188
216
|
const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
|
|
189
217
|
if (fs.existsSync(schemaPath)) {
|
|
190
218
|
try {
|
|
@@ -209,7 +237,7 @@ export function listSchemasWithInfo(projectRoot) {
|
|
|
209
237
|
const userDir = getUserSchemasDir();
|
|
210
238
|
if (fs.existsSync(userDir)) {
|
|
211
239
|
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
|
|
212
|
-
if (entry
|
|
240
|
+
if (isSchemaDir(userDir, entry) && !seenNames.has(entry.name)) {
|
|
213
241
|
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
|
|
214
242
|
if (fs.existsSync(schemaPath)) {
|
|
215
243
|
try {
|
|
@@ -233,7 +261,7 @@ export function listSchemasWithInfo(projectRoot) {
|
|
|
233
261
|
const packageDir = getPackageSchemasDir();
|
|
234
262
|
if (fs.existsSync(packageDir)) {
|
|
235
263
|
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
|
|
236
|
-
if (entry
|
|
264
|
+
if (isSchemaDir(packageDir, entry) && !seenNames.has(entry.name)) {
|
|
237
265
|
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
|
|
238
266
|
if (fs.existsSync(schemaPath)) {
|
|
239
267
|
try {
|