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.
@@ -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