@suss/checker-intent 0.21.1 → 0.22.1
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 +8 -1
- package/dist/index.cjs +185 -7
- package/dist/index.js +185 -7
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ the same boundary key, and emits:
|
|
|
27
27
|
- `undeclaredOutcome`: code produces a REST status, or reaches a boundary, that the intent doesn't declare (info; intent under-specifies).
|
|
28
28
|
- `unkeyableBoundary`: the intent's boundary has no key to pair on, so nothing was compared (warning).
|
|
29
29
|
- `undescribedOutcome`: a declared outcome no PRD scenario links to (info).
|
|
30
|
+
- `renamedBoundary`: a declared store the unit never touches, paired with an undeclared store of the same system it touches with the same verbs on the same outcomes instead.
|
|
30
31
|
|
|
31
32
|
v0 checks system intent (`kind: boundary`). PRD outcome intent
|
|
32
33
|
(`kind: prd`), which covers scenarios and links, is a separate pass.
|
|
@@ -53,10 +54,16 @@ An access whose container the code cannot settle (a wrapper handed its table as
|
|
|
53
54
|
|
|
54
55
|
A `when` clause that says which boundary the branch read is compared too, which is what makes `when` more than prose.
|
|
55
56
|
|
|
56
|
-
`boundaryGuardsOf` in `@suss/behavioral-ir` says, for each branch of the code, which boundary its guards turned on and whether the guard passed because something was there. A declared outcome then narrows to the branches whose guards match every boundary clause the intent stated, and `uncoveredOutcome` reports one that produces the ending on a different condition. So an intent saying "404 when a read of `aws.dynamodb:Invoices` finds nothing" fails when the code's 404 turns on the row being present.
|
|
57
|
+
`boundaryGuardsOf` in `@suss/behavioral-ir` says, for each branch of the code, which boundary its guards turned on and whether the guard passed because something was there. A declared outcome then narrows to the branches whose guards match every boundary clause the intent stated, and `uncoveredOutcome` reports one that produces the ending on a different condition. So an intent saying "404 when a read of `aws.dynamodb:Invoices` finds nothing" fails when the code's 404 turns on the row being present. When the unit makes no effect against that boundary anywhere, in any transition, the message points at the boundary as the cause instead.
|
|
57
58
|
|
|
58
59
|
The boundary resolves through `namesBoundary` again, and `finds` has to agree when the clause states it. A clause about an input, and a clause left as a sentence, are prose to this pass and are not compared: the paths and the words have no counterpart in the summary to check against.
|
|
59
60
|
|
|
61
|
+
## When a store was renamed
|
|
62
|
+
|
|
63
|
+
A store that gets renamed in code without the intent doc catching up produces a pile of findings from one cause: the old name is declared and never touched, the new one is touched and never declared, and both an `uncoveredOutcome` (for the vanished store) and an `undeclaredOutcome` (for the one that appeared) fire for every verb and every outcome that used it. `renamedBoundary` folds all of those into one finding when the pairing is unambiguous: the two boundaries share a system prefix, their verbs match exactly, the new one satisfies every declared use the old one had, and each side has exactly one candidate on the other. It stays an error, same as the findings it replaces: the document and the code still disagree, and folding is a guess about the cause rather than a change in whether that disagreement matters.
|
|
64
|
+
|
|
65
|
+
Pairing only considers reads and writes. A queue channel or a deployed unit is addressed by name, so a different callee produces its own uncoveredOutcome and undeclaredOutcome findings without folding.
|
|
66
|
+
|
|
60
67
|
## The coverage question, both ways
|
|
61
68
|
|
|
62
69
|
The three scenario kinds ask whether a scenario points at an outcome that exists. `undescribedOutcome` asks it the other way: which declared behaviour has nobody written down a reason for. That is the question a product reader has, and the two artifacts to answer it from are the same two.
|
package/dist/index.cjs
CHANGED
|
@@ -335,20 +335,33 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
335
335
|
const everyEffect = impl.transitions.flatMap(
|
|
336
336
|
(t) => codeEffectsOf(t, deployment)
|
|
337
337
|
);
|
|
338
|
+
const vanished = [];
|
|
339
|
+
const undeclared = [];
|
|
338
340
|
for (const outcome of intent.outcomes) {
|
|
339
341
|
const reached = outcome.kind === "effect" ? everyEffect : codeOutcomes.filter((co) => outcomeMatches(outcome, co)).flatMap((co) => co.effects);
|
|
340
342
|
for (const effect of outcome.effects) {
|
|
341
343
|
if (reached.some((made) => effectMatches(effect, made))) {
|
|
342
344
|
continue;
|
|
343
345
|
}
|
|
344
|
-
|
|
346
|
+
const finding = {
|
|
345
347
|
kind: "uncoveredOutcome",
|
|
346
348
|
severity: "error",
|
|
347
349
|
boundary,
|
|
348
350
|
intent: { name: intent.name, outcomeId: outcome.id },
|
|
349
351
|
code: ref,
|
|
350
352
|
message: `Intent "${intent.name}" declares that ${outcome.id} results in ${describeEffect(effect)} at ${boundary}; no transition of ${impl.identity.name} does that.${unsettledNote(reached, effect, deployment)}`
|
|
351
|
-
}
|
|
353
|
+
};
|
|
354
|
+
findings.push(finding);
|
|
355
|
+
if (neverTouchesBoundary(effect.names, everyEffect)) {
|
|
356
|
+
vanished.push({
|
|
357
|
+
finding,
|
|
358
|
+
boundary: effect.names,
|
|
359
|
+
does: effect.does,
|
|
360
|
+
kind: "effect",
|
|
361
|
+
effect,
|
|
362
|
+
reached
|
|
363
|
+
});
|
|
364
|
+
}
|
|
352
365
|
}
|
|
353
366
|
if (outcome.kind === "effect") {
|
|
354
367
|
continue;
|
|
@@ -375,14 +388,32 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
375
388
|
const unmet = stated.find(
|
|
376
389
|
(c) => !ending.some((co) => conditionMet(c, co.turnsOn))
|
|
377
390
|
);
|
|
378
|
-
|
|
391
|
+
const finding = {
|
|
379
392
|
kind: "uncoveredOutcome",
|
|
380
393
|
severity: "error",
|
|
381
394
|
boundary,
|
|
382
395
|
intent: { name: intent.name, outcomeId: outcome.id },
|
|
383
396
|
code: ref,
|
|
384
|
-
message:
|
|
385
|
-
|
|
397
|
+
message: unmetConditionMessage(
|
|
398
|
+
intent,
|
|
399
|
+
outcome,
|
|
400
|
+
boundary,
|
|
401
|
+
impl,
|
|
402
|
+
unmet,
|
|
403
|
+
everyEffect
|
|
404
|
+
)
|
|
405
|
+
};
|
|
406
|
+
findings.push(finding);
|
|
407
|
+
if (unmet !== void 0 && neverTouchesBoundary(unmet.at.names, everyEffect)) {
|
|
408
|
+
vanished.push({
|
|
409
|
+
finding,
|
|
410
|
+
boundary: unmet.at.names,
|
|
411
|
+
does: unmet.at.does,
|
|
412
|
+
kind: "condition",
|
|
413
|
+
unmet,
|
|
414
|
+
ending
|
|
415
|
+
});
|
|
416
|
+
}
|
|
386
417
|
continue;
|
|
387
418
|
}
|
|
388
419
|
const declaredBody = outcome.body;
|
|
@@ -436,16 +467,163 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
436
467
|
continue;
|
|
437
468
|
}
|
|
438
469
|
said.add(spelled);
|
|
439
|
-
|
|
470
|
+
const finding = {
|
|
440
471
|
kind: "undeclaredOutcome",
|
|
441
472
|
severity: "info",
|
|
442
473
|
boundary,
|
|
443
474
|
intent: { name: intent.name },
|
|
444
475
|
code: ref,
|
|
445
476
|
message: `${impl.identity.name} ${spelled} at ${boundary}; intent "${intent.name}" does not declare it.`
|
|
477
|
+
};
|
|
478
|
+
findings.push(finding);
|
|
479
|
+
undeclared.push({ finding, boundary: made.label, does: made.does });
|
|
480
|
+
}
|
|
481
|
+
return foldRenamedBoundaries(
|
|
482
|
+
intent,
|
|
483
|
+
impl,
|
|
484
|
+
boundary,
|
|
485
|
+
ref,
|
|
486
|
+
findings,
|
|
487
|
+
vanished,
|
|
488
|
+
undeclared
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
function neverTouchesBoundary(names, everyEffect) {
|
|
492
|
+
return everyEffect.every((made) => !(0, import_ir_core.namesBoundary)(names, made.binding));
|
|
493
|
+
}
|
|
494
|
+
function unmetConditionMessage(intent, outcome, boundary, impl, unmet, everyEffect) {
|
|
495
|
+
const declared = `Intent "${intent.name}" declares ${describeOutcome(outcome)} at ${boundary} when ${unmet?.said ?? outcome.when}`;
|
|
496
|
+
if (unmet !== void 0 && neverTouchesBoundary(unmet.at.names, everyEffect)) {
|
|
497
|
+
return `${declared}; ${impl.identity.name} never ${unmet.at.does} ${unmet.at.names}.`;
|
|
498
|
+
}
|
|
499
|
+
return `${declared}; ${impl.identity.name} produces it on a different condition.`;
|
|
500
|
+
}
|
|
501
|
+
function satisfiedBy(use, candidate) {
|
|
502
|
+
if (use.kind === "effect") {
|
|
503
|
+
return use.reached.some(
|
|
504
|
+
(made) => effectMatches({ ...use.effect, names: candidate }, made)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return use.ending.some(
|
|
508
|
+
(co) => conditionMet(
|
|
509
|
+
{ ...use.unmet, at: { ...use.unmet.at, names: candidate } },
|
|
510
|
+
co.turnsOn
|
|
511
|
+
)
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
function foldRenamedBoundaries(intent, impl, boundary, ref, findings, vanished, undeclared) {
|
|
515
|
+
const vanishedByBoundary = groupByBoundary(vanished);
|
|
516
|
+
const undeclaredByBoundary = groupByBoundary(undeclared);
|
|
517
|
+
const pairings = matchRenamedBoundaries(
|
|
518
|
+
vanishedByBoundary,
|
|
519
|
+
undeclaredByBoundary
|
|
520
|
+
);
|
|
521
|
+
if (pairings.length === 0) {
|
|
522
|
+
return findings;
|
|
523
|
+
}
|
|
524
|
+
const explained = /* @__PURE__ */ new Set();
|
|
525
|
+
const renamed = [];
|
|
526
|
+
for (const pairing of pairings) {
|
|
527
|
+
for (const use of pairing.vanishedUses) {
|
|
528
|
+
explained.add(use.finding);
|
|
529
|
+
}
|
|
530
|
+
for (const use of pairing.undeclaredUses) {
|
|
531
|
+
explained.add(use.finding);
|
|
532
|
+
}
|
|
533
|
+
renamed.push({
|
|
534
|
+
kind: "renamedBoundary",
|
|
535
|
+
severity: "error",
|
|
536
|
+
boundary,
|
|
537
|
+
intent: { name: intent.name },
|
|
538
|
+
code: ref,
|
|
539
|
+
message: `Intent "${intent.name}" declares ${pairing.from}; ${impl.identity.name} ${joinVerbs(pairing.does)} ${pairing.to} instead, with the same outcomes. If the store was renamed, update the intent.`
|
|
446
540
|
});
|
|
447
541
|
}
|
|
448
|
-
return findings;
|
|
542
|
+
return [...findings.filter((finding) => !explained.has(finding)), ...renamed];
|
|
543
|
+
}
|
|
544
|
+
function matchRenamedBoundaries(vanishedByBoundary, undeclaredByBoundary) {
|
|
545
|
+
const candidatesFor = /* @__PURE__ */ new Map();
|
|
546
|
+
const candidateCountAgainst = /* @__PURE__ */ new Map();
|
|
547
|
+
for (const [from, uses] of vanishedByBoundary) {
|
|
548
|
+
const verbs = new Set(uses.map((use) => use.does));
|
|
549
|
+
if (!isStorageVerbs(verbs)) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
for (const [to, madeUses] of undeclaredByBoundary) {
|
|
553
|
+
if (!sameSystem(from, to)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (!sameVerbs(verbs, new Set(madeUses.map((use) => use.does)))) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (!uses.every((use) => satisfiedBy(use, to))) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
pushCandidate(candidatesFor, from, {
|
|
563
|
+
from,
|
|
564
|
+
to,
|
|
565
|
+
does: verbs,
|
|
566
|
+
vanishedUses: uses,
|
|
567
|
+
undeclaredUses: madeUses
|
|
568
|
+
});
|
|
569
|
+
incrementCount(candidateCountAgainst, to);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const pairings = [];
|
|
573
|
+
for (const [, candidates] of candidatesFor) {
|
|
574
|
+
if (candidates.length !== 1) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const [pairing] = candidates;
|
|
578
|
+
if (candidateCountAgainst.get(pairing.to) !== 1) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
pairings.push(pairing);
|
|
582
|
+
}
|
|
583
|
+
return pairings;
|
|
584
|
+
}
|
|
585
|
+
function pushCandidate(map, key, value) {
|
|
586
|
+
const existing = map.get(key);
|
|
587
|
+
if (existing === void 0) {
|
|
588
|
+
map.set(key, [value]);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
existing.push(value);
|
|
592
|
+
}
|
|
593
|
+
function incrementCount(map, key) {
|
|
594
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
595
|
+
}
|
|
596
|
+
function isStorageVerbs(verbs) {
|
|
597
|
+
return [...verbs].every((verb) => verb === "reads" || verb === "writes");
|
|
598
|
+
}
|
|
599
|
+
function sameVerbs(a, b) {
|
|
600
|
+
return a.size === b.size && [...a].every((verb) => b.has(verb));
|
|
601
|
+
}
|
|
602
|
+
function sameSystem(a, b) {
|
|
603
|
+
return a !== b && systemPrefix(a) === systemPrefix(b);
|
|
604
|
+
}
|
|
605
|
+
function systemPrefix(name) {
|
|
606
|
+
const colon = name.indexOf(":");
|
|
607
|
+
return colon === -1 ? name : name.slice(0, colon + 1);
|
|
608
|
+
}
|
|
609
|
+
function joinVerbs(verbs) {
|
|
610
|
+
const list = [...verbs];
|
|
611
|
+
if (list.length < 2) {
|
|
612
|
+
return list.join("");
|
|
613
|
+
}
|
|
614
|
+
return `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
615
|
+
}
|
|
616
|
+
function groupByBoundary(items) {
|
|
617
|
+
const groups = /* @__PURE__ */ new Map();
|
|
618
|
+
for (const item of items) {
|
|
619
|
+
const bucket = groups.get(item.boundary);
|
|
620
|
+
if (bucket === void 0) {
|
|
621
|
+
groups.set(item.boundary, [item]);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
bucket.push(item);
|
|
625
|
+
}
|
|
626
|
+
return groups;
|
|
449
627
|
}
|
|
450
628
|
function unsettledNote(reached, declared, deployment) {
|
|
451
629
|
const variables = /* @__PURE__ */ new Set();
|
package/dist/index.js
CHANGED
|
@@ -330,20 +330,33 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
330
330
|
const everyEffect = impl.transitions.flatMap(
|
|
331
331
|
(t) => codeEffectsOf(t, deployment)
|
|
332
332
|
);
|
|
333
|
+
const vanished = [];
|
|
334
|
+
const undeclared = [];
|
|
333
335
|
for (const outcome of intent.outcomes) {
|
|
334
336
|
const reached = outcome.kind === "effect" ? everyEffect : codeOutcomes.filter((co) => outcomeMatches(outcome, co)).flatMap((co) => co.effects);
|
|
335
337
|
for (const effect of outcome.effects) {
|
|
336
338
|
if (reached.some((made) => effectMatches(effect, made))) {
|
|
337
339
|
continue;
|
|
338
340
|
}
|
|
339
|
-
|
|
341
|
+
const finding = {
|
|
340
342
|
kind: "uncoveredOutcome",
|
|
341
343
|
severity: "error",
|
|
342
344
|
boundary,
|
|
343
345
|
intent: { name: intent.name, outcomeId: outcome.id },
|
|
344
346
|
code: ref,
|
|
345
347
|
message: `Intent "${intent.name}" declares that ${outcome.id} results in ${describeEffect(effect)} at ${boundary}; no transition of ${impl.identity.name} does that.${unsettledNote(reached, effect, deployment)}`
|
|
346
|
-
}
|
|
348
|
+
};
|
|
349
|
+
findings.push(finding);
|
|
350
|
+
if (neverTouchesBoundary(effect.names, everyEffect)) {
|
|
351
|
+
vanished.push({
|
|
352
|
+
finding,
|
|
353
|
+
boundary: effect.names,
|
|
354
|
+
does: effect.does,
|
|
355
|
+
kind: "effect",
|
|
356
|
+
effect,
|
|
357
|
+
reached
|
|
358
|
+
});
|
|
359
|
+
}
|
|
347
360
|
}
|
|
348
361
|
if (outcome.kind === "effect") {
|
|
349
362
|
continue;
|
|
@@ -370,14 +383,32 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
370
383
|
const unmet = stated.find(
|
|
371
384
|
(c) => !ending.some((co) => conditionMet(c, co.turnsOn))
|
|
372
385
|
);
|
|
373
|
-
|
|
386
|
+
const finding = {
|
|
374
387
|
kind: "uncoveredOutcome",
|
|
375
388
|
severity: "error",
|
|
376
389
|
boundary,
|
|
377
390
|
intent: { name: intent.name, outcomeId: outcome.id },
|
|
378
391
|
code: ref,
|
|
379
|
-
message:
|
|
380
|
-
|
|
392
|
+
message: unmetConditionMessage(
|
|
393
|
+
intent,
|
|
394
|
+
outcome,
|
|
395
|
+
boundary,
|
|
396
|
+
impl,
|
|
397
|
+
unmet,
|
|
398
|
+
everyEffect
|
|
399
|
+
)
|
|
400
|
+
};
|
|
401
|
+
findings.push(finding);
|
|
402
|
+
if (unmet !== void 0 && neverTouchesBoundary(unmet.at.names, everyEffect)) {
|
|
403
|
+
vanished.push({
|
|
404
|
+
finding,
|
|
405
|
+
boundary: unmet.at.names,
|
|
406
|
+
does: unmet.at.does,
|
|
407
|
+
kind: "condition",
|
|
408
|
+
unmet,
|
|
409
|
+
ending
|
|
410
|
+
});
|
|
411
|
+
}
|
|
381
412
|
continue;
|
|
382
413
|
}
|
|
383
414
|
const declaredBody = outcome.body;
|
|
@@ -431,16 +462,163 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
431
462
|
continue;
|
|
432
463
|
}
|
|
433
464
|
said.add(spelled);
|
|
434
|
-
|
|
465
|
+
const finding = {
|
|
435
466
|
kind: "undeclaredOutcome",
|
|
436
467
|
severity: "info",
|
|
437
468
|
boundary,
|
|
438
469
|
intent: { name: intent.name },
|
|
439
470
|
code: ref,
|
|
440
471
|
message: `${impl.identity.name} ${spelled} at ${boundary}; intent "${intent.name}" does not declare it.`
|
|
472
|
+
};
|
|
473
|
+
findings.push(finding);
|
|
474
|
+
undeclared.push({ finding, boundary: made.label, does: made.does });
|
|
475
|
+
}
|
|
476
|
+
return foldRenamedBoundaries(
|
|
477
|
+
intent,
|
|
478
|
+
impl,
|
|
479
|
+
boundary,
|
|
480
|
+
ref,
|
|
481
|
+
findings,
|
|
482
|
+
vanished,
|
|
483
|
+
undeclared
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
function neverTouchesBoundary(names, everyEffect) {
|
|
487
|
+
return everyEffect.every((made) => !namesBoundary(names, made.binding));
|
|
488
|
+
}
|
|
489
|
+
function unmetConditionMessage(intent, outcome, boundary, impl, unmet, everyEffect) {
|
|
490
|
+
const declared = `Intent "${intent.name}" declares ${describeOutcome(outcome)} at ${boundary} when ${unmet?.said ?? outcome.when}`;
|
|
491
|
+
if (unmet !== void 0 && neverTouchesBoundary(unmet.at.names, everyEffect)) {
|
|
492
|
+
return `${declared}; ${impl.identity.name} never ${unmet.at.does} ${unmet.at.names}.`;
|
|
493
|
+
}
|
|
494
|
+
return `${declared}; ${impl.identity.name} produces it on a different condition.`;
|
|
495
|
+
}
|
|
496
|
+
function satisfiedBy(use, candidate) {
|
|
497
|
+
if (use.kind === "effect") {
|
|
498
|
+
return use.reached.some(
|
|
499
|
+
(made) => effectMatches({ ...use.effect, names: candidate }, made)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return use.ending.some(
|
|
503
|
+
(co) => conditionMet(
|
|
504
|
+
{ ...use.unmet, at: { ...use.unmet.at, names: candidate } },
|
|
505
|
+
co.turnsOn
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
function foldRenamedBoundaries(intent, impl, boundary, ref, findings, vanished, undeclared) {
|
|
510
|
+
const vanishedByBoundary = groupByBoundary(vanished);
|
|
511
|
+
const undeclaredByBoundary = groupByBoundary(undeclared);
|
|
512
|
+
const pairings = matchRenamedBoundaries(
|
|
513
|
+
vanishedByBoundary,
|
|
514
|
+
undeclaredByBoundary
|
|
515
|
+
);
|
|
516
|
+
if (pairings.length === 0) {
|
|
517
|
+
return findings;
|
|
518
|
+
}
|
|
519
|
+
const explained = /* @__PURE__ */ new Set();
|
|
520
|
+
const renamed = [];
|
|
521
|
+
for (const pairing of pairings) {
|
|
522
|
+
for (const use of pairing.vanishedUses) {
|
|
523
|
+
explained.add(use.finding);
|
|
524
|
+
}
|
|
525
|
+
for (const use of pairing.undeclaredUses) {
|
|
526
|
+
explained.add(use.finding);
|
|
527
|
+
}
|
|
528
|
+
renamed.push({
|
|
529
|
+
kind: "renamedBoundary",
|
|
530
|
+
severity: "error",
|
|
531
|
+
boundary,
|
|
532
|
+
intent: { name: intent.name },
|
|
533
|
+
code: ref,
|
|
534
|
+
message: `Intent "${intent.name}" declares ${pairing.from}; ${impl.identity.name} ${joinVerbs(pairing.does)} ${pairing.to} instead, with the same outcomes. If the store was renamed, update the intent.`
|
|
441
535
|
});
|
|
442
536
|
}
|
|
443
|
-
return findings;
|
|
537
|
+
return [...findings.filter((finding) => !explained.has(finding)), ...renamed];
|
|
538
|
+
}
|
|
539
|
+
function matchRenamedBoundaries(vanishedByBoundary, undeclaredByBoundary) {
|
|
540
|
+
const candidatesFor = /* @__PURE__ */ new Map();
|
|
541
|
+
const candidateCountAgainst = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const [from, uses] of vanishedByBoundary) {
|
|
543
|
+
const verbs = new Set(uses.map((use) => use.does));
|
|
544
|
+
if (!isStorageVerbs(verbs)) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
for (const [to, madeUses] of undeclaredByBoundary) {
|
|
548
|
+
if (!sameSystem(from, to)) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (!sameVerbs(verbs, new Set(madeUses.map((use) => use.does)))) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (!uses.every((use) => satisfiedBy(use, to))) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
pushCandidate(candidatesFor, from, {
|
|
558
|
+
from,
|
|
559
|
+
to,
|
|
560
|
+
does: verbs,
|
|
561
|
+
vanishedUses: uses,
|
|
562
|
+
undeclaredUses: madeUses
|
|
563
|
+
});
|
|
564
|
+
incrementCount(candidateCountAgainst, to);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const pairings = [];
|
|
568
|
+
for (const [, candidates] of candidatesFor) {
|
|
569
|
+
if (candidates.length !== 1) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const [pairing] = candidates;
|
|
573
|
+
if (candidateCountAgainst.get(pairing.to) !== 1) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
pairings.push(pairing);
|
|
577
|
+
}
|
|
578
|
+
return pairings;
|
|
579
|
+
}
|
|
580
|
+
function pushCandidate(map, key, value) {
|
|
581
|
+
const existing = map.get(key);
|
|
582
|
+
if (existing === void 0) {
|
|
583
|
+
map.set(key, [value]);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
existing.push(value);
|
|
587
|
+
}
|
|
588
|
+
function incrementCount(map, key) {
|
|
589
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
590
|
+
}
|
|
591
|
+
function isStorageVerbs(verbs) {
|
|
592
|
+
return [...verbs].every((verb) => verb === "reads" || verb === "writes");
|
|
593
|
+
}
|
|
594
|
+
function sameVerbs(a, b) {
|
|
595
|
+
return a.size === b.size && [...a].every((verb) => b.has(verb));
|
|
596
|
+
}
|
|
597
|
+
function sameSystem(a, b) {
|
|
598
|
+
return a !== b && systemPrefix(a) === systemPrefix(b);
|
|
599
|
+
}
|
|
600
|
+
function systemPrefix(name) {
|
|
601
|
+
const colon = name.indexOf(":");
|
|
602
|
+
return colon === -1 ? name : name.slice(0, colon + 1);
|
|
603
|
+
}
|
|
604
|
+
function joinVerbs(verbs) {
|
|
605
|
+
const list = [...verbs];
|
|
606
|
+
if (list.length < 2) {
|
|
607
|
+
return list.join("");
|
|
608
|
+
}
|
|
609
|
+
return `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
610
|
+
}
|
|
611
|
+
function groupByBoundary(items) {
|
|
612
|
+
const groups = /* @__PURE__ */ new Map();
|
|
613
|
+
for (const item of items) {
|
|
614
|
+
const bucket = groups.get(item.boundary);
|
|
615
|
+
if (bucket === void 0) {
|
|
616
|
+
groups.set(item.boundary, [item]);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
bucket.push(item);
|
|
620
|
+
}
|
|
621
|
+
return groups;
|
|
444
622
|
}
|
|
445
623
|
function unsettledNote(reached, declared, deployment) {
|
|
446
624
|
const variables = /* @__PURE__ */ new Set();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/checker-intent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pair team-authored intent (IntentSummary) against derived code (BehavioralSummary) and report where they diverge.",
|
|
6
6
|
"keywords": [
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"zod": "^4.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@suss/behavioral-ir": "0.
|
|
51
|
-
"@suss/intent-ir": "0.
|
|
52
|
-
"@suss/ir-core": "0.
|
|
50
|
+
"@suss/behavioral-ir": "0.22.1",
|
|
51
|
+
"@suss/intent-ir": "0.22.1",
|
|
52
|
+
"@suss/ir-core": "0.22.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"tsup": "*",
|