@suss/checker-intent 0.32.0 → 0.33.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 +2 -2
- package/dist/index.cjs +119 -15
- package/dist/index.js +110 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -100,8 +100,8 @@ It does **not** depend on
|
|
|
100
100
|
|
|
101
101
|
## More
|
|
102
102
|
|
|
103
|
-
- [Documentation](https://
|
|
104
|
-
- [Every package and pack suss ships](https://
|
|
103
|
+
- [Documentation](https://suss.sh/)
|
|
104
|
+
- [Every package and pack suss ships](https://suss.sh/packs/catalog)
|
|
105
105
|
- [Source and issues](https://github.com/nimbuscloud-ai/suss)
|
|
106
106
|
|
|
107
107
|
## Coverage
|
package/dist/index.cjs
CHANGED
|
@@ -25,15 +25,109 @@ __export(index_exports, {
|
|
|
25
25
|
whatWouldKeyIt: () => whatWouldKeyIt
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
|
-
var
|
|
28
|
+
var import_behavioral_ir2 = require("@suss/behavioral-ir");
|
|
29
29
|
var import_ir_core = require("@suss/ir-core");
|
|
30
|
+
|
|
31
|
+
// src/receivedInput.ts
|
|
32
|
+
var import_behavioral_ir = require("@suss/behavioral-ir");
|
|
33
|
+
function overlaps(a, b) {
|
|
34
|
+
const left = comparable(a);
|
|
35
|
+
const right = comparable(b);
|
|
36
|
+
const shared = Math.min(left.length, right.length);
|
|
37
|
+
for (let i = 0; i < shared; i += 1) {
|
|
38
|
+
if (left[i] !== right[i]) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
function comparable(path) {
|
|
45
|
+
if (path[0] !== "headers") {
|
|
46
|
+
return [...path];
|
|
47
|
+
}
|
|
48
|
+
return path.map(
|
|
49
|
+
(segment, index) => index === 1 ? segment.toLowerCase() : segment
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function checkReceivesBlock(pairing) {
|
|
53
|
+
const declared = pairing.intent.receives;
|
|
54
|
+
if (declared.length === 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const result = (0, import_behavioral_ir.boundaryInputReads)(
|
|
58
|
+
pairing.impl,
|
|
59
|
+
pairing.intent.boundary,
|
|
60
|
+
pairing.wrappers
|
|
61
|
+
);
|
|
62
|
+
if (!result.read) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const read = result.reads.paths;
|
|
66
|
+
const unread = declared.filter(
|
|
67
|
+
(field) => !read.some((path) => overlaps(path, field.path))
|
|
68
|
+
);
|
|
69
|
+
const alreadyDescribed = DESCRIBED_WITHOUT_LISTING[pairing.intent.boundary.semantics.name];
|
|
70
|
+
const undeclared = read.filter(
|
|
71
|
+
(path) => !declared.some((field) => overlaps(path, field.path)) && !alreadyDescribed(declared, path)
|
|
72
|
+
);
|
|
73
|
+
return [
|
|
74
|
+
...unread.map((field) => unreadFinding(pairing, field)),
|
|
75
|
+
...undeclaredFindings(pairing, undeclared)
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
var LISTS_EVERYTHING = () => false;
|
|
79
|
+
var DESCRIBED_WITHOUT_LISTING = {
|
|
80
|
+
rest: (declared, path) => path[0] === "body" && declared.some((field) => field.path[0] === "body"),
|
|
81
|
+
"function-call": LISTS_EVERYTHING,
|
|
82
|
+
"message-bus": LISTS_EVERYTHING,
|
|
83
|
+
storage: LISTS_EVERYTHING,
|
|
84
|
+
"unit-invocation": LISTS_EVERYTHING,
|
|
85
|
+
"graphql-resolver": LISTS_EVERYTHING,
|
|
86
|
+
"graphql-operation": LISTS_EVERYTHING,
|
|
87
|
+
"runtime-config": LISTS_EVERYTHING,
|
|
88
|
+
metric: LISTS_EVERYTHING
|
|
89
|
+
};
|
|
90
|
+
function unreadFinding(pairing, field) {
|
|
91
|
+
const needsIt = field.required ? " and needs it" : "";
|
|
92
|
+
return {
|
|
93
|
+
kind: "unreadInputField",
|
|
94
|
+
severity: field.required ? "warning" : "info",
|
|
95
|
+
boundary: pairing.boundary,
|
|
96
|
+
intent: { name: pairing.intent.name },
|
|
97
|
+
code: pairing.code,
|
|
98
|
+
message: `Intent "${pairing.intent.name}" says ${pairing.boundary} receives ${(0, import_behavioral_ir.formatPath)(field.path)}${needsIt}; ${pairing.impl.identity.name} never reads it.`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function undeclaredFindings(pairing, undeclared) {
|
|
102
|
+
const said = /* @__PURE__ */ new Set();
|
|
103
|
+
const findings = [];
|
|
104
|
+
for (const path of undeclared) {
|
|
105
|
+
const spelled = (0, import_behavioral_ir.formatPath)(path);
|
|
106
|
+
if (said.has(spelled)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
said.add(spelled);
|
|
110
|
+
findings.push({
|
|
111
|
+
kind: "undeclaredInputRead",
|
|
112
|
+
severity: "info",
|
|
113
|
+
boundary: pairing.boundary,
|
|
114
|
+
intent: { name: pairing.intent.name },
|
|
115
|
+
code: pairing.code,
|
|
116
|
+
message: `${pairing.impl.identity.name} reads ${spelled} off what it was handed at ${pairing.boundary}; intent "${pairing.intent.name}" does not declare it under receives.`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/index.ts
|
|
30
123
|
function checkIntentAgreement(intents, code) {
|
|
31
124
|
const findings = [];
|
|
32
125
|
const checked = [];
|
|
33
126
|
const unchecked = [];
|
|
34
127
|
const codeByBoundary = indexCodeByBoundary(code);
|
|
35
128
|
const boundaryByName = indexBoundaryIntentsByName(intents);
|
|
36
|
-
const deploymentOfUnit = (0,
|
|
129
|
+
const deploymentOfUnit = (0, import_behavioral_ir2.deploymentOf)(code);
|
|
130
|
+
const wrappersOfUnit = (0, import_behavioral_ir2.wrappersAround)(code);
|
|
37
131
|
for (const intent of intents) {
|
|
38
132
|
if (intent.kind === "prd") {
|
|
39
133
|
const result2 = checkPrdCoverage(intent, boundaryByName);
|
|
@@ -44,7 +138,8 @@ function checkIntentAgreement(intents, code) {
|
|
|
44
138
|
const result = checkBoundaryIntent(
|
|
45
139
|
intent,
|
|
46
140
|
codeByBoundary,
|
|
47
|
-
deploymentOfUnit
|
|
141
|
+
deploymentOfUnit,
|
|
142
|
+
wrappersOfUnit
|
|
48
143
|
);
|
|
49
144
|
findings.push(...withProvenance(result.findings, intent.source));
|
|
50
145
|
checked.push(...result.checked);
|
|
@@ -88,7 +183,7 @@ function checkOutcomesDescribed(intents) {
|
|
|
88
183
|
}
|
|
89
184
|
return findings;
|
|
90
185
|
}
|
|
91
|
-
function checkBoundaryIntent(intent, codeByBoundary, deploymentOfUnit) {
|
|
186
|
+
function checkBoundaryIntent(intent, codeByBoundary, deploymentOfUnit, wrappersOfUnit) {
|
|
92
187
|
const key = (0, import_ir_core.pairingKey)(intent.boundary);
|
|
93
188
|
if (key === null) {
|
|
94
189
|
return {
|
|
@@ -142,7 +237,13 @@ function checkBoundaryIntent(intent, codeByBoundary, deploymentOfUnit) {
|
|
|
142
237
|
const findings = [];
|
|
143
238
|
for (const impl of impls) {
|
|
144
239
|
findings.push(
|
|
145
|
-
...compareIntentToImpl(
|
|
240
|
+
...compareIntentToImpl(
|
|
241
|
+
intent,
|
|
242
|
+
impl,
|
|
243
|
+
label,
|
|
244
|
+
deploymentOfUnit(impl),
|
|
245
|
+
wrappersOfUnit(impl)
|
|
246
|
+
)
|
|
146
247
|
);
|
|
147
248
|
}
|
|
148
249
|
return {
|
|
@@ -159,7 +260,7 @@ function checkBoundaryIntent(intent, codeByBoundary, deploymentOfUnit) {
|
|
|
159
260
|
};
|
|
160
261
|
}
|
|
161
262
|
function codeRef(impl) {
|
|
162
|
-
return (0,
|
|
263
|
+
return (0, import_behavioral_ir2.summaryRef)(impl);
|
|
163
264
|
}
|
|
164
265
|
function checkPrdCoverage(prd, boundaryByName) {
|
|
165
266
|
const findings = [];
|
|
@@ -303,8 +404,8 @@ function applyIntentSuppressions(findings, rules, opts = {}) {
|
|
|
303
404
|
}
|
|
304
405
|
function indexCodeByBoundary(code) {
|
|
305
406
|
const byKey = /* @__PURE__ */ new Map();
|
|
306
|
-
for (const summary of (0,
|
|
307
|
-
if (
|
|
407
|
+
for (const summary of (0, import_behavioral_ir2.withDeclaredDelivery)(code)) {
|
|
408
|
+
if (import_behavioral_ir2.BOUNDARY_ROLE[summary.kind] !== "provider") {
|
|
308
409
|
continue;
|
|
309
410
|
}
|
|
310
411
|
if (summary.confidence.source === "declared") {
|
|
@@ -327,10 +428,10 @@ function indexCodeByBoundary(code) {
|
|
|
327
428
|
}
|
|
328
429
|
return byKey;
|
|
329
430
|
}
|
|
330
|
-
function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
431
|
+
function compareIntentToImpl(intent, impl, boundary, deployment, wrappers) {
|
|
331
432
|
const findings = [];
|
|
332
433
|
const ref = codeRef(impl);
|
|
333
|
-
const calls = (0,
|
|
434
|
+
const calls = (0, import_behavioral_ir2.boundaryCalls)(impl);
|
|
334
435
|
const codeOutcomes = impl.transitions.map((t) => toCodeOutcome(t, calls, deployment)).filter((o) => o !== null);
|
|
335
436
|
const everyEffect = impl.transitions.flatMap(
|
|
336
437
|
(t) => codeEffectsOf(t, deployment)
|
|
@@ -478,6 +579,9 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
478
579
|
findings.push(finding);
|
|
479
580
|
undeclared.push({ finding, boundary: made.label, does: made.does });
|
|
480
581
|
}
|
|
582
|
+
findings.push(
|
|
583
|
+
...checkReceivesBlock({ intent, impl, wrappers, boundary, code: ref })
|
|
584
|
+
);
|
|
481
585
|
return foldRenamedBoundaries(
|
|
482
586
|
intent,
|
|
483
587
|
impl,
|
|
@@ -657,9 +761,9 @@ function conditionMet(declared, turnsOn) {
|
|
|
657
761
|
function toCodeOutcome(t, calls, deployment) {
|
|
658
762
|
const output = t.output;
|
|
659
763
|
const effects = codeEffectsOf(t, deployment);
|
|
660
|
-
const turnsOn = (0,
|
|
764
|
+
const turnsOn = (0, import_behavioral_ir2.boundaryGuardsOf)(t, calls).map((guard) => ({
|
|
661
765
|
...guard,
|
|
662
|
-
binding: (0,
|
|
766
|
+
binding: (0, import_behavioral_ir2.groundBinding)(guard.binding, deployment)
|
|
663
767
|
}));
|
|
664
768
|
if (output.type === "response") {
|
|
665
769
|
const status = output.statusCode !== null && output.statusCode.type === "literal" ? Number(output.statusCode.value) : null;
|
|
@@ -700,13 +804,13 @@ function codeEffectsOf(t, deployment) {
|
|
|
700
804
|
if (effect.type !== "interaction") {
|
|
701
805
|
continue;
|
|
702
806
|
}
|
|
703
|
-
if ((0,
|
|
807
|
+
if ((0, import_behavioral_ir2.goesThroughRelation)(effect.interaction)) {
|
|
704
808
|
continue;
|
|
705
809
|
}
|
|
706
|
-
const binding = (0,
|
|
810
|
+
const binding = (0, import_behavioral_ir2.groundBinding)(effect.binding, deployment);
|
|
707
811
|
const label = (0, import_ir_core.displayLabel)(binding);
|
|
708
812
|
const touched = accessDetail(effect.interaction);
|
|
709
|
-
for (const does of (0,
|
|
813
|
+
for (const does of (0, import_behavioral_ir2.relationsOf)(effect.interaction)) {
|
|
710
814
|
reached.push({ does, binding, label, ...touched });
|
|
711
815
|
}
|
|
712
816
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
groundBinding,
|
|
9
9
|
relationsOf,
|
|
10
10
|
summaryRef,
|
|
11
|
-
withDeclaredDelivery
|
|
11
|
+
withDeclaredDelivery,
|
|
12
|
+
wrappersAround
|
|
12
13
|
} from "@suss/behavioral-ir";
|
|
13
14
|
import {
|
|
14
15
|
applySuppressionsToFindings,
|
|
@@ -22,6 +23,99 @@ import {
|
|
|
22
23
|
ruleBoundaryMatchesKey,
|
|
23
24
|
semanticsAgree
|
|
24
25
|
} from "@suss/ir-core";
|
|
26
|
+
|
|
27
|
+
// src/receivedInput.ts
|
|
28
|
+
import { boundaryInputReads, formatPath } from "@suss/behavioral-ir";
|
|
29
|
+
function overlaps(a, b) {
|
|
30
|
+
const left = comparable(a);
|
|
31
|
+
const right = comparable(b);
|
|
32
|
+
const shared = Math.min(left.length, right.length);
|
|
33
|
+
for (let i = 0; i < shared; i += 1) {
|
|
34
|
+
if (left[i] !== right[i]) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function comparable(path) {
|
|
41
|
+
if (path[0] !== "headers") {
|
|
42
|
+
return [...path];
|
|
43
|
+
}
|
|
44
|
+
return path.map(
|
|
45
|
+
(segment, index) => index === 1 ? segment.toLowerCase() : segment
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function checkReceivesBlock(pairing) {
|
|
49
|
+
const declared = pairing.intent.receives;
|
|
50
|
+
if (declared.length === 0) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const result = boundaryInputReads(
|
|
54
|
+
pairing.impl,
|
|
55
|
+
pairing.intent.boundary,
|
|
56
|
+
pairing.wrappers
|
|
57
|
+
);
|
|
58
|
+
if (!result.read) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const read = result.reads.paths;
|
|
62
|
+
const unread = declared.filter(
|
|
63
|
+
(field) => !read.some((path) => overlaps(path, field.path))
|
|
64
|
+
);
|
|
65
|
+
const alreadyDescribed = DESCRIBED_WITHOUT_LISTING[pairing.intent.boundary.semantics.name];
|
|
66
|
+
const undeclared = read.filter(
|
|
67
|
+
(path) => !declared.some((field) => overlaps(path, field.path)) && !alreadyDescribed(declared, path)
|
|
68
|
+
);
|
|
69
|
+
return [
|
|
70
|
+
...unread.map((field) => unreadFinding(pairing, field)),
|
|
71
|
+
...undeclaredFindings(pairing, undeclared)
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
var LISTS_EVERYTHING = () => false;
|
|
75
|
+
var DESCRIBED_WITHOUT_LISTING = {
|
|
76
|
+
rest: (declared, path) => path[0] === "body" && declared.some((field) => field.path[0] === "body"),
|
|
77
|
+
"function-call": LISTS_EVERYTHING,
|
|
78
|
+
"message-bus": LISTS_EVERYTHING,
|
|
79
|
+
storage: LISTS_EVERYTHING,
|
|
80
|
+
"unit-invocation": LISTS_EVERYTHING,
|
|
81
|
+
"graphql-resolver": LISTS_EVERYTHING,
|
|
82
|
+
"graphql-operation": LISTS_EVERYTHING,
|
|
83
|
+
"runtime-config": LISTS_EVERYTHING,
|
|
84
|
+
metric: LISTS_EVERYTHING
|
|
85
|
+
};
|
|
86
|
+
function unreadFinding(pairing, field) {
|
|
87
|
+
const needsIt = field.required ? " and needs it" : "";
|
|
88
|
+
return {
|
|
89
|
+
kind: "unreadInputField",
|
|
90
|
+
severity: field.required ? "warning" : "info",
|
|
91
|
+
boundary: pairing.boundary,
|
|
92
|
+
intent: { name: pairing.intent.name },
|
|
93
|
+
code: pairing.code,
|
|
94
|
+
message: `Intent "${pairing.intent.name}" says ${pairing.boundary} receives ${formatPath(field.path)}${needsIt}; ${pairing.impl.identity.name} never reads it.`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function undeclaredFindings(pairing, undeclared) {
|
|
98
|
+
const said = /* @__PURE__ */ new Set();
|
|
99
|
+
const findings = [];
|
|
100
|
+
for (const path of undeclared) {
|
|
101
|
+
const spelled = formatPath(path);
|
|
102
|
+
if (said.has(spelled)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
said.add(spelled);
|
|
106
|
+
findings.push({
|
|
107
|
+
kind: "undeclaredInputRead",
|
|
108
|
+
severity: "info",
|
|
109
|
+
boundary: pairing.boundary,
|
|
110
|
+
intent: { name: pairing.intent.name },
|
|
111
|
+
code: pairing.code,
|
|
112
|
+
message: `${pairing.impl.identity.name} reads ${spelled} off what it was handed at ${pairing.boundary}; intent "${pairing.intent.name}" does not declare it under receives.`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return findings;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/index.ts
|
|
25
119
|
function checkIntentAgreement(intents, code) {
|
|
26
120
|
const findings = [];
|
|
27
121
|
const checked = [];
|
|
@@ -29,6 +123,7 @@ function checkIntentAgreement(intents, code) {
|
|
|
29
123
|
const codeByBoundary = indexCodeByBoundary(code);
|
|
30
124
|
const boundaryByName = indexBoundaryIntentsByName(intents);
|
|
31
125
|
const deploymentOfUnit = deploymentOf(code);
|
|
126
|
+
const wrappersOfUnit = wrappersAround(code);
|
|
32
127
|
for (const intent of intents) {
|
|
33
128
|
if (intent.kind === "prd") {
|
|
34
129
|
const result2 = checkPrdCoverage(intent, boundaryByName);
|
|
@@ -39,7 +134,8 @@ function checkIntentAgreement(intents, code) {
|
|
|
39
134
|
const result = checkBoundaryIntent(
|
|
40
135
|
intent,
|
|
41
136
|
codeByBoundary,
|
|
42
|
-
deploymentOfUnit
|
|
137
|
+
deploymentOfUnit,
|
|
138
|
+
wrappersOfUnit
|
|
43
139
|
);
|
|
44
140
|
findings.push(...withProvenance(result.findings, intent.source));
|
|
45
141
|
checked.push(...result.checked);
|
|
@@ -83,7 +179,7 @@ function checkOutcomesDescribed(intents) {
|
|
|
83
179
|
}
|
|
84
180
|
return findings;
|
|
85
181
|
}
|
|
86
|
-
function checkBoundaryIntent(intent, codeByBoundary, deploymentOfUnit) {
|
|
182
|
+
function checkBoundaryIntent(intent, codeByBoundary, deploymentOfUnit, wrappersOfUnit) {
|
|
87
183
|
const key = pairingKey(intent.boundary);
|
|
88
184
|
if (key === null) {
|
|
89
185
|
return {
|
|
@@ -137,7 +233,13 @@ function checkBoundaryIntent(intent, codeByBoundary, deploymentOfUnit) {
|
|
|
137
233
|
const findings = [];
|
|
138
234
|
for (const impl of impls) {
|
|
139
235
|
findings.push(
|
|
140
|
-
...compareIntentToImpl(
|
|
236
|
+
...compareIntentToImpl(
|
|
237
|
+
intent,
|
|
238
|
+
impl,
|
|
239
|
+
label,
|
|
240
|
+
deploymentOfUnit(impl),
|
|
241
|
+
wrappersOfUnit(impl)
|
|
242
|
+
)
|
|
141
243
|
);
|
|
142
244
|
}
|
|
143
245
|
return {
|
|
@@ -322,7 +424,7 @@ function indexCodeByBoundary(code) {
|
|
|
322
424
|
}
|
|
323
425
|
return byKey;
|
|
324
426
|
}
|
|
325
|
-
function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
427
|
+
function compareIntentToImpl(intent, impl, boundary, deployment, wrappers) {
|
|
326
428
|
const findings = [];
|
|
327
429
|
const ref = codeRef(impl);
|
|
328
430
|
const calls = boundaryCalls(impl);
|
|
@@ -473,6 +575,9 @@ function compareIntentToImpl(intent, impl, boundary, deployment) {
|
|
|
473
575
|
findings.push(finding);
|
|
474
576
|
undeclared.push({ finding, boundary: made.label, does: made.does });
|
|
475
577
|
}
|
|
578
|
+
findings.push(
|
|
579
|
+
...checkReceivesBlock({ intent, impl, wrappers, boundary, code: ref })
|
|
580
|
+
);
|
|
476
581
|
return foldRenamedBoundaries(
|
|
477
582
|
intent,
|
|
478
583
|
impl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/checker-intent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Compare the intent your team wrote down against what suss read out of the code, and report where the two have drifted apart.",
|
|
6
6
|
"keywords": [
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"zod": "^4.0.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@suss/behavioral-ir": "0.
|
|
53
|
-
"@suss/intent-ir": "0.
|
|
54
|
-
"@suss/ir-core": "0.
|
|
52
|
+
"@suss/behavioral-ir": "0.33.1",
|
|
53
|
+
"@suss/intent-ir": "0.33.1",
|
|
54
|
+
"@suss/ir-core": "0.33.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"tsup": "*",
|