@drkmttr/adhere 0.10.0 → 0.11.0
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 +177 -41
- package/package.json +7 -7
- package/presets/effect/basics/gen-for-sequencing.md +1 -1
- package/presets/effect/data/brand-meaningful-primitives.md +1 -1
- package/presets/effect/data/variants-are-tagged-unions.md +1 -1
- package/presets/effect/services/dependencies-through-layers.md +1 -1
- package/presets/react/effects/clean-up-what-they-start.md +23 -0
- package/presets/react/effects/event-logic-in-handlers.md +29 -0
- package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
- package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
- package/presets/react/effects/notify-parents-in-handlers.md +29 -0
- package/presets/react/server/no-private-data-to-client.md +23 -0
- package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
- package/presets/react/state/no-contradictory-state.md +17 -0
- package/presets/react/state/no-props-copied-into-state.md +23 -0
- package/presets/react/state/store-ids-not-copies.md +20 -0
- package/presets/security/constant-time-secret-compare.md +22 -0
- package/presets/security/no-secrets-in-output.md +19 -0
- package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
- package/presets/security/parameterized-queries.md +16 -0
- package/presets/typescript/async/concurrent-independent-work.md +23 -0
- package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
- package/presets/typescript/design/no-argument-mutation.md +20 -0
- package/presets/typescript/errors/no-swallowed-errors.md +29 -0
- package/presets/typescript/errors/release-on-every-path.md +29 -0
- package/presets/typescript/testing/assert-outcomes.md +22 -0
- package/presets/typescript/testing/independent-tests.md +41 -0
- package/presets/typescript/types/assertions-only-when-proven.md +20 -0
- package/presets/typescript/types/derive-dont-restate.md +24 -0
- package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
- package/presets/typescript/types/validate-external-data.md +21 -0
- package/skills/adhere/SKILL.md +4 -3
- package/skills/adhere-fix/SKILL.md +10 -4
- package/src/cli.ts +9 -1
- package/src/config.ts +29 -1
- package/src/excerpt.ts +72 -62
- package/src/highlight.ts +2 -0
- package/src/markdown.ts +6 -0
- package/src/presets.ts +6 -0
- package/src/rules.ts +1 -1
- package/src/services/AuditCache.ts +31 -5
- package/src/services/Jev.http.ts +76 -11
- package/src/services/Jev.ts +191 -73
- package/src/services/SourceWalker.ts +23 -7
- package/src/suppress.ts +13 -7
- package/src/version.ts +25 -1
- package/src/workflows/audit.ts +96 -26
- package/src/workflows/format.ts +51 -11
- package/presets/alchemy/data/parameterized-sql.md +0 -17
- package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -19
- package/presets/effect/basics/external-calls-are-resilient.md +0 -21
package/src/workflows/audit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isNote, withoutComments } from "#comments.ts";
|
|
2
2
|
import { type Examples, examplesOf, type Level, type Rule, type RuleId } from "#config.ts";
|
|
3
|
-
import { type Excerpt,
|
|
3
|
+
import { type Excerpt, type Range } from "#excerpt.ts";
|
|
4
4
|
import type { ScannedFile, WalkUnavailable } from "#models/Audit.ts";
|
|
5
5
|
import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
6
6
|
import { AuditCache, answersOf, type Judgment, sha256, type Tally } from "#services/AuditCache.ts";
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
judgeLoad,
|
|
14
14
|
judgeRequests,
|
|
15
15
|
linterQuestion,
|
|
16
|
+
type Located,
|
|
16
17
|
locateRequests,
|
|
18
|
+
matcherQuestions,
|
|
17
19
|
questionRoom,
|
|
18
20
|
tokensOf,
|
|
19
21
|
} from "#services/Jev.ts";
|
|
@@ -32,9 +34,18 @@ export interface Finding {
|
|
|
32
34
|
readonly description: string;
|
|
33
35
|
readonly examples: Examples;
|
|
34
36
|
readonly file: string;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/** The section of the file Jev points at, which a finding is in. */
|
|
38
|
+
readonly section: Range;
|
|
39
|
+
/** The lines in the section the code that breaks the rule starts and ends on, when Jev named them. */
|
|
40
|
+
readonly lines?: Range;
|
|
41
|
+
/** The section's code as written, comments and all, without blank lines at either end. */
|
|
37
42
|
readonly excerpt: Excerpt;
|
|
43
|
+
/**
|
|
44
|
+
* Jev's probability that the file does not show enough to check the rule,
|
|
45
|
+
* when its probability that it does is below the sufficiency threshold:
|
|
46
|
+
* the report warns, and says how to add the context.
|
|
47
|
+
*/
|
|
48
|
+
readonly insufficiency?: number;
|
|
38
49
|
readonly probability: number;
|
|
39
50
|
/** An error fails the run; a warning does not, unless `--deny-warnings`. */
|
|
40
51
|
readonly level: Level;
|
|
@@ -90,13 +101,51 @@ const fileResult = (
|
|
|
90
101
|
|
|
91
102
|
const isEmpty = Record.isEmptyReadonlyRecord;
|
|
92
103
|
|
|
104
|
+
/** How Jev reads a file, which a judgment depends on: a new layout re-judges every rule. */
|
|
105
|
+
const LAYOUT = "sections";
|
|
106
|
+
|
|
93
107
|
/**
|
|
94
|
-
* What a judgment depends on besides the file: the model,
|
|
95
|
-
* asked for the rule, which
|
|
96
|
-
*
|
|
108
|
+
* What a judgment depends on besides the file: the model, how the file is
|
|
109
|
+
* laid out, and the questions asked for the rule, which carry the rule: its
|
|
110
|
+
* judge question, and its matchers' when it has any. An edited rule or
|
|
111
|
+
* matcher re-judges the rule, and a question asked differently re-judges
|
|
112
|
+
* every rule.
|
|
97
113
|
*/
|
|
98
|
-
const fingerprintOf = (model: string, rule: Rule) =>
|
|
99
|
-
|
|
114
|
+
const fingerprintOf = (model: string, rule: Rule) => {
|
|
115
|
+
const matchers = matcherQuestions("", rule).map(([, question]) => question);
|
|
116
|
+
const scope = matchers.length === 0 ? "" : `\u0000${JSON.stringify(matchers)}`;
|
|
117
|
+
return sha256(`${model}\u0000${LAYOUT}\u0000${JSON.stringify(judgeQuestion(rule))}${scope}`);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** A section's lines as written, without the blank lines at either end. */
|
|
121
|
+
const excerptOf = (lines: ReadonlyArray<string>, { first, last }: Range): Excerpt => {
|
|
122
|
+
let start = first;
|
|
123
|
+
let end = last;
|
|
124
|
+
while (start < end && (lines[start - 1] ?? "").trim() === "") start += 1;
|
|
125
|
+
while (end > start && (lines[end - 1] ?? "").trim() === "") end -= 1;
|
|
126
|
+
return { start, lines: lines.slice(start - 1, end) };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** A matcher's score counts as a yes above this. */
|
|
130
|
+
const MATCHER_THRESHOLD = 0.5;
|
|
131
|
+
|
|
132
|
+
/** Whether a judgment's matchers let it stand: a yes to every `appliesTo`, and to no `excludeIf`. */
|
|
133
|
+
const inScope = (judgment: Judgment): boolean =>
|
|
134
|
+
(judgment.appliesTo ?? []).every((score) => score > MATCHER_THRESHOLD) &&
|
|
135
|
+
!(judgment.excludeIf ?? []).some((score) => score > MATCHER_THRESHOLD);
|
|
136
|
+
|
|
137
|
+
/** The matchers that dropped a judgment, with their scores, as the debug log names them. */
|
|
138
|
+
const scopeMisses = (rule: Rule, judgment: Judgment): string =>
|
|
139
|
+
[
|
|
140
|
+
...(rule.appliesTo ?? []).flatMap((matcher, index) => {
|
|
141
|
+
const score = judgment.appliesTo?.[index] ?? 0;
|
|
142
|
+
return score > MATCHER_THRESHOLD ? [] : [`appliesTo "${matcher}" (${score.toFixed(2)})`];
|
|
143
|
+
}),
|
|
144
|
+
...(rule.excludeIf ?? []).flatMap((matcher, index) => {
|
|
145
|
+
const score = judgment.excludeIf?.[index] ?? 0;
|
|
146
|
+
return score > MATCHER_THRESHOLD ? [`excludeIf "${matcher}" (${score.toFixed(2)})`] : [];
|
|
147
|
+
}),
|
|
148
|
+
].join(", ");
|
|
100
149
|
|
|
101
150
|
/** One file before anything is sent: its rules, and which of them the cache answers. */
|
|
102
151
|
export interface FilePlan {
|
|
@@ -143,6 +192,8 @@ export interface AuditPlan {
|
|
|
143
192
|
readonly tokens: number;
|
|
144
193
|
/** The model's price per million input tokens, in dollars, when adhere knows it. */
|
|
145
194
|
readonly price?: number;
|
|
195
|
+
/** Below this, a finding's sufficiency adds a warning to it. */
|
|
196
|
+
readonly sufficiencyThreshold: number;
|
|
146
197
|
}
|
|
147
198
|
|
|
148
199
|
/** A file finished, for a progress counter: the requests it took and the findings it has. */
|
|
@@ -408,6 +459,7 @@ export const planAudit = (
|
|
|
408
459
|
requests: loads.reduce((sum, load) => sum + load.requests, 0),
|
|
409
460
|
tokens: loads.reduce((sum, load) => sum + load.tokens, 0),
|
|
410
461
|
...(price === undefined ? {} : { price }),
|
|
462
|
+
sufficiencyThreshold: config.sufficiencyThreshold,
|
|
411
463
|
};
|
|
412
464
|
});
|
|
413
465
|
|
|
@@ -457,14 +509,25 @@ export const executeAudit = (
|
|
|
457
509
|
deferred,
|
|
458
510
|
sampled,
|
|
459
511
|
}: FilePlan) {
|
|
460
|
-
const { probabilities, linter }: Judged = isEmpty(pending)
|
|
512
|
+
const { probabilities, linter, matchers }: Judged = isEmpty(pending)
|
|
461
513
|
? { probabilities: {}, linter: {} }
|
|
462
514
|
: yield* jev.judge(file.lines, rulesOf(pending), Object.keys(sampled)).pipe(inFile(file));
|
|
463
515
|
if (!isEmpty(linter)) yield* record(file.path, sampled, linter);
|
|
464
516
|
const judged: Record<RuleId, Judgment> = { ...kept };
|
|
465
517
|
for (const [id, probability] of Object.entries(probabilities)) {
|
|
466
518
|
const k = pending[id];
|
|
467
|
-
if (k !== undefined)
|
|
519
|
+
if (k !== undefined) {
|
|
520
|
+
judged[id] = { fingerprint: k.fingerprint, probability, ...matchers?.[id] };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Above its threshold, but its matchers put the code out of the rule's scope.
|
|
524
|
+
for (const [id, judgment] of Object.entries(judged)) {
|
|
525
|
+
const k = prepared[id];
|
|
526
|
+
if (k !== undefined && judgment.probability > k.threshold && !inScope(judgment)) {
|
|
527
|
+
yield* Effect.logDebug(
|
|
528
|
+
`${file.path}: ${id} (${judgment.probability.toFixed(2)}) dropped by ${scopeMisses(k.rule, judgment)}`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
468
531
|
}
|
|
469
532
|
|
|
470
533
|
const flagged = Record.filter(prepared, (k, id) => {
|
|
@@ -472,29 +535,30 @@ export const executeAudit = (
|
|
|
472
535
|
return (
|
|
473
536
|
judgment !== undefined &&
|
|
474
537
|
judgment.probability > k.threshold &&
|
|
475
|
-
judgment
|
|
538
|
+
inScope(judgment) &&
|
|
539
|
+
(judgment.section === undefined || judgment.sufficiency === undefined)
|
|
476
540
|
);
|
|
477
541
|
});
|
|
478
|
-
// A blocked
|
|
479
|
-
// asks only for the
|
|
542
|
+
// A blocked locate question still leaves the judgments worth caching: a rerun
|
|
543
|
+
// asks only for the sections again, not for every rule.
|
|
480
544
|
const {
|
|
481
|
-
|
|
545
|
+
places,
|
|
482
546
|
blocked,
|
|
483
547
|
}: {
|
|
484
|
-
readonly
|
|
548
|
+
readonly places: Readonly<Record<RuleId, Located>>;
|
|
485
549
|
readonly blocked: Blocked | undefined;
|
|
486
550
|
} = isEmpty(flagged)
|
|
487
|
-
? {
|
|
551
|
+
? { places: {}, blocked: undefined }
|
|
488
552
|
: yield* jev.locate(file.lines, rulesOf(flagged)).pipe(
|
|
489
|
-
Effect.map((
|
|
553
|
+
Effect.map((places) => ({ places, blocked: undefined })),
|
|
490
554
|
Effect.catchTag("JevBlocked", ({ ray }) =>
|
|
491
|
-
Effect.succeed({
|
|
555
|
+
Effect.succeed({ places: {}, blocked: { file: file.path, ray } }),
|
|
492
556
|
),
|
|
493
557
|
inFile(file),
|
|
494
558
|
);
|
|
495
559
|
const located = Record.map(judged, (judgment, id) => {
|
|
496
|
-
const
|
|
497
|
-
return
|
|
560
|
+
const place = places[id];
|
|
561
|
+
return place === undefined ? judgment : { ...judgment, ...place };
|
|
498
562
|
});
|
|
499
563
|
|
|
500
564
|
const cached = isEmpty(pending) && isEmpty(flagged);
|
|
@@ -504,8 +568,9 @@ export const executeAudit = (
|
|
|
504
568
|
.flatMap(([id, judgment]): ReadonlyArray<Finding> => {
|
|
505
569
|
const k = prepared[id];
|
|
506
570
|
return k !== undefined &&
|
|
507
|
-
judgment.
|
|
508
|
-
judgment.probability > k.threshold
|
|
571
|
+
judgment.section !== undefined &&
|
|
572
|
+
judgment.probability > k.threshold &&
|
|
573
|
+
inScope(judgment)
|
|
509
574
|
? [
|
|
510
575
|
{
|
|
511
576
|
rule: id,
|
|
@@ -513,15 +578,20 @@ export const executeAudit = (
|
|
|
513
578
|
description: k.rule.description,
|
|
514
579
|
examples: examplesOf(k.rule),
|
|
515
580
|
file: file.path,
|
|
516
|
-
|
|
517
|
-
|
|
581
|
+
section: judgment.section,
|
|
582
|
+
...(judgment.lines === undefined ? {} : { lines: judgment.lines }),
|
|
583
|
+
excerpt: excerptOf(original, judgment.section),
|
|
584
|
+
...(judgment.sufficiency !== undefined &&
|
|
585
|
+
judgment.sufficiency < plan.sufficiencyThreshold
|
|
586
|
+
? { insufficiency: 1 - judgment.sufficiency }
|
|
587
|
+
: {}),
|
|
518
588
|
probability: judgment.probability,
|
|
519
589
|
level: k.rule.level ?? "error",
|
|
520
590
|
},
|
|
521
591
|
]
|
|
522
592
|
: [];
|
|
523
593
|
})
|
|
524
|
-
.sort((a, b) => a.
|
|
594
|
+
.sort((a, b) => (a.lines ?? a.section).first - (b.lines ?? b.section).first);
|
|
525
595
|
return {
|
|
526
596
|
result:
|
|
527
597
|
blocked === undefined
|
|
@@ -583,7 +653,7 @@ export const executeAudit = (
|
|
|
583
653
|
!suppresses(
|
|
584
654
|
plan.suppressions,
|
|
585
655
|
shownId({ id: finding.rule, preset: finding.preset }),
|
|
586
|
-
finding.
|
|
656
|
+
finding.lines ?? finding.section,
|
|
587
657
|
),
|
|
588
658
|
);
|
|
589
659
|
if (shown.length < result.findings.length) {
|
package/src/workflows/format.ts
CHANGED
|
@@ -102,36 +102,74 @@ const header = (finding: Finding): Line => [
|
|
|
102
102
|
];
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* The path,
|
|
106
|
-
*
|
|
105
|
+
* The path at the lines Jev points at, or the section's first line when it
|
|
106
|
+
* named none, then the section with each line numbered in the gutter. One
|
|
107
|
+
* line Jev points at is underlined under its code; several are marked by a
|
|
108
|
+
* bar in a column of their own beside the code, as `vp lint` marks a span,
|
|
109
|
+
* so each line of code keeps one line of the report.
|
|
107
110
|
*/
|
|
108
111
|
const excerpt = (finding: Finding, root: string | undefined): ReadonlyArray<Line> => {
|
|
109
112
|
const { start, lines } = finding.excerpt;
|
|
110
113
|
const width = String(start + lines.length - 1).length;
|
|
111
114
|
const gutter = span(" ".repeat(width + 2));
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
115
|
+
const indentOf = (line: string) => line.slice(0, line.length - line.trimStart().length);
|
|
116
|
+
const pointed = finding.lines;
|
|
117
|
+
const at = pointed?.first ?? start;
|
|
118
|
+
const location = `:${at}:${indentOf(lines[at - start] ?? "").length + 1}]`;
|
|
119
|
+
const spanning = pointed !== undefined && pointed.last > pointed.first;
|
|
120
|
+
const inside = (line: number) =>
|
|
121
|
+
pointed !== undefined && pointed.first <= line && line <= pointed.last;
|
|
117
122
|
return [
|
|
118
123
|
[gutter, span("╭─["), span(displayPath(finding.file, root), "path"), span(location)],
|
|
119
124
|
...highlighted(lines.join("\n")).flatMap((code, index): ReadonlyArray<Line> => {
|
|
120
125
|
const number = String(start + index);
|
|
126
|
+
const marker = spanning ? [inside(start + index) ? span("┃ ", "underline") : span(" ")] : [];
|
|
121
127
|
const numbered = [
|
|
122
128
|
span(" ".repeat(width - number.length + 1)),
|
|
123
129
|
span(number, "lineNumber"),
|
|
124
130
|
span(" │ "),
|
|
131
|
+
...marker,
|
|
125
132
|
...code,
|
|
126
133
|
];
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
const text = lines[index] ?? "";
|
|
135
|
+
// The line's own indentation, tabs and all, lines the underline up under its code.
|
|
136
|
+
return !spanning && inside(start + index) && text.trim() !== ""
|
|
137
|
+
? [
|
|
138
|
+
numbered,
|
|
139
|
+
[
|
|
140
|
+
gutter,
|
|
141
|
+
span("· "),
|
|
142
|
+
span(indentOf(text)),
|
|
143
|
+
span("─".repeat(text.trim().length), "underline"),
|
|
144
|
+
],
|
|
145
|
+
]
|
|
129
146
|
: [numbered];
|
|
130
147
|
}),
|
|
131
148
|
[gutter, span("╰────")],
|
|
132
149
|
];
|
|
133
150
|
};
|
|
134
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The warning on a finding whose file may not show enough to check its rule,
|
|
154
|
+
* with Jev's probability that it does not, and help: how to add what the
|
|
155
|
+
* code relies on as a note Jev reads, in the form the README asks for.
|
|
156
|
+
*/
|
|
157
|
+
const insufficient = (finding: Finding): ReadonlyArray<Line> =>
|
|
158
|
+
finding.insufficiency === undefined
|
|
159
|
+
? []
|
|
160
|
+
: [
|
|
161
|
+
[
|
|
162
|
+
span(" warning: ", "warning"),
|
|
163
|
+
span(
|
|
164
|
+
`this file may not show enough to check this rule (Jev: ${finding.insufficiency.toFixed(2)} that it does not)`,
|
|
165
|
+
),
|
|
166
|
+
],
|
|
167
|
+
...hanging(span(" help: ", "label"), [
|
|
168
|
+
[span("add what the code relies on outside this file as a note Jev reads:")],
|
|
169
|
+
highlighted("// @adhere <the fact>, and how you know it")[0] ?? [],
|
|
170
|
+
]),
|
|
171
|
+
];
|
|
172
|
+
|
|
135
173
|
/**
|
|
136
174
|
* The code to write as the hint. A rule with only code not to write shows
|
|
137
175
|
* that instead, labeled with its word: `never`, or `should not`.
|
|
@@ -145,12 +183,14 @@ const hint = (finding: Finding): ReadonlyArray<Line> => {
|
|
|
145
183
|
|
|
146
184
|
/**
|
|
147
185
|
* One diagnostic, in the frame `vp lint` prints on a terminal: a header with
|
|
148
|
-
* the rule in red, the
|
|
149
|
-
*
|
|
186
|
+
* the rule in red, the section of the file Jev points at with the lines it
|
|
187
|
+
* names in pink, a warning when the file may not show enough to decide, and
|
|
188
|
+
* the code to write as the hint.
|
|
150
189
|
*/
|
|
151
190
|
const frame = (finding: Finding, root: string | undefined): ReadonlyArray<Line> => [
|
|
152
191
|
header(finding),
|
|
153
192
|
...excerpt(finding, root),
|
|
193
|
+
...insufficient(finding),
|
|
154
194
|
...hint(finding),
|
|
155
195
|
];
|
|
156
196
|
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: SQL must pass values as parameters, through tagged-template interpolation, sql.in, sql.insert, or prepare().bind(), never by string concatenation or a template string handed to an unsafe or raw API.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Must
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
const user = yield* sql`SELECT * FROM users WHERE email = ${email}`;
|
|
9
|
-
const row = yield* Effect.promise(() => d1.prepare("SELECT * FROM users WHERE id = ?").bind(id).first());
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Never
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
const user = yield* sql.unsafe(`SELECT * FROM users WHERE email = '${email}'`);
|
|
16
|
-
const row = yield* Effect.promise(() => d1.prepare("SELECT * FROM users WHERE id = " + id).first());
|
|
17
|
-
```
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: A secret, credential, connection string, token, signed URL, or private key must never be logged, returned in an HTTP response, or returned as a stack output; code must expose only names, presence, or non-secret metadata.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Must
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
if (url.pathname === "/secret") {
|
|
9
|
-
return yield* HttpServerResponse.json({ name: "API_TOKEN", set: Redacted.value(token).length > 0 });
|
|
10
|
-
}
|
|
11
|
-
return { serial: certificate.serial, expires: certificate.notAfter }; // stack output
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Never
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
yield* Effect.log(`token=${Redacted.value(token)}`);
|
|
18
|
-
return { databaseUrl: connection.databaseUrl, tlsKey: certificate.privateKey }; // stack output
|
|
19
|
-
```
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: A call over the network, such as an HTTP request, a database query, or a third-party API, should carry a timeout and a retry schedule, and should not go out bare. A call through a client that already applies both, such as an HttpClient built with a timeout and retryTransient or an SDK with its own retries, has them. Calls through the platform FileSystem and Path services are local and are not in scope.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Should
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
const retryPolicy = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3)));
|
|
9
|
-
|
|
10
|
-
const resilientCall = HttpClient.get("https://api.example.com/users").pipe(
|
|
11
|
-
Effect.timeout("2 seconds"),
|
|
12
|
-
Effect.retry(retryPolicy),
|
|
13
|
-
Effect.timeout("10 seconds"),
|
|
14
|
-
);
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Should not
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
const response = yield* HttpClient.get(`https://api.example.com/users/${id}`);
|
|
21
|
-
```
|