@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.
Files changed (32) hide show
  1. package/README.md +15 -3
  2. package/dist/commands/workflow/instructions.js +36 -8
  3. package/dist/commands/workflow/shared.d.ts +5 -0
  4. package/dist/core/archive.js +243 -76
  5. package/dist/core/artifact-graph/outputs.d.ts +1 -1
  6. package/dist/core/artifact-graph/outputs.js +38 -8
  7. package/dist/core/command-generation/adapters/continue.js +4 -0
  8. package/dist/core/command-generation/adapters/kilocode.d.ts +1 -1
  9. package/dist/core/command-generation/adapters/kilocode.js +2 -2
  10. package/dist/core/init.js +9 -3
  11. package/dist/core/legacy-cleanup.d.ts +1 -1
  12. package/dist/core/legacy-cleanup.js +29 -4
  13. package/dist/core/parsers/requirement-blocks.d.ts +34 -3
  14. package/dist/core/parsers/requirement-blocks.js +65 -23
  15. package/dist/core/specs-apply.d.ts +20 -0
  16. package/dist/core/specs-apply.js +19 -6
  17. package/dist/core/templates/workflows/archive-change.js +30 -16
  18. package/dist/core/templates/workflows/bulk-archive-change.js +28 -14
  19. package/dist/core/templates/workflows/continue-change.js +0 -2
  20. package/dist/core/templates/workflows/explore.js +2 -2
  21. package/dist/core/templates/workflows/ff-change.js +2 -2
  22. package/dist/core/templates/workflows/onboard.js +9 -4
  23. package/dist/core/templates/workflows/update-change.js +21 -9
  24. package/dist/core/templates/workflows/verify-change.js +115 -52
  25. package/dist/core/update.js +20 -7
  26. package/dist/core/validation/purpose-placeholder.js +24 -9
  27. package/dist/core/validation/validator.js +6 -5
  28. package/dist/utils/file-system.js +20 -3
  29. package/dist/utils/line-endings.d.ts +34 -0
  30. package/dist/utils/line-endings.js +46 -0
  31. package/package.json +2 -2
  32. 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, findMissingCurrentScenarios, } from '../parsers/requirement-blocks.js';
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 missing = findMissingCurrentScenarios(current, block);
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
- 'Copy them into the MODIFIED block (a MODIFIED requirement replaces the whole block, so archive refuses to drop them).',
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, existingContent);
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, '\n\n');
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.1",
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": "^11.2.1",
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.