@fission-ai/openspec 1.13.1 → 1.13.2
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 +15 -3
- package/dist/commands/workflow/instructions.js +36 -8
- package/dist/commands/workflow/shared.d.ts +5 -0
- package/dist/core/archive.js +243 -76
- package/dist/core/artifact-graph/outputs.d.ts +1 -1
- package/dist/core/artifact-graph/outputs.js +38 -8
- package/dist/core/command-generation/adapters/continue.js +4 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +1 -1
- package/dist/core/command-generation/adapters/kilocode.js +2 -2
- package/dist/core/init.js +9 -3
- package/dist/core/legacy-cleanup.d.ts +1 -1
- package/dist/core/legacy-cleanup.js +29 -4
- package/dist/core/parsers/requirement-blocks.d.ts +34 -3
- package/dist/core/parsers/requirement-blocks.js +65 -23
- package/dist/core/specs-apply.d.ts +20 -0
- package/dist/core/specs-apply.js +19 -6
- package/dist/core/templates/workflows/archive-change.js +30 -16
- package/dist/core/templates/workflows/bulk-archive-change.js +28 -14
- package/dist/core/templates/workflows/continue-change.js +0 -2
- package/dist/core/templates/workflows/explore.js +2 -2
- package/dist/core/templates/workflows/ff-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +9 -4
- package/dist/core/templates/workflows/update-change.js +21 -9
- package/dist/core/templates/workflows/verify-change.js +115 -52
- package/dist/core/update.js +20 -7
- package/dist/core/validation/purpose-placeholder.js +24 -9
- package/dist/core/validation/validator.js +6 -5
- package/dist/utils/file-system.js +20 -3
- package/dist/utils/line-endings.d.ts +34 -0
- package/dist/utils/line-endings.js +46 -0
- package/package.json +2 -2
- package/schemas/spec-driven/schema.yaml +8 -0
|
@@ -5,7 +5,7 @@ import { MarkdownParser } from '../parsers/markdown-parser.js';
|
|
|
5
5
|
import { ChangeParser } from '../parsers/change-parser.js';
|
|
6
6
|
import { findSpecUpdates, buildUpdatedSpec } from '../specs-apply.js';
|
|
7
7
|
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
8
|
-
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection,
|
|
8
|
+
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, diffScenarioNames, describeScenarioBalance, } from '../parsers/requirement-blocks.js';
|
|
9
9
|
import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, countEmptyScenarios, } from '../parsers/requirement-text.js';
|
|
10
10
|
import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
|
|
11
11
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
@@ -639,15 +639,16 @@ export class Validator {
|
|
|
639
639
|
const current = currentBlockFor(key);
|
|
640
640
|
if (!current)
|
|
641
641
|
continue;
|
|
642
|
-
const
|
|
643
|
-
if (missing.length === 0)
|
|
642
|
+
const diff = diffScenarioNames(current, block);
|
|
643
|
+
if (diff.missing.length === 0)
|
|
644
644
|
continue;
|
|
645
645
|
issues.push({
|
|
646
646
|
level: 'ERROR',
|
|
647
647
|
path: entryPath,
|
|
648
648
|
message: `MODIFIED "${block.name}" omits scenario(s) the current spec still has: ` +
|
|
649
|
-
`${missing.map(name => `"${name}"`).join(', ')}. ` +
|
|
650
|
-
|
|
649
|
+
`${diff.missing.map(name => `"${name}"`).join(', ')}. ` +
|
|
650
|
+
`${describeScenarioBalance(diff)} ` +
|
|
651
|
+
'Copy the omitted scenarios into the MODIFIED block (a MODIFIED requirement replaces the whole block, so archive refuses to drop them).',
|
|
651
652
|
});
|
|
652
653
|
}
|
|
653
654
|
return issues;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as nodeFs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { detectLineEnding, matchLineEnding } from './line-endings.js';
|
|
3
4
|
const fs = nodeFs.promises;
|
|
4
5
|
const { constants: fsConstants } = nodeFs;
|
|
5
6
|
function hasOwnerGroupOrOtherWriteBit(stats) {
|
|
@@ -277,8 +278,14 @@ export class FileSystemUtils {
|
|
|
277
278
|
}
|
|
278
279
|
static async updateFileWithMarkers(filePath, content, startMarker, endMarker) {
|
|
279
280
|
let existingContent = '';
|
|
281
|
+
// The managed block is composed with '\n', so splicing it into a CRLF file
|
|
282
|
+
// would leave mixed endings behind. bash reports a stray '\r' in .bashrc as
|
|
283
|
+
// "$'\r': command not found", so settle the whole file on the convention it
|
|
284
|
+
// already used. A file that does not exist yet stays LF.
|
|
285
|
+
let originalContent;
|
|
280
286
|
if (await this.fileExists(filePath)) {
|
|
281
287
|
existingContent = await this.readFile(filePath);
|
|
288
|
+
originalContent = existingContent;
|
|
282
289
|
const startIndex = findMarkerIndex(existingContent, startMarker);
|
|
283
290
|
const endIndex = startIndex !== -1
|
|
284
291
|
? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
|
|
@@ -301,7 +308,9 @@ export class FileSystemUtils {
|
|
|
301
308
|
else {
|
|
302
309
|
existingContent = startMarker + '\n' + content + '\n' + endMarker;
|
|
303
310
|
}
|
|
304
|
-
await this.writeFile(filePath,
|
|
311
|
+
await this.writeFile(filePath, originalContent === undefined
|
|
312
|
+
? existingContent
|
|
313
|
+
: matchLineEnding(existingContent, originalContent));
|
|
305
314
|
}
|
|
306
315
|
static async ensureWritePermissions(dirPath) {
|
|
307
316
|
try {
|
|
@@ -377,14 +386,22 @@ export function removeMarkerBlock(content, startMarker, endMarker) {
|
|
|
377
386
|
}
|
|
378
387
|
const before = content.substring(0, lineStart);
|
|
379
388
|
const after = content.substring(lineEnd);
|
|
389
|
+
// The file's own newline, used for every ending this function writes. The
|
|
390
|
+
// blank-line collapse below rebuilds the separator it matched, so spelling it
|
|
391
|
+
// '\n' would leave a CRLF file with a mixed pair wherever a run was collapsed
|
|
392
|
+
// - the stray '\r' that bash reports as "$'\r': command not found".
|
|
393
|
+
//
|
|
394
|
+
// Dominant rather than "contains a CRLF anywhere", so that one stray CRLF in
|
|
395
|
+
// an otherwise-LF file does not pull the whole rewrite to CRLF. This is the
|
|
396
|
+
// same reading matchLineEnding uses, so both write paths agree.
|
|
397
|
+
const newline = detectLineEnding(content) ?? '\n';
|
|
380
398
|
// Clean up double blank lines (handle both Unix \n and Windows \r\n)
|
|
381
399
|
let result = before + after;
|
|
382
|
-
result = result.replace(/(\r?\n){3,}/g,
|
|
400
|
+
result = result.replace(/(\r?\n){3,}/g, newline + newline);
|
|
383
401
|
// Trim trailing whitespace but preserve leading whitespace and original newline style
|
|
384
402
|
if (result.trimEnd() === '') {
|
|
385
403
|
return '';
|
|
386
404
|
}
|
|
387
|
-
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
388
405
|
return result.trimEnd() + newline;
|
|
389
406
|
}
|
|
390
407
|
//# sourceMappingURL=file-system.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-ending helpers.
|
|
3
|
+
*
|
|
4
|
+
* Every parser in this codebase normalizes CRLF to LF on the way in, so all
|
|
5
|
+
* serialization logic can assume '\n'. That leaves the write side responsible
|
|
6
|
+
* for restoring whatever convention the file already used: without it, editing
|
|
7
|
+
* one requirement in a CRLF spec rewrites every line of the file and buries the
|
|
8
|
+
* real change in the diff.
|
|
9
|
+
*/
|
|
10
|
+
export type LineEnding = '\n' | '\r\n';
|
|
11
|
+
/**
|
|
12
|
+
* The dominant line ending in `content`, or undefined when it holds no line
|
|
13
|
+
* break to judge from.
|
|
14
|
+
*
|
|
15
|
+
* Mixed files resolve to whichever ending is more common, with CRLF winning a
|
|
16
|
+
* tie: a file that is mostly CRLF is a CRLF file that picked up a stray LF, and
|
|
17
|
+
* settling the whole file on one ending is what keeps later diffs small.
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectLineEnding(content: string): LineEnding | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Re-apply `ending` to LF-normalized `content`.
|
|
22
|
+
*
|
|
23
|
+
* Normalizes to LF first, so the result is uniform even if the caller passed
|
|
24
|
+
* content that already contained CRLF.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyLineEnding(content: string, ending: LineEnding): string;
|
|
27
|
+
/**
|
|
28
|
+
* Rewrite `content` to match the convention of `original`.
|
|
29
|
+
*
|
|
30
|
+
* When `original` has no line break to learn from, `content` is left as LF —
|
|
31
|
+
* the portable default this project writes new files with.
|
|
32
|
+
*/
|
|
33
|
+
export declare function matchLineEnding(content: string, original: string): string;
|
|
34
|
+
//# sourceMappingURL=line-endings.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-ending helpers.
|
|
3
|
+
*
|
|
4
|
+
* Every parser in this codebase normalizes CRLF to LF on the way in, so all
|
|
5
|
+
* serialization logic can assume '\n'. That leaves the write side responsible
|
|
6
|
+
* for restoring whatever convention the file already used: without it, editing
|
|
7
|
+
* one requirement in a CRLF spec rewrites every line of the file and buries the
|
|
8
|
+
* real change in the diff.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The dominant line ending in `content`, or undefined when it holds no line
|
|
12
|
+
* break to judge from.
|
|
13
|
+
*
|
|
14
|
+
* Mixed files resolve to whichever ending is more common, with CRLF winning a
|
|
15
|
+
* tie: a file that is mostly CRLF is a CRLF file that picked up a stray LF, and
|
|
16
|
+
* settling the whole file on one ending is what keeps later diffs small.
|
|
17
|
+
*/
|
|
18
|
+
export function detectLineEnding(content) {
|
|
19
|
+
const crlf = content.match(/\r\n/g)?.length ?? 0;
|
|
20
|
+
// Count LFs not preceded by CR, so CRLF is never also counted as LF.
|
|
21
|
+
const lf = content.match(/(?<!\r)\n/g)?.length ?? 0;
|
|
22
|
+
if (crlf === 0 && lf === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
return crlf >= lf ? '\r\n' : '\n';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Re-apply `ending` to LF-normalized `content`.
|
|
28
|
+
*
|
|
29
|
+
* Normalizes to LF first, so the result is uniform even if the caller passed
|
|
30
|
+
* content that already contained CRLF.
|
|
31
|
+
*/
|
|
32
|
+
export function applyLineEnding(content, ending) {
|
|
33
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
34
|
+
return ending === '\n' ? normalized : normalized.replace(/\n/g, '\r\n');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Rewrite `content` to match the convention of `original`.
|
|
38
|
+
*
|
|
39
|
+
* When `original` has no line break to learn from, `content` is left as LF —
|
|
40
|
+
* the portable default this project writes new files with.
|
|
41
|
+
*/
|
|
42
|
+
export function matchLineEnding(content, original) {
|
|
43
|
+
const ending = detectLineEnding(original);
|
|
44
|
+
return ending === undefined ? content : applyLineEnding(content, ending);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=line-endings.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.2",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"vitest": "^4.1.11"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@inquirer/core": "^
|
|
55
|
+
"@inquirer/core": "^12.0.0",
|
|
56
56
|
"@inquirer/prompts": "^8.5.2",
|
|
57
57
|
"chalk": "^5.6.2",
|
|
58
58
|
"commander": "^14.0.0",
|
|
@@ -210,6 +210,13 @@ artifacts:
|
|
|
210
210
|
that task's checkbox description. Use a separate verification task only
|
|
211
211
|
when it checks broader integration or system behavior that spans
|
|
212
212
|
multiple implementation tasks.
|
|
213
|
+
- Each task group MUST land the tests and documentation its own work
|
|
214
|
+
calls for. Do NOT collect testing or documentation into a final group -
|
|
215
|
+
when a late group first exercises work from an early one, the failures
|
|
216
|
+
cascade back through every group in between and force rework. A group
|
|
217
|
+
whose work calls for neither, such as scaffolding or dependency setup,
|
|
218
|
+
carries neither. A final group is for integration checks only, not for
|
|
219
|
+
the tests and docs an earlier group owed.
|
|
213
220
|
|
|
214
221
|
Example:
|
|
215
222
|
```
|
|
@@ -224,6 +231,7 @@ artifacts:
|
|
|
224
231
|
|
|
225
232
|
- [ ] 2.1 Implement data export function and verify the export test passes
|
|
226
233
|
- [ ] 2.2 Add CSV formatting utilities and verify unit tests cover quoting and delimiters
|
|
234
|
+
- [ ] 2.3 Document the export API in docs/export.md and verify the documented command runs as written
|
|
227
235
|
```
|
|
228
236
|
|
|
229
237
|
Reference specs for what needs to be built, design for how to build it.
|