thoth-agents 0.3.9 → 0.3.10

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.
@@ -0,0 +1,158 @@
1
+ export const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2
+
3
+ export function parseRequirementDelta(metadata) {
4
+ if (metadata === 'INTERNAL') return { operation: 'INTERNAL' };
5
+
6
+ const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
7
+ if (change && CAPABILITY_PATTERN.test(change[2])) {
8
+ return { operation: change[1], capability: change[2] };
9
+ }
10
+
11
+ const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
12
+ if (
13
+ rename &&
14
+ CAPABILITY_PATTERN.test(rename[1]) &&
15
+ rename[2].trim().length > 0
16
+ ) {
17
+ return {
18
+ operation: 'RENAMED',
19
+ capability: rename[1],
20
+ previousTitle: rename[2].trim(),
21
+ };
22
+ }
23
+
24
+ return undefined;
25
+ }
26
+
27
+ export function parseCanonicalSpec(content) {
28
+ const candidates = [...content.matchAll(/^### Requirement:[^\r\n]*$/gm)];
29
+ const headings = [
30
+ ...content.matchAll(/^### Requirement:[ \t]+(\S(?:[^\r\n]*?\S)?)[ \t]*$/gm),
31
+ ];
32
+ if (candidates.length !== headings.length) {
33
+ throw new Error(
34
+ 'Canonical specification contains a malformed Requirement heading',
35
+ );
36
+ }
37
+ const requirements = new Map();
38
+ const prefix =
39
+ headings.length === 0
40
+ ? content.trimEnd()
41
+ : content.slice(0, headings[0].index).trimEnd();
42
+
43
+ for (const [index, heading] of headings.entries()) {
44
+ const start = heading.index ?? 0;
45
+ const end = headings[index + 1]?.index ?? content.length;
46
+ const title = heading[1].trim();
47
+ if (requirements.has(title)) {
48
+ throw new Error(`Canonical specification repeats requirement: ${title}`);
49
+ }
50
+ requirements.set(title, content.slice(start, end).trim());
51
+ }
52
+
53
+ return { prefix, requirements };
54
+ }
55
+
56
+ function issue(code, capability, title, message) {
57
+ return { code, capability, title, message };
58
+ }
59
+
60
+ export function preflightRequirementDeltas({
61
+ capability,
62
+ present,
63
+ requirements,
64
+ deltas,
65
+ }) {
66
+ const initialTitles = [...requirements.keys()];
67
+ const titles = new Set(initialTitles);
68
+ const errors = [];
69
+ const warnings = [];
70
+
71
+ for (const delta of deltas) {
72
+ if (delta.operation === 'ADDED') {
73
+ if (titles.has(delta.title)) {
74
+ errors.push(
75
+ issue(
76
+ 'SDD-SPEC-DELTA-ADDED-EXISTS',
77
+ capability,
78
+ delta.title,
79
+ `${capability} already contains requirement: ${delta.title}`,
80
+ ),
81
+ );
82
+ continue;
83
+ }
84
+ if (present && initialTitles.length > 0) {
85
+ warnings.push(
86
+ issue(
87
+ 'SDD-SPEC-DELTA-ADDED-REVIEW',
88
+ capability,
89
+ delta.title,
90
+ `${capability} already contains canonical requirements; confirm ${delta.title} does not overlap: ${initialTitles.join(', ')}`,
91
+ ),
92
+ );
93
+ }
94
+ titles.add(delta.title);
95
+ continue;
96
+ }
97
+
98
+ if (!present) {
99
+ const code =
100
+ delta.operation === 'RENAMED'
101
+ ? 'SDD-SPEC-DELTA-RENAMED-SOURCE-MISSING'
102
+ : `SDD-SPEC-DELTA-${delta.operation}-MISSING`;
103
+ errors.push(
104
+ issue(
105
+ code,
106
+ capability,
107
+ delta.title,
108
+ `${capability} has no canonical specification for ${delta.operation}: ${delta.title}`,
109
+ ),
110
+ );
111
+ continue;
112
+ }
113
+
114
+ if (delta.operation === 'RENAMED') {
115
+ if (!titles.has(delta.previousTitle)) {
116
+ errors.push(
117
+ issue(
118
+ 'SDD-SPEC-DELTA-RENAMED-SOURCE-MISSING',
119
+ capability,
120
+ delta.title,
121
+ `${capability} does not contain requirement: ${delta.previousTitle}`,
122
+ ),
123
+ );
124
+ continue;
125
+ }
126
+ if (delta.previousTitle !== delta.title && titles.has(delta.title)) {
127
+ errors.push(
128
+ issue(
129
+ 'SDD-SPEC-DELTA-RENAMED-TARGET-EXISTS',
130
+ capability,
131
+ delta.title,
132
+ `${capability} already contains requirement: ${delta.title}`,
133
+ ),
134
+ );
135
+ continue;
136
+ }
137
+ titles.delete(delta.previousTitle);
138
+ titles.add(delta.title);
139
+ continue;
140
+ }
141
+
142
+ if (!titles.has(delta.title)) {
143
+ errors.push(
144
+ issue(
145
+ `SDD-SPEC-DELTA-${delta.operation}-MISSING`,
146
+ capability,
147
+ delta.title,
148
+ `${capability} does not contain requirement: ${delta.title}`,
149
+ ),
150
+ );
151
+ continue;
152
+ }
153
+
154
+ if (delta.operation === 'REMOVED') titles.delete(delta.title);
155
+ }
156
+
157
+ return { errors, warnings, titles };
158
+ }
@@ -2,6 +2,12 @@
2
2
 
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import { join, posix, resolve } from 'node:path';
5
+ import {
6
+ CAPABILITY_PATTERN,
7
+ parseCanonicalSpec,
8
+ parseRequirementDelta,
9
+ preflightRequirementDeltas,
10
+ } from './durable-deltas.mjs';
5
11
 
6
12
  function parseArgs(argv) {
7
13
  const values = { route: 'full', through: 'ready', json: false };
@@ -86,34 +92,6 @@ function checkUniqueSequential(ids, prefix, artifact, errors) {
86
92
  }
87
93
  }
88
94
 
89
- const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
90
-
91
- function parseRequirementDelta(metadata) {
92
- if (metadata === 'INTERNAL') {
93
- return { operation: 'INTERNAL' };
94
- }
95
-
96
- const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
97
- if (change && CAPABILITY_PATTERN.test(change[2])) {
98
- return { operation: change[1], capability: change[2] };
99
- }
100
-
101
- const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
102
- if (
103
- rename &&
104
- CAPABILITY_PATTERN.test(rename[1]) &&
105
- rename[2].trim().length > 0
106
- ) {
107
- return {
108
- operation: 'RENAMED',
109
- capability: rename[1],
110
- previousTitle: rename[2].trim(),
111
- };
112
- }
113
-
114
- return undefined;
115
- }
116
-
117
95
  function parseAffectedCapabilities(intentSection) {
118
96
  const match = /^\*\*Affected capabilities\*\*:\s*(.+)$/im.exec(
119
97
  intentSection ?? '',
@@ -264,6 +242,7 @@ function validateSpec(content, errors) {
264
242
  ),
265
243
  );
266
244
  }
245
+ const durableDeltas = [];
267
246
  if (frIds.length === 0) {
268
247
  errors.push(issue('SDD-SPEC-FR', artifact, 'Add FR-### requirements.'));
269
248
  } else {
@@ -298,6 +277,11 @@ function validateSpec(content, errors) {
298
277
  `FR-${match[1]} targets undeclared capability ${delta.capability}.`,
299
278
  ),
300
279
  );
280
+ } else if (delta.operation !== 'INTERNAL') {
281
+ durableDeltas.push({
282
+ title: match[2].trim(),
283
+ ...delta,
284
+ });
301
285
  }
302
286
  }
303
287
  }
@@ -369,9 +353,56 @@ function validateSpec(content, errors) {
369
353
  outcomeScIds: successCriteria
370
354
  .filter((match) => match[2].toLowerCase() === 'outcome')
371
355
  .map((match) => match[1]),
356
+ durableDeltas,
372
357
  };
373
358
  }
374
359
 
360
+ function validateDurableDeltaIntent(specRoot, deltas, errors, warnings) {
361
+ const byCapability = new Map();
362
+ for (const delta of deltas) {
363
+ const current = byCapability.get(delta.capability) ?? [];
364
+ current.push(delta);
365
+ byCapability.set(delta.capability, current);
366
+ }
367
+
368
+ for (const [capability, capabilityDeltas] of byCapability) {
369
+ const path = join(specRoot, capability, 'spec.md');
370
+ const present = existsSync(path);
371
+ let canonical;
372
+ try {
373
+ canonical = present
374
+ ? parseCanonicalSpec(readFileSync(path, 'utf8'))
375
+ : { requirements: new Map() };
376
+ } catch (error) {
377
+ errors.push(
378
+ issue(
379
+ 'SDD-SPEC-DELTA-BASELINE',
380
+ 'spec.md',
381
+ `${capability} canonical baseline is invalid: ${error instanceof Error ? error.message : String(error)}`,
382
+ ),
383
+ );
384
+ continue;
385
+ }
386
+
387
+ const result = preflightRequirementDeltas({
388
+ capability,
389
+ present,
390
+ requirements: canonical.requirements,
391
+ deltas: capabilityDeltas,
392
+ });
393
+ errors.push(
394
+ ...result.errors.map((entry) =>
395
+ issue(entry.code, 'spec.md', entry.message),
396
+ ),
397
+ );
398
+ warnings.push(
399
+ ...result.warnings.map((entry) =>
400
+ issue(entry.code, 'spec.md', entry.message),
401
+ ),
402
+ );
403
+ }
404
+ }
405
+
375
406
  function constitutionEntries(content) {
376
407
  if (content === undefined) return [];
377
408
  return content
@@ -1047,6 +1078,14 @@ function validate({ change, route, through }) {
1047
1078
 
1048
1079
  const specContract =
1049
1080
  spec === undefined ? undefined : validateSpec(spec, errors);
1081
+ if (specContract !== undefined) {
1082
+ validateDurableDeltaIntent(
1083
+ join(root, '..', '..', 'specs'),
1084
+ specContract.durableDeltas,
1085
+ errors,
1086
+ warnings,
1087
+ );
1088
+ }
1050
1089
  const constitution = readArtifact(
1051
1090
  join(root, '..', '..'),
1052
1091
  'memory/constitution.md',
@@ -30,7 +30,15 @@ As a [actor], I can [capability] so that [value].
30
30
 
31
31
  ## Functional requirements
32
32
 
33
- - **FR-001 — [Requirement name]**: `[ADDED capability-slug]` The system MUST [observable behavior].
33
+ - **FR-001 — [Requirement name]**: `[DELTA capability-slug]` The system MUST [observable behavior].
34
+
35
+ Replace `DELTA` after inspecting `openspec/specs/<capability>/spec.md`. Use
36
+ `ADDED` only for behavior not expressed by a canonical requirement, `MODIFIED`
37
+ or `REMOVED` only with the exact existing requirement title, and
38
+ `RENAMED capability-slug FROM Exact previous title` when the title changes. Only
39
+ `ADDED` is valid when the canonical capability is absent. An addition to an
40
+ existing nonempty capability requires semantic-overlap review even when its
41
+ exact title is new.
34
42
 
35
43
  Use exactly one delta marker per FR: `[INTERNAL]`, `[ADDED capability-slug]`,
36
44
  `[MODIFIED capability-slug]`, `[REMOVED capability-slug]`, or
@@ -31,6 +31,13 @@ The script reads delta metadata directly from `spec.md`:
31
31
  - `[RENAMED capability FROM Previous title]` renames and replaces it.
32
32
  - `[INTERNAL]` never changes `openspec/specs/`.
33
33
 
34
+ The SDD validator preflights these operations against the canonical requirement
35
+ titles from `specify` onward. Archive reuses the same ordered parser and
36
+ preflight as a final defense before staging writes, preserving stable
37
+ `SDD-SPEC-DELTA-*` incompatibility codes. An `ADDED` warning for an existing
38
+ nonempty capability requires semantic-overlap review because differently named
39
+ requirements cannot be proven distinct by exact-title tooling alone.
40
+
34
41
  All delta targets are validated before any permanent specification changes. The
35
42
  operation stages and rolls back the full canonical update if a delta, report
36
43
  update, or final move raises a handled error in the active process. Report and
@@ -10,8 +10,13 @@ import {
10
10
  writeFileSync,
11
11
  } from 'node:fs';
12
12
  import { basename, dirname, join, resolve } from 'node:path';
13
+ import {
14
+ CAPABILITY_PATTERN,
15
+ parseCanonicalSpec,
16
+ parseRequirementDelta as parseSharedRequirementDelta,
17
+ preflightRequirementDeltas,
18
+ } from '../../thoth-sdd/scripts/durable-deltas.mjs';
13
19
 
14
- const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
20
  const TEST_FAULTS = new Set(
16
21
  (process.env.THOTH_ARCHIVE_TEST_FAULT ?? '')
17
22
  .split(',')
@@ -270,26 +275,8 @@ function assertArchivable(change, contract) {
270
275
  }
271
276
 
272
277
  function parseDeltaMetadata(metadata) {
273
- if (metadata === 'INTERNAL') return { operation: 'INTERNAL' };
274
-
275
- const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
276
- if (change && CAPABILITY_PATTERN.test(change[2])) {
277
- return { operation: change[1], capability: change[2] };
278
- }
279
-
280
- const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
281
- if (
282
- rename &&
283
- CAPABILITY_PATTERN.test(rename[1]) &&
284
- rename[2].trim().length > 0
285
- ) {
286
- return {
287
- operation: 'RENAMED',
288
- capability: rename[1],
289
- previousTitle: rename[2].trim(),
290
- };
291
- }
292
-
278
+ const delta = parseSharedRequirementDelta(metadata);
279
+ if (delta) return delta;
293
280
  throw new Error(`Invalid requirement delta metadata: [${metadata}]`);
294
281
  }
295
282
 
@@ -418,27 +405,6 @@ function parseChangeSpec(spec) {
418
405
  };
419
406
  }
420
407
 
421
- function parseCanonicalSpec(content) {
422
- const headings = [...content.matchAll(/^### Requirement:\s+(.+?)\s*$/gm)];
423
- const requirements = new Map();
424
- const prefix =
425
- headings.length === 0
426
- ? content.trimEnd()
427
- : content.slice(0, headings[0].index).trimEnd();
428
-
429
- for (const [index, heading] of headings.entries()) {
430
- const start = heading.index ?? 0;
431
- const end = headings[index + 1]?.index ?? content.length;
432
- const title = heading[1].trim();
433
- if (requirements.has(title)) {
434
- throw new Error(`Canonical specification repeats requirement: ${title}`);
435
- }
436
- requirements.set(title, content.slice(start, end).trim());
437
- }
438
-
439
- return { prefix, requirements };
440
- }
441
-
442
408
  function newCanonicalSpec(capability) {
443
409
  const title = capability
444
410
  .split('-')
@@ -480,41 +446,34 @@ function planCanonicalUpdates(specRoot, deltas) {
480
446
  for (const [capability, capabilityDeltas] of byCapability) {
481
447
  const path = join(specRoot, capability, 'spec.md');
482
448
  const present = existsSync(path);
483
- const canonical = present
484
- ? parseCanonicalSpec(readFileSync(path, 'utf8'))
485
- : newCanonicalSpec(capability);
449
+ let canonical;
450
+ try {
451
+ canonical = present
452
+ ? parseCanonicalSpec(readFileSync(path, 'utf8'))
453
+ : newCanonicalSpec(capability);
454
+ } catch (error) {
455
+ throw new Error(
456
+ `SDD-SPEC-DELTA-BASELINE: ${capability} canonical baseline is invalid: ${errorMessage(error)}`,
457
+ );
458
+ }
459
+ const preflight = preflightRequirementDeltas({
460
+ capability,
461
+ present,
462
+ requirements: canonical.requirements,
463
+ deltas: capabilityDeltas,
464
+ });
465
+ if (preflight.errors.length > 0) {
466
+ const [first] = preflight.errors;
467
+ throw new Error(`${first.code}: ${first.message}`);
468
+ }
486
469
 
487
470
  for (const delta of capabilityDeltas) {
488
471
  if (delta.operation === 'ADDED') {
489
- if (canonical.requirements.has(delta.title)) {
490
- throw new Error(
491
- `${capability} already contains requirement: ${delta.title}`,
492
- );
493
- }
494
472
  canonical.requirements.set(delta.title, renderRequirement(delta));
495
473
  continue;
496
474
  }
497
475
 
498
- if (!present) {
499
- throw new Error(
500
- `${capability} has no canonical specification for ${delta.operation}`,
501
- );
502
- }
503
-
504
476
  if (delta.operation === 'RENAMED') {
505
- if (!canonical.requirements.has(delta.previousTitle)) {
506
- throw new Error(
507
- `${capability} does not contain requirement: ${delta.previousTitle}`,
508
- );
509
- }
510
- if (
511
- delta.previousTitle !== delta.title &&
512
- canonical.requirements.has(delta.title)
513
- ) {
514
- throw new Error(
515
- `${capability} already contains requirement: ${delta.title}`,
516
- );
517
- }
518
477
  const entries = [...canonical.requirements.entries()];
519
478
  canonical.requirements = new Map(
520
479
  entries.map(([title, block]) =>
@@ -526,11 +485,6 @@ function planCanonicalUpdates(specRoot, deltas) {
526
485
  continue;
527
486
  }
528
487
 
529
- if (!canonical.requirements.has(delta.title)) {
530
- throw new Error(
531
- `${capability} does not contain requirement: ${delta.title}`,
532
- );
533
- }
534
488
  if (delta.operation === 'MODIFIED') {
535
489
  canonical.requirements.set(delta.title, renderRequirement(delta));
536
490
  } else if (delta.operation === 'REMOVED') {
@@ -11,6 +11,16 @@ implementation-neutral.
11
11
  `None`).
12
12
  - Define prioritized, independently testable `US#` stories. Every story needs
13
13
  `Covers: FR-###, SC-###` and Given/When/Then acceptance scenarios.
14
+ - Before choosing durable delta metadata, read
15
+ `openspec/specs/<capability>/spec.md` when it exists and compare the exact
16
+ `### Requirement:` titles and normative behavior:
17
+ - use `ADDED` only when no canonical requirement already expresses the
18
+ behavior;
19
+ - use `MODIFIED` or `REMOVED` only with the exact existing title;
20
+ - use `RENAMED <capability> FROM <exact previous title>` when the title changes;
21
+ - only `ADDED` is valid when the canonical capability does not exist.
22
+ A validator warning on `ADDED` into an existing nonempty capability requires
23
+ semantic-overlap review; exact-title validity alone does not prove new intent.
14
24
  - Give every sequential FR a descriptive title, a normative `MUST`/`SHALL`
15
25
  statement, and exactly one delta marker: `INTERNAL`, `ADDED`, `MODIFIED`,
16
26
  `REMOVED`, or `RENAMED ... FROM ...`.
@@ -20,4 +30,5 @@ implementation-neutral.
20
30
  - State assumptions, dependencies, edge cases, and explicit non-goals.
21
31
 
22
32
  Do not advance while a material ambiguity would change scope, behavior,
23
- architecture, or durable delta intent.
33
+ architecture, or durable delta intent. Run the `specify` gate before planning;
34
+ do not defer canonical delta reconciliation until archive.
@@ -0,0 +1,158 @@
1
+ export const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2
+
3
+ export function parseRequirementDelta(metadata) {
4
+ if (metadata === 'INTERNAL') return { operation: 'INTERNAL' };
5
+
6
+ const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
7
+ if (change && CAPABILITY_PATTERN.test(change[2])) {
8
+ return { operation: change[1], capability: change[2] };
9
+ }
10
+
11
+ const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
12
+ if (
13
+ rename &&
14
+ CAPABILITY_PATTERN.test(rename[1]) &&
15
+ rename[2].trim().length > 0
16
+ ) {
17
+ return {
18
+ operation: 'RENAMED',
19
+ capability: rename[1],
20
+ previousTitle: rename[2].trim(),
21
+ };
22
+ }
23
+
24
+ return undefined;
25
+ }
26
+
27
+ export function parseCanonicalSpec(content) {
28
+ const candidates = [...content.matchAll(/^### Requirement:[^\r\n]*$/gm)];
29
+ const headings = [
30
+ ...content.matchAll(/^### Requirement:[ \t]+(\S(?:[^\r\n]*?\S)?)[ \t]*$/gm),
31
+ ];
32
+ if (candidates.length !== headings.length) {
33
+ throw new Error(
34
+ 'Canonical specification contains a malformed Requirement heading',
35
+ );
36
+ }
37
+ const requirements = new Map();
38
+ const prefix =
39
+ headings.length === 0
40
+ ? content.trimEnd()
41
+ : content.slice(0, headings[0].index).trimEnd();
42
+
43
+ for (const [index, heading] of headings.entries()) {
44
+ const start = heading.index ?? 0;
45
+ const end = headings[index + 1]?.index ?? content.length;
46
+ const title = heading[1].trim();
47
+ if (requirements.has(title)) {
48
+ throw new Error(`Canonical specification repeats requirement: ${title}`);
49
+ }
50
+ requirements.set(title, content.slice(start, end).trim());
51
+ }
52
+
53
+ return { prefix, requirements };
54
+ }
55
+
56
+ function issue(code, capability, title, message) {
57
+ return { code, capability, title, message };
58
+ }
59
+
60
+ export function preflightRequirementDeltas({
61
+ capability,
62
+ present,
63
+ requirements,
64
+ deltas,
65
+ }) {
66
+ const initialTitles = [...requirements.keys()];
67
+ const titles = new Set(initialTitles);
68
+ const errors = [];
69
+ const warnings = [];
70
+
71
+ for (const delta of deltas) {
72
+ if (delta.operation === 'ADDED') {
73
+ if (titles.has(delta.title)) {
74
+ errors.push(
75
+ issue(
76
+ 'SDD-SPEC-DELTA-ADDED-EXISTS',
77
+ capability,
78
+ delta.title,
79
+ `${capability} already contains requirement: ${delta.title}`,
80
+ ),
81
+ );
82
+ continue;
83
+ }
84
+ if (present && initialTitles.length > 0) {
85
+ warnings.push(
86
+ issue(
87
+ 'SDD-SPEC-DELTA-ADDED-REVIEW',
88
+ capability,
89
+ delta.title,
90
+ `${capability} already contains canonical requirements; confirm ${delta.title} does not overlap: ${initialTitles.join(', ')}`,
91
+ ),
92
+ );
93
+ }
94
+ titles.add(delta.title);
95
+ continue;
96
+ }
97
+
98
+ if (!present) {
99
+ const code =
100
+ delta.operation === 'RENAMED'
101
+ ? 'SDD-SPEC-DELTA-RENAMED-SOURCE-MISSING'
102
+ : `SDD-SPEC-DELTA-${delta.operation}-MISSING`;
103
+ errors.push(
104
+ issue(
105
+ code,
106
+ capability,
107
+ delta.title,
108
+ `${capability} has no canonical specification for ${delta.operation}: ${delta.title}`,
109
+ ),
110
+ );
111
+ continue;
112
+ }
113
+
114
+ if (delta.operation === 'RENAMED') {
115
+ if (!titles.has(delta.previousTitle)) {
116
+ errors.push(
117
+ issue(
118
+ 'SDD-SPEC-DELTA-RENAMED-SOURCE-MISSING',
119
+ capability,
120
+ delta.title,
121
+ `${capability} does not contain requirement: ${delta.previousTitle}`,
122
+ ),
123
+ );
124
+ continue;
125
+ }
126
+ if (delta.previousTitle !== delta.title && titles.has(delta.title)) {
127
+ errors.push(
128
+ issue(
129
+ 'SDD-SPEC-DELTA-RENAMED-TARGET-EXISTS',
130
+ capability,
131
+ delta.title,
132
+ `${capability} already contains requirement: ${delta.title}`,
133
+ ),
134
+ );
135
+ continue;
136
+ }
137
+ titles.delete(delta.previousTitle);
138
+ titles.add(delta.title);
139
+ continue;
140
+ }
141
+
142
+ if (!titles.has(delta.title)) {
143
+ errors.push(
144
+ issue(
145
+ `SDD-SPEC-DELTA-${delta.operation}-MISSING`,
146
+ capability,
147
+ delta.title,
148
+ `${capability} does not contain requirement: ${delta.title}`,
149
+ ),
150
+ );
151
+ continue;
152
+ }
153
+
154
+ if (delta.operation === 'REMOVED') titles.delete(delta.title);
155
+ }
156
+
157
+ return { errors, warnings, titles };
158
+ }