@intentic/sandbox-contract 1.223.0 → 1.225.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 +14 -14
- package/dist/agent-catalog.js +3 -3
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.js +43 -43
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/extension-update.js +2 -2
- package/dist/chores/extension-update.js.map +1 -1
- package/dist/chores/fix-deps.js +1 -1
- package/dist/chores/fix-deps.js.map +1 -1
- package/dist/chores/probes.js +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/chores/verdict.js +2 -2
- package/dist/chores/verdict.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +41 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.d.ts +80 -0
- package/dist/contracts/exit.contract.d.ts.map +1 -0
- package/dist/contracts/exit.contract.js +13 -0
- package/dist/contracts/exit.contract.js.map +1 -0
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +12 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/events.d.ts +4 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -5
- package/dist/events.js.map +1 -1
- package/dist/fast-tier.d.ts +9 -0
- package/dist/fast-tier.d.ts.map +1 -0
- package/dist/fast-tier.js +19 -0
- package/dist/fast-tier.js.map +1 -0
- package/dist/history-state.js +3 -3
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +214 -68
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts +1 -0
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +5 -0
- package/dist/model-order.js.map +1 -1
- package/dist/output-fields.d.ts.map +1 -1
- package/dist/output-fields.js +2 -2
- package/dist/output-fields.js.map +1 -1
- package/dist/prompt-complexity.d.ts +19 -0
- package/dist/prompt-complexity.d.ts.map +1 -0
- package/dist/prompt-complexity.js +83 -0
- package/dist/prompt-complexity.js.map +1 -0
- package/dist/publish-drafts.js +2 -2
- package/dist/publish-drafts.js.map +1 -1
- package/dist/schemas.d.ts +236 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +116 -2
- package/dist/schemas.js.map +1 -1
- package/dist/workflow-faults.js +3 -3
- package/dist/workflow-faults.js.map +1 -1
- package/dist/workspace-state.d.ts +20 -20
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +20 -20
- package/dist/workspace-state.js.map +1 -1
- package/package.json +5 -5
- package/src/agent-catalog.test.ts +25 -25
- package/src/agent-catalog.ts +81 -81
- package/src/agent-run-model.test.ts +3 -3
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-ledger.test.ts +13 -13
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/chores.ts +109 -109
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +5 -5
- package/src/chores/fix-deps.ts +4 -4
- package/src/chores/probes.test.ts +6 -6
- package/src/chores/probes.ts +17 -17
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.test.ts +3 -3
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.test.ts +20 -20
- package/src/chores/verdict.ts +25 -25
- package/src/contract-lock.test.ts +1 -1
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/exit.contract.ts +42 -0
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +8 -8
- package/src/events.test.ts +3 -3
- package/src/events.ts +142 -142
- package/src/fast-tier.test.ts +88 -0
- package/src/fast-tier.ts +72 -0
- package/src/history-state.ts +15 -15
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.test.ts +1 -1
- package/src/hostnames.ts +15 -15
- package/src/index.ts +13 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.test.ts +11 -11
- package/src/model-order.ts +52 -30
- package/src/output-fields.ts +11 -11
- package/src/path-refs.test.ts +4 -4
- package/src/path-refs.ts +6 -6
- package/src/prompt-complexity.test.ts +160 -0
- package/src/prompt-complexity.ts +271 -0
- package/src/publish-drafts.ts +8 -8
- package/src/quick-model.test.ts +11 -11
- package/src/quick-model.ts +15 -15
- package/src/routes.test.ts +11 -5
- package/src/routes.ts +19 -19
- package/src/runtime-state.test.ts +1 -1
- package/src/runtime-state.ts +22 -22
- package/src/schemas.test.ts +8 -8
- package/src/schemas.ts +1238 -921
- package/src/search-globs.test.ts +2 -2
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.test.ts +1 -1
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.test.ts +9 -9
- package/src/title.ts +21 -21
- package/src/tunnel-ids.test.ts +3 -3
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.test.ts +3 -3
- package/src/versions.ts +6 -6
- package/src/workflow-faults.test.ts +6 -6
- package/src/workflow-faults.ts +14 -14
- package/src/workspace-state.test.ts +32 -32
- package/src/workspace-state.ts +139 -139
package/src/chores/chores.ts
CHANGED
|
@@ -3,30 +3,30 @@ import { bucketOf, digestOf } from "./digest.js";
|
|
|
3
3
|
import { CHORE_INVARIANTS, composeAsk, REPORT_INVARIANTS, TRIAGE_NOTE } from "./prompt.js";
|
|
4
4
|
import { componentStem, frameworksOf, idiomRule, normalizePath, UI_FRAMEWORKS, usesTailwind } from "./stack.js";
|
|
5
5
|
|
|
6
|
-
/* THE CHORE BOOK
|
|
6
|
+
/* THE CHORE BOOK, what routine maintenance a repository is owed, and what has to be TRUE before we say so.
|
|
7
7
|
*
|
|
8
8
|
* Everything in here is a standing offer: work that is worth doing eventually, that nobody will ever put on a
|
|
9
9
|
* sprint board, and that a person cannot notice is overdue by looking at their editor. The engineering problem is
|
|
10
|
-
* not finding such work
|
|
10
|
+
* not finding such work, any linter will hand you a thousand findings, it is deciding which of them is worth
|
|
11
11
|
* interrupting somebody about, on a surface they will still be reading in six months.
|
|
12
12
|
*
|
|
13
13
|
* Three rules, and every entry below obeys all three:
|
|
14
14
|
*
|
|
15
15
|
* 1. DELTAS, NOT ABSOLUTES. "38 packages are undocumented" is a statistic; it will be true every day for a year,
|
|
16
16
|
* and a tile lit every day teaches the eye to stop seeing the rail. "A package appeared that nothing explains"
|
|
17
|
-
* is an event. So a chore's `digest` is built from the IDENTITIES of what it found
|
|
18
|
-
* advisories, which files
|
|
17
|
+
* is an event. So a chore's `digest` is built from the IDENTITIES of what it found, which packages, which
|
|
18
|
+
* advisories, which files, and the rail speaks when that set changes, not while it is merely non-empty. The
|
|
19
19
|
* standing count still shows inside the panel, next to the thing it describes, which is where a statistic
|
|
20
20
|
* belongs.
|
|
21
21
|
*
|
|
22
22
|
* 2. LEADER-RELATIVE, NOT TUNED. Nowhere in here is there a threshold that would need a different value for a
|
|
23
|
-
* Rust repo, a fresh scaffold, or a ten-year monolith
|
|
23
|
+
* Rust repo, a fresh scaffold, or a ten-year monolith, with one deliberate exception (duplication's 5%, which
|
|
24
24
|
* is a percentage of the tree and therefore already scale-free). "Three times the median of its own ranking"
|
|
25
25
|
* needs no calibration and cannot rot.
|
|
26
26
|
*
|
|
27
27
|
* 3. THE EVIDENCE IS THE TRUTH; THE LEDGER ONLY DEBOUNCES. Nothing here can be ticked off. A chore goes quiet
|
|
28
|
-
* because the measurement moved, which means someone fixing it by hand
|
|
29
|
-
* accident
|
|
28
|
+
* because the measurement moved, which means someone fixing it by hand, or an unrelated change fixing it by
|
|
29
|
+
* accident, is registered exactly like a chore turn doing it. The ledger's only power is to stop the rail
|
|
30
30
|
* repeating itself about evidence a turn has already been spent on (verdict.ts).
|
|
31
31
|
*
|
|
32
32
|
* What is NOT here is as deliberate. There is no composite score, no letter grade, no "health: 78%". Those are
|
|
@@ -38,7 +38,7 @@ import { componentStem, frameworksOf, idiomRule, normalizePath, UI_FRAMEWORKS, u
|
|
|
38
38
|
export type ChoreStance = "act" | "report";
|
|
39
39
|
|
|
40
40
|
/* WHAT KIND OF CLAIM A CHORE MAKES ON SOMEONE'S ATTENTION. Four of them, ordered from "this is a risk you are
|
|
41
|
-
* carrying right now" to "this is worth thinking about this quarter"
|
|
41
|
+
* carrying right now" to "this is worth thinking about this quarter", see CHORE_KINDS at the foot of this file,
|
|
42
42
|
* which carries the argument and the words the panel groups under. */
|
|
43
43
|
export type ChoreKind = "carrying" | "accruing" | "drifting" | "surveying";
|
|
44
44
|
|
|
@@ -56,15 +56,15 @@ export interface ChoreContext {
|
|
|
56
56
|
export interface ChoreFinding {
|
|
57
57
|
// One line, in numbers, for the row. The reader decides from this whether to open anything.
|
|
58
58
|
readonly headline: string;
|
|
59
|
-
// The evidence itself, one claim per line
|
|
59
|
+
// The evidence itself, one claim per line, what the panel lists under the row, and what makes the headline
|
|
60
60
|
// checkable rather than something to be believed.
|
|
61
61
|
readonly detail: readonly string[];
|
|
62
62
|
// The identity of THIS evidence. See digest.ts: it is what the rail's transitions are measured against.
|
|
63
63
|
readonly digest: string;
|
|
64
|
-
// `warning` is for a risk the owner is carrying right now
|
|
64
|
+
// `warning` is for a risk the owner is carrying right now, a live advisory, a runtime past its EOL. Everything
|
|
65
65
|
// else is `info`, including large and ugly numbers, because "there is a lot of it" is not an emergency.
|
|
66
66
|
readonly severity: "info" | "warning";
|
|
67
|
-
// The numbers again, in the agent's terms, for the prompt's "Why:" line. Exact
|
|
67
|
+
// The numbers again, in the agent's terms, for the prompt's "Why:" line. Exact, the agent may recount them.
|
|
68
68
|
readonly why: string;
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -81,7 +81,7 @@ export interface Chore {
|
|
|
81
81
|
* reason: the reading order is the one editorial claim this surface makes, and a claim spelled as a comment
|
|
82
82
|
* beside a hand-maintained list is one nobody can check and the compiler cannot keep. */
|
|
83
83
|
readonly kind: ChoreKind;
|
|
84
|
-
/* THE RULE, in words
|
|
84
|
+
/* THE RULE, in words, what has to be true for this chore to be due, stated so a reader can check it against
|
|
85
85
|
* the evidence below it and disagree.
|
|
86
86
|
*
|
|
87
87
|
* This is not decoration. A row that says "4 majors waiting" and nothing else is asking to be taken on
|
|
@@ -89,23 +89,23 @@ export interface Chore {
|
|
|
89
89
|
* argue with, and arguing with it is how the book gets better. It rides into the prompt too, so the agent is
|
|
90
90
|
* told the rule it was woken by rather than left to infer it from the numbers.
|
|
91
91
|
*
|
|
92
|
-
* Kept as prose next to the code that implements it, which means it can drift from it
|
|
92
|
+
* Kept as prose next to the code that implements it, which means it can drift from it, the tests below
|
|
93
93
|
* cannot check English. The rule for writing one: say the THRESHOLD, not the subject. "Duplication is high"
|
|
94
94
|
* is a topic; "more than 5% of the tree is duplicated" is a criterion. */
|
|
95
95
|
readonly criterion: string;
|
|
96
|
-
/* WHETHER THIS IS A QUESTION WORTH ASKING OF THIS REPOSITORY AT ALL
|
|
96
|
+
/* WHETHER THIS IS A QUESTION WORTH ASKING OF THIS REPOSITORY AT ALL, returns undefined when it is, and what
|
|
97
97
|
* is MISSING when it is not.
|
|
98
98
|
*
|
|
99
99
|
* Distinct from `assess`, and the distinction is the whole point: `assess` asks whether the answer is yes,
|
|
100
100
|
* this asks whether the question makes sense. "Re-read the documentation against the code" in a repository
|
|
101
|
-
* with no documentation is not a chore that is currently clear
|
|
101
|
+
* with no documentation is not a chore that is currently clear, it is one that will never apply here, and
|
|
102
102
|
* showing it as clear says we checked something we cannot check. A chore that does not apply is dropped from
|
|
103
103
|
* the panel entirely; a line in the scope strip records that it was considered.
|
|
104
104
|
*
|
|
105
|
-
* A BARE CAUSE
|
|
105
|
+
* A BARE CAUSE, "no Dockerfile", never "this repository ships no Dockerfile, so there is no image to slim".
|
|
106
106
|
* Same spelling as `ProbeSpec.unavailable`, and for the same reason both surfaces need: one absent
|
|
107
107
|
* package.json rules out five chores, and five sentences saying so at length is the wall of text this phrasing
|
|
108
|
-
* exists to prevent. The panel groups by this string, so the CONSEQUENCE
|
|
108
|
+
* exists to prevent. The panel groups by this string, so the CONSEQUENCE, which chores it costs, is the list
|
|
109
109
|
* beside it rather than a clause repeated inside every entry. Identical causes must be spelled identically or
|
|
110
110
|
* they group apart.
|
|
111
111
|
*
|
|
@@ -113,7 +113,7 @@ export interface Chore {
|
|
|
113
113
|
* fact the daemon holds without measuring anything. If a gate needed a probe it would be describing the
|
|
114
114
|
* answer rather than the question. */
|
|
115
115
|
readonly applies?: (signals: ChoreSignals) => string | undefined;
|
|
116
|
-
// Whether the turn is allowed to CHANGE anything. Not a hint
|
|
116
|
+
// Whether the turn is allowed to CHANGE anything. Not a hint, it selects the invariants block, and a
|
|
117
117
|
// report-stance chore is told in words that editing would be a surprise.
|
|
118
118
|
readonly stance: ChoreStance;
|
|
119
119
|
// Probes that must have run and succeeded before this chore can be assessed at all. Missing ⇒ `unavailable`:
|
|
@@ -126,13 +126,13 @@ export interface Chore {
|
|
|
126
126
|
// A survey has no measurement: it is due on its cadence and clear otherwise. Named rather than inferred from
|
|
127
127
|
// an empty `needs`, because the two are different claims and the panel says which one a row is.
|
|
128
128
|
readonly survey?: true;
|
|
129
|
-
/* THE SCHEDULED FORM, for the chores worth running unattended
|
|
129
|
+
/* THE SCHEDULED FORM, for the chores worth running unattended, what the Automations page offers as a
|
|
130
130
|
* one-click "code chore", and the second way this book is consumed.
|
|
131
131
|
*
|
|
132
132
|
* The two modes are genuinely different and both are wanted. The Maintenance panel is EVIDENCE-driven: it
|
|
133
133
|
* reads what the daemon already measured and offers a turn against a specific finding you can read first. An
|
|
134
134
|
* automation is SCHEDULE-driven: it wakes on a clock, at 3am, with nobody watching. So an automation cannot
|
|
135
|
-
* carry a finding
|
|
135
|
+
* carry a finding, there is no verdict at fire time, and instead it carries a GUARD: a shell one-liner that
|
|
136
136
|
* runs for free on the sandbox's own clock and exits non-zero to skip, so the half that costs a turn only
|
|
137
137
|
* starts when there is something to start it for.
|
|
138
138
|
*
|
|
@@ -144,12 +144,12 @@ export interface Chore {
|
|
|
144
144
|
readonly guard: string;
|
|
145
145
|
readonly note: string;
|
|
146
146
|
readonly report: string;
|
|
147
|
-
// How the woken turn is told what it is looking at
|
|
147
|
+
// How the woken turn is told what it is looking at, the "Why:" line, in place of a finding.
|
|
148
148
|
readonly woke: string;
|
|
149
149
|
};
|
|
150
150
|
readonly assess: (context: ChoreContext) => ChoreFinding | undefined;
|
|
151
151
|
// The prompt's three variable parts (prompt.ts owns the shape). `diagnosis` says what the numbers MEAN, `goal`
|
|
152
|
-
// says what shape to move towards
|
|
152
|
+
// says what shape to move towards, never a design, and `done` is falsifiable by the agent itself.
|
|
153
153
|
readonly diagnosis: string;
|
|
154
154
|
readonly goal: string;
|
|
155
155
|
readonly done: string;
|
|
@@ -158,7 +158,7 @@ export interface Chore {
|
|
|
158
158
|
const DAY_MS = 86_400_000;
|
|
159
159
|
|
|
160
160
|
/* Where a scheduled chore's guard leaves its report for the woken turn to read. Under /tmp because they are
|
|
161
|
-
* inputs to a turn that starts moments later, never something to keep
|
|
161
|
+
* inputs to a turn that starts moments later, never something to keep, and deliberately the SAME paths the
|
|
162
162
|
* probe runner uses, so a workspace that runs both does not keep two copies of the same measurement. */
|
|
163
163
|
const AUDIT_REPORT = `/tmp/intentic-chore-audit.json`;
|
|
164
164
|
const KNIP_REPORT = `/tmp/intentic-chore-knip.json`;
|
|
@@ -167,7 +167,7 @@ const JSCPD_REPORT = `${JSCPD_DIR}/jscpd-report.json`;
|
|
|
167
167
|
|
|
168
168
|
// How a repo is named to a person and to an agent. "root" is the wire id the daemon's git and health routes
|
|
169
169
|
// already use for the workspace's own repository, and it is a word an agent would otherwise read as a directory
|
|
170
|
-
// called "root"
|
|
170
|
+
// called "root", so it is spelled out here, once, rather than at every call site that builds a prompt.
|
|
171
171
|
export const repoLabel = (repo: string): string => (repo === `root` || repo === `` ? `the workspace root repository` : repo);
|
|
172
172
|
|
|
173
173
|
// The same repository, named for a surface that has a 16rem column or a chip to say it in. `repoLabel` is prose
|
|
@@ -181,7 +181,7 @@ const plural = (count: number, one: string, many = `${one}s`): string => `${coun
|
|
|
181
181
|
// row is a morning's work or a project.
|
|
182
182
|
const outdatedLine = (entry: OutdatedPackage): string => `${entry.kind} · ${entry.name} ${entry.current} → ${entry.latest}`;
|
|
183
183
|
|
|
184
|
-
// The `facts` of a probe that actually ran. Anything else
|
|
184
|
+
// The `facts` of a probe that actually ran. Anything else, never run, unavailable, failed, reads as absent, so
|
|
185
185
|
// no assess() can accidentally treat an unmeasured repo as a measured clean one.
|
|
186
186
|
const factsOf = <T extends ProbeId>(context: ChoreContext, id: T): Extract<NonNullable<ProbeResult["facts"]>, { id: T }> | undefined => {
|
|
187
187
|
const probe = context.probes.get(id);
|
|
@@ -198,7 +198,7 @@ const BLOCKING = new Set<Advisory["severity"]>([`critical`, `high`]);
|
|
|
198
198
|
/* SECURITY. The only chore with no cadence at all: an advisory is not something that becomes worth looking at
|
|
199
199
|
* after thirty days, and there is nothing periodic about it. It is also the only one that reaches `warning`
|
|
200
200
|
* routinely, which is exactly why the bar is critical-or-high and production-or-dev is carried through to the
|
|
201
|
-
* prompt rather than flattened
|
|
201
|
+
* prompt rather than flattened, a moderate advisory in a build-time-only tool badging red is how `warning` stops
|
|
202
202
|
* meaning anything within a week. */
|
|
203
203
|
const security: Chore = {
|
|
204
204
|
id: `security-advisories`,
|
|
@@ -237,24 +237,24 @@ const security: Chore = {
|
|
|
237
237
|
.toSorted((left, right) => left.name.localeCompare(right.name))
|
|
238
238
|
.map(
|
|
239
239
|
(advisory) =>
|
|
240
|
-
`${advisory.severity} · ${advisory.name}
|
|
240
|
+
`${advisory.severity} · ${advisory.name}, ${advisory.title}${advisory.patched === undefined ? ` (no patch yet)` : ``}`,
|
|
241
241
|
),
|
|
242
242
|
// Identities, not counts: every advisory that appears or is fixed is genuinely news, and there is no
|
|
243
243
|
// ordinary drift here to absorb.
|
|
244
244
|
digest: digestOf(...blocking.map((advisory) => `${advisory.name}@${advisory.severity}`).toSorted()),
|
|
245
245
|
severity: production.length > 0 ? `warning` : `info`,
|
|
246
246
|
// Named, not counted. "1 high advisory" tells an agent nothing it can act on, and the first thing it
|
|
247
|
-
// would have to do is re-derive the list we already have
|
|
247
|
+
// would have to do is re-derive the list we already have, badly, because pnpm audit is slow and it
|
|
248
248
|
// would be reading a different tree by then.
|
|
249
249
|
why:
|
|
250
250
|
`pnpm audit reports ${plural(blocking.length, `high or critical advisory`, `high or critical advisories`)} against ` +
|
|
251
|
-
`${repoLabel(context.repo)}
|
|
251
|
+
`${repoLabel(context.repo)}, ${production.length} reaching a production dependency path, ${patchable.length} with a published patched range: ` +
|
|
252
252
|
`${blocking.map((advisory) => `${advisory.name} (${advisory.severity}${advisory.dev ? `, dev-only` : ``}${advisory.patched === undefined ? `, no patch` : `, fixed in ${advisory.patched}`})`).join(`; `)}.`,
|
|
253
253
|
};
|
|
254
254
|
},
|
|
255
255
|
diagnosis: `An advisory with a published fix is a version bump someone has to actually make; one without is a risk to decide about.`,
|
|
256
256
|
goal:
|
|
257
|
-
`For each advisory, establish whether this workspace reaches the vulnerable code path at all
|
|
257
|
+
`For each advisory, establish whether this workspace reaches the vulnerable code path at all: a transitive dependency of a ` +
|
|
258
258
|
`build-time tool is a different problem from one in a running service. Where the fix is a version bump the lockfile can absorb, ` +
|
|
259
259
|
`make it. Where it needs a real upgrade or has no patch published, leave it and say what it would take. Never rewrite ` +
|
|
260
260
|
`application code to route around a CVE.`,
|
|
@@ -262,7 +262,7 @@ const security: Chore = {
|
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
/* DEPENDENCIES. Majors are the finding; the total is context. A repo that is forty patch releases behind is a
|
|
265
|
-
* morning's work and does not need a rail tile, while one major on a framework is a project
|
|
265
|
+
* morning's work and does not need a rail tile, while one major on a framework is a project, so the digest is
|
|
266
266
|
* built from WHICH packages have a major waiting, and a new one appearing is the event. The total count rides
|
|
267
267
|
* along bucketed (digest.ts) so that ordinary drift, which is constant, does not read as news. */
|
|
268
268
|
const OUTDATED_NOISE_FLOOR = 20;
|
|
@@ -297,7 +297,7 @@ const dependencies: Chore = {
|
|
|
297
297
|
detail: majors.toSorted((left, right) => left.name.localeCompare(right.name)).map(outdatedLine),
|
|
298
298
|
digest: digestOf(...majors.map((entry) => `${entry.name}@${entry.latest}`).toSorted(), `total:${bucketOf(facts.packages.length)}`),
|
|
299
299
|
severity: `info`,
|
|
300
|
-
// The majors are named because they are what the turn is actually about
|
|
300
|
+
// The majors are named because they are what the turn is actually about, the minors and patches are a
|
|
301
301
|
// bulk operation the agent will enumerate itself, and listing four hundred of them here would bury it.
|
|
302
302
|
why:
|
|
303
303
|
`pnpm outdated reports ${plural(facts.packages.length, `dependency`, `dependencies`)} behind the registry in ` +
|
|
@@ -307,7 +307,7 @@ const dependencies: Chore = {
|
|
|
307
307
|
},
|
|
308
308
|
diagnosis: `Version drift is cheap to fix continuously and expensive to fix in one go, because the majors start depending on each other.`,
|
|
309
309
|
goal:
|
|
310
|
-
`Take the patch and minor upgrades in one pass
|
|
310
|
+
`Take the patch and minor upgrades in one pass: those are what the lockfile can absorb without argument. Then take the majors ` +
|
|
311
311
|
`ONE AT A TIME, reading each one's changelog for breaking changes before you touch anything, and stop at the first one that ` +
|
|
312
312
|
`needs more than a mechanical fix: leave it, and say what it would take. Do not batch majors; a failing test after eight of them ` +
|
|
313
313
|
`is a bisect nobody wanted.`,
|
|
@@ -367,7 +367,7 @@ const deadCode: Chore = {
|
|
|
367
367
|
},
|
|
368
368
|
diagnosis: `Code nothing reaches still has to be read, type-checked and kept compiling by everyone who works nearby.`,
|
|
369
369
|
goal:
|
|
370
|
-
`Re-run knip yourself first
|
|
370
|
+
`Re-run knip yourself first: this measurement is hours old and the tree has moved. Then check each finding against how the ` +
|
|
371
371
|
`file is actually used: knip is confidently wrong about anything reachable from OUTSIDE the repository, which means a package's ` +
|
|
372
372
|
`public entry points, files a bundler or framework loads by convention, and types consumed only by a downstream package. Delete ` +
|
|
373
373
|
`what is genuinely unreachable. Leave the false positives and list them in one line each, so the next run's reader knows they ` +
|
|
@@ -386,7 +386,7 @@ const duplication: Chore = {
|
|
|
386
386
|
id: `duplication`,
|
|
387
387
|
title: `Find duplication worth collapsing`,
|
|
388
388
|
icon: `clone`,
|
|
389
|
-
description: `Copy-paste that has grown past a fifth of a percent of the tree. Reports only
|
|
389
|
+
description: `Copy-paste that has grown past a fifth of a percent of the tree. Reports only, extracting is a design call.`,
|
|
390
390
|
kind: `drifting`,
|
|
391
391
|
criterion: `jscpd reports more than 5% of the scanned tree duplicated.`,
|
|
392
392
|
stance: `report`,
|
|
@@ -421,16 +421,16 @@ const duplication: Chore = {
|
|
|
421
421
|
`the largest are ${top.map((clone) => `${clone.first} ↔ ${clone.second} (${clone.lines} lines)`).join(`; `)}.`,
|
|
422
422
|
};
|
|
423
423
|
},
|
|
424
|
-
diagnosis: `Duplication only costs anything when the copies have to change together
|
|
424
|
+
diagnosis: `Duplication only costs anything when the copies have to change together, and only some of it does.`,
|
|
425
425
|
goal:
|
|
426
426
|
`Report the clones where the copies genuinely have to change together. For each: cite both file:line ranges, say what the shared ` +
|
|
427
427
|
`concept actually is, and name where the extraction would live. Then say explicitly which of the reported clones you are NOT ` +
|
|
428
|
-
`recommending against
|
|
428
|
+
`recommending against: generated files, deliberately repetitive tests, and lookalikes owned by different subsystems, so the ` +
|
|
429
429
|
`next reader knows the list was triaged rather than truncated.`,
|
|
430
430
|
done: `Done when every clone in the report has either a named extraction or a one-line reason it should stay.`,
|
|
431
431
|
};
|
|
432
432
|
|
|
433
|
-
/* DOCUMENTATION. The evidence is a package with no README
|
|
433
|
+
/* DOCUMENTATION. The evidence is a package with no README, which IS its architecture document in this
|
|
434
434
|
* workspace, so this is a stat on the package directory rather than a lookup in a parallel tree. It sounds like
|
|
435
435
|
* a coverage statistic
|
|
436
436
|
* and would be one if the rail read it directly. It does not: the digest is the SET of undocumented package
|
|
@@ -440,7 +440,7 @@ const documentation: Chore = {
|
|
|
440
440
|
id: `documentation-refresh`,
|
|
441
441
|
title: `Document what nothing explains`,
|
|
442
442
|
icon: `file-edit`,
|
|
443
|
-
description: `Packages in this repository with no README
|
|
443
|
+
description: `Packages in this repository with no README, new ones first.`,
|
|
444
444
|
kind: `drifting`,
|
|
445
445
|
criterion: `A workspace package has no README.`,
|
|
446
446
|
applies: (signals) => (signals.packages.length > 0 ? undefined : `not a workspace`),
|
|
@@ -464,7 +464,7 @@ const documentation: Chore = {
|
|
|
464
464
|
},
|
|
465
465
|
diagnosis: `A package nobody can read the shape of gets worked in by guesswork, and the guesses accumulate.`,
|
|
466
466
|
goal:
|
|
467
|
-
`Follow this workspace's own documentation conventions
|
|
467
|
+
`Follow this workspace's own documentation conventions: read them first, they are not optional and they are not generic. For ` +
|
|
468
468
|
`each undocumented package, read the package before you write a word about it, and produce the document its conventions call ` +
|
|
469
469
|
`for: what the package is FOR, how it fits the system, and which files matter. Explain at the module level. Never describe code ` +
|
|
470
470
|
`line by line, and never document a package you did not read.`,
|
|
@@ -472,11 +472,11 @@ const documentation: Chore = {
|
|
|
472
472
|
};
|
|
473
473
|
|
|
474
474
|
/* COMPLEXITY. The one chore whose evidence comes from the resident index rather than a subprocess, and the one
|
|
475
|
-
* most at risk of being a ranking laundered into a to-do list
|
|
475
|
+
* most at risk of being a ranking laundered into a to-do list, there is ALWAYS a top of a hotspot ranking, and
|
|
476
476
|
* "your worst file" is not a finding. So it does not report the ranking. It reports the two shapes within it that
|
|
477
477
|
* are genuinely arguable:
|
|
478
478
|
*
|
|
479
|
-
* volatile AND
|
|
479
|
+
* volatile AND depended-on a hotspot that is also a key module: every edit ripples outward.
|
|
480
480
|
* out of proportion branching three times the median of its own ranking: tangled, not merely busy.
|
|
481
481
|
*
|
|
482
482
|
* Both are relative to the same list the user is reading, so nothing here needs tuning per repository or per
|
|
@@ -496,7 +496,7 @@ const complexity: Chore = {
|
|
|
496
496
|
id: `complexity`,
|
|
497
497
|
title: `Simplify what everything waits on`,
|
|
498
498
|
icon: `wave-pulse`,
|
|
499
|
-
description: `Files that both churn and carry the repository
|
|
499
|
+
description: `Files that both churn and carry the repository, where edits are slow and ripple outward.`,
|
|
500
500
|
kind: `accruing`,
|
|
501
501
|
criterion: `A file in the hotspot ranking is also a key module, or its branching is three times the median of that ranking.`,
|
|
502
502
|
stance: `act`,
|
|
@@ -520,7 +520,7 @@ const complexity: Chore = {
|
|
|
520
520
|
keyModules.has(path) ? `churns and the rest of the repository imports it` : `${branches} branch points against a median of ${middle}`;
|
|
521
521
|
return {
|
|
522
522
|
headline: `${plural(found.length, `file`)} where every edit is slow and ripples outward`,
|
|
523
|
-
detail: found.map((hotspot) => `${hotspot.path}
|
|
523
|
+
detail: found.map((hotspot) => `${hotspot.path}, ${hotspot.commits} commits, ${reason(hotspot.path, hotspot.complexity)}`),
|
|
524
524
|
digest: digestOf(...found.map((hotspot) => hotspot.path).toSorted()),
|
|
525
525
|
severity: `info`,
|
|
526
526
|
why:
|
|
@@ -530,16 +530,16 @@ const complexity: Chore = {
|
|
|
530
530
|
},
|
|
531
531
|
diagnosis: `A file that changes constantly and branches heavily makes every edit near it slow and easy to get wrong.`,
|
|
532
532
|
goal:
|
|
533
|
-
`Take ONE file
|
|
533
|
+
`Take ONE file: the worst of them, and no more. Read it first. If the rest of the repository imports it, separate the stable ` +
|
|
534
534
|
`contract from the churn: a narrow surface for importers, the volatile implementation private behind it. If it is simply ` +
|
|
535
|
-
`tangled, flatten it where it stands
|
|
536
|
-
`lookups
|
|
535
|
+
`tangled, flatten it where it stands: edge cases as early returns, compound conditions behind named predicates, long chains as ` +
|
|
536
|
+
`lookups, and extract a unit only if a cohesive one falls out. Behaviour stays identical, and no re-export shims are left behind.`,
|
|
537
537
|
done: `Done when \`iq hotspots\` reports materially fewer branch points for that file, the repository's checks pass, and no importer changed meaning.`,
|
|
538
538
|
};
|
|
539
539
|
|
|
540
540
|
/* RUNTIME. A static table, and it is honest about being one: there is no network call here, so the dates below
|
|
541
541
|
* are a fact about the day this file was last edited rather than a live feed. That is the right trade for a
|
|
542
|
-
* signal that moves twice a year and must work on a box with no outbound access
|
|
542
|
+
* signal that moves twice a year and must work on a box with no outbound access, but it does mean this table is
|
|
543
543
|
* maintenance in its own right, and a major missing from it reads as "not end-of-life", which is the safe way to
|
|
544
544
|
* be wrong. Source: nodejs/Release. */
|
|
545
545
|
const NODE_EOL: Readonly<Record<number, string>> = {
|
|
@@ -550,7 +550,7 @@ const NODE_EOL: Readonly<Record<number, string>> = {
|
|
|
550
550
|
24: `2028-04-30`,
|
|
551
551
|
};
|
|
552
552
|
// How far ahead of an end-of-life date the chore starts speaking. A quarter, because moving a runtime is planned
|
|
553
|
-
// work
|
|
553
|
+
// work, telling someone the day security patches stop is telling them too late to do anything but scramble.
|
|
554
554
|
const EOL_HORIZON_MS = 90 * DAY_MS;
|
|
555
555
|
|
|
556
556
|
const runtime: Chore = {
|
|
@@ -591,14 +591,14 @@ const runtime: Chore = {
|
|
|
591
591
|
digest: digestOf(`node:${major}`, past ? `eol` : `approaching`),
|
|
592
592
|
severity: past ? `warning` : `info`,
|
|
593
593
|
why:
|
|
594
|
-
`This sandbox runs ${context.node}, and Node ${major} ${past ? `reached end of life on ${eol}` : `reaches end of life on ${eol}`}
|
|
594
|
+
`This sandbox runs ${context.node}, and Node ${major} ${past ? `reached end of life on ${eol}` : `reaches end of life on ${eol}`}, ` +
|
|
595
595
|
`${plural(pinned.length, `package`)} in ${repoLabel(context.repo)} pin a node engine range.`,
|
|
596
596
|
};
|
|
597
597
|
},
|
|
598
598
|
diagnosis: `An unsupported runtime stops receiving security patches, so every advisory against it stays open permanently.`,
|
|
599
599
|
goal:
|
|
600
600
|
`Establish what actually pins this runtime: the image's own base, the workspace's useNodeVersion, and each package's engines ` +
|
|
601
|
-
`range. Propose the smallest move to a supported LTS
|
|
601
|
+
`range. Propose the smallest move to a supported LTS, which of those pins have to change, in what order, and what is likely to ` +
|
|
602
602
|
`break at that boundary. Make the pin changes that are mechanical; do NOT attempt the image rebuild itself.`,
|
|
603
603
|
done: `Done when the pins name a supported release, the repository's type-check and tests pass on it, and anything needing a rebuild is named as such.`,
|
|
604
604
|
};
|
|
@@ -607,7 +607,7 @@ const runtime: Chore = {
|
|
|
607
607
|
* using a library for this?"). Two libraries that solve the same problem in one tree is a fact, not an opinion:
|
|
608
608
|
* somebody added the second one without removing the first, both are now in the bundle, and new code picks
|
|
609
609
|
* whichever the neighbouring file used. The table below is deliberately short and only names categories where
|
|
610
|
-
* having two is genuinely a mistake
|
|
610
|
+
* having two is genuinely a mistake, not, say, two test runners, which is an ordinary migration. */
|
|
611
611
|
const CATEGORIES: readonly { readonly category: string; readonly members: readonly string[] }[] = [
|
|
612
612
|
{ category: `date handling`, members: [`moment`, `dayjs`, `date-fns`, `luxon`, `js-joda`] },
|
|
613
613
|
{ category: `HTTP clients`, members: [`axios`, `got`, `node-fetch`, `superagent`, `undici`, `request`] },
|
|
@@ -622,7 +622,7 @@ const libraries: Chore = {
|
|
|
622
622
|
id: `library-overlap`,
|
|
623
623
|
title: `Settle on one library per job`,
|
|
624
624
|
icon: `box`,
|
|
625
|
-
description: `Two dependencies solving the same problem
|
|
625
|
+
description: `Two dependencies solving the same problem, both shipped, both maintained, one picked at random.`,
|
|
626
626
|
kind: `drifting`,
|
|
627
627
|
criterion: `Two or more installed dependencies do the same job.`,
|
|
628
628
|
applies: (signals) => (signals.packages.length > 0 ? undefined : `not a workspace`),
|
|
@@ -649,7 +649,7 @@ const libraries: Chore = {
|
|
|
649
649
|
},
|
|
650
650
|
diagnosis: `Two libraries for one job means both ship, both need upgrading, and new code picks whichever the neighbouring file used.`,
|
|
651
651
|
goal:
|
|
652
|
-
`For each overlapping pair, find out which one is actually
|
|
652
|
+
`For each overlapping pair, find out which one is actually used. How many call sites each has, whether one is a ` +
|
|
653
653
|
`transitive dependency nobody chose, and whether either is unmaintained. Recommend the one to keep and estimate the migration ` +
|
|
654
654
|
`honestly, including the call sites where the two libraries genuinely differ in behaviour. Where the overlap is deliberate or ` +
|
|
655
655
|
`the second is only transitive, say so and close the question.`,
|
|
@@ -659,17 +659,17 @@ const libraries: Chore = {
|
|
|
659
659
|
/* ---- THE FRONT-END CHORES -------------------------------------------------------------------------------------
|
|
660
660
|
*
|
|
661
661
|
* Four chores that only exist where a UI framework does, kept together because they share one gate and one
|
|
662
|
-
* probe
|
|
662
|
+
* probe, and split across the reading order in CHORES, since where a row belongs is decided by what KIND of
|
|
663
663
|
* finding it is, not by which file paragraph it was written in.
|
|
664
664
|
*
|
|
665
665
|
* They gate on `shape.deps` rather than on `signals.packages`, and that is not interchangeable. `packages` is
|
|
666
|
-
* populated from pnpm-workspace.yaml, so it is EMPTY for a repository that is not a monorepo
|
|
666
|
+
* populated from pnpm-workspace.yaml, so it is EMPTY for a repository that is not a monorepo, which is what a
|
|
667
667
|
* Vite app, a Next app and an Angular CLI project all are. A framework gate reading it would be permanently dark
|
|
668
668
|
* in the overwhelming majority of the repositories these four were written for, and dark silently: the chores
|
|
669
669
|
* would not appear, the footer would say the repository has no packages, and nothing would look broken.
|
|
670
670
|
*
|
|
671
671
|
* All four also say something the rest of the book does not have to. A component, a class name and a bundle chunk
|
|
672
|
-
* are things nobody sees the whole of
|
|
672
|
+
* are things nobody sees the whole of, you read one component at a time, and the tenth copy of a button looks
|
|
673
673
|
* exactly like the first nine did. That is the same argument the whole surface rests on, just further from the
|
|
674
674
|
* places a compiler will ever help. */
|
|
675
675
|
|
|
@@ -689,21 +689,21 @@ const bytesLabel = (bytes: number): string => (bytes >= 1024 * 1024 ? `${(bytes
|
|
|
689
689
|
/* BUNDLE. What a browser downloads before anything appears, which is the fact about a front-end that is furthest
|
|
690
690
|
* from anything visible in an editor: every dependency looks the same size in an import statement.
|
|
691
691
|
*
|
|
692
|
-
* The criterion is a SHARE, and that is deliberate
|
|
692
|
+
* The criterion is a SHARE, and that is deliberate, it is the second exception to the book's leader-relative
|
|
693
693
|
* rule, and it earns the same defence duplication's 5% does. A byte threshold would need a different value for a
|
|
694
694
|
* marketing page and an IDE, would be argued about forever, and would be wrong the moment either one grew. "One
|
|
695
695
|
* chunk is more than half of everything you ship" needs no calibration: it says the build is not split, which is
|
|
696
696
|
* true or false at any size. A well-split app has its largest chunk well under this whatever it weighs, and a
|
|
697
|
-
* small app that genuinely is one chunk trips it and is right to
|
|
697
|
+
* small app that genuinely is one chunk trips it and is right to, that IS its entire download.
|
|
698
698
|
*
|
|
699
699
|
* Report-stance. Where the split boundaries go is a routing and product decision, and an agent that lazily
|
|
700
700
|
* imported things unattended at three in the morning would be making it. */
|
|
701
701
|
const BUNDLE_SHARE_FLOOR = 50;
|
|
702
|
-
// Below this there is no ranking to be an outlier in
|
|
702
|
+
// Below this there is no ranking to be an outlier in, two files cannot tell you anything about how a build is
|
|
703
703
|
// divided, and the largest of them is over half by arithmetic rather than by fault.
|
|
704
704
|
const BUNDLE_MIN_ASSETS = 3;
|
|
705
705
|
|
|
706
|
-
/* An asset's name with its content hash taken out
|
|
706
|
+
/* An asset's name with its content hash taken out, `assets/vendor-DlAUqK2U.js` becomes `assets/vendor.js`.
|
|
707
707
|
*
|
|
708
708
|
* Without this the digest changes on every single build, because a content hash changing is the entire point of a
|
|
709
709
|
* content hash. The chore would badge after every `pnpm build` while reporting nothing new, which is precisely
|
|
@@ -734,7 +734,7 @@ const bundleWeight: Chore = {
|
|
|
734
734
|
return undefined;
|
|
735
735
|
}
|
|
736
736
|
// By GZIP, not by raw bytes. What is on disk is not what crosses the wire, and a large but highly
|
|
737
|
-
// compressible asset
|
|
737
|
+
// compressible asset, a source map comment, a big JSON blob, is not the download this is about.
|
|
738
738
|
const ranked = assets.toSorted((left, right) => right.gzip - left.gzip);
|
|
739
739
|
const largest = ranked[0];
|
|
740
740
|
if (largest === undefined) {
|
|
@@ -762,7 +762,7 @@ const bundleWeight: Chore = {
|
|
|
762
762
|
severity: `info`,
|
|
763
763
|
why:
|
|
764
764
|
`The build output in ${dir}/ of ${repoLabel(context.repo)} is ${bytesLabel(totalGzip)} gzipped across ` +
|
|
765
|
-
`${plural(assets.length, `asset`)}, and ${largest.path} alone is ${bytesLabel(largest.gzip)} of it
|
|
765
|
+
`${plural(assets.length, `asset`)}, and ${largest.path} alone is ${bytesLabel(largest.gzip)} of it: ${Math.round(share)}%. ` +
|
|
766
766
|
`The next largest are ${ranked
|
|
767
767
|
.slice(1, 4)
|
|
768
768
|
.map((asset) => `${asset.path} (${bytesLabel(asset.gzip)})`)
|
|
@@ -772,7 +772,7 @@ const bundleWeight: Chore = {
|
|
|
772
772
|
},
|
|
773
773
|
diagnosis: `Everything in the first chunk is downloaded and parsed before anything renders, whether or not the visitor needed it.`,
|
|
774
774
|
goal:
|
|
775
|
-
`Find out what is actually IN the dominant chunk before proposing anything
|
|
775
|
+
`Find out what is actually IN the dominant chunk before proposing anything: the repository's own bundler can report this, and a ` +
|
|
776
776
|
`recommendation made without it is guesswork. Then report the split worth making: which routes or features could load on demand, ` +
|
|
777
777
|
`which dependencies are pulled in wholesale for one function, and which are only used behind an interaction nobody has yet had. ` +
|
|
778
778
|
`Name the boundary for each and estimate what it saves. Where the chunk is genuinely all first-paint code, say so and close it.`,
|
|
@@ -780,7 +780,7 @@ const bundleWeight: Chore = {
|
|
|
780
780
|
};
|
|
781
781
|
|
|
782
782
|
/* FRAMEWORK IDIOMS. A migration nobody finished, which is the most ordinary state for a front-end of any age: the
|
|
783
|
-
* new way arrived, the new files use it, and the old files keep working
|
|
783
|
+
* new way arrived, the new files use it, and the old files keep working, so nothing ever forces the rest.
|
|
784
784
|
*
|
|
785
785
|
* The digest is the one place this chore differs in shape from its neighbours, and it has to. Digesting the file
|
|
786
786
|
* identities, the way the documentation chore does, would re-badge every time anyone touched any of two hundred
|
|
@@ -807,13 +807,13 @@ const frameworkIdiom: Chore = {
|
|
|
807
807
|
* embarrassing.
|
|
808
808
|
*
|
|
809
809
|
* AN IDIOM THIS BUILD HAS NEVER HEARD OF. The daemon composes the sweep from its own copy of the table, so
|
|
810
|
-
* a sandbox image ahead of the browser can report a rule that has no label or replacement here
|
|
810
|
+
* a sandbox image ahead of the browser can report a rule that has no label or replacement here, and a row
|
|
811
811
|
* saying "42 files use react-foo" with no idea what to do about them is worse than no row.
|
|
812
812
|
*
|
|
813
813
|
* AN IDIOM BELONGING TO A FRAMEWORK THIS REPOSITORY DOES NOT USE. A probe's command is a fixed string, so
|
|
814
814
|
* every rule in the table is swept in every repository, and an Angular pattern gets its chance in a Vue
|
|
815
|
-
* codebase: `RouterModule.forRoot` inside a comment, a `*ngIf` in an example string, and
|
|
816
|
-
* caught this
|
|
815
|
+
* codebase: `RouterModule.forRoot` inside a comment, a `*ngIf` in an example string, and, the case that
|
|
816
|
+
* caught this, the book's own rule table quoting its own patterns back at it. What the repository
|
|
817
817
|
* DECLARES is the arbiter, the same `deps` the gate above reads. */
|
|
818
818
|
const frameworks = new Set(frameworksOf(context.signals.shape.deps).map((framework) => framework.id));
|
|
819
819
|
const found = facts.scan.idioms.flatMap(({ id, files }) => {
|
|
@@ -842,8 +842,8 @@ const frameworkIdiom: Chore = {
|
|
|
842
842
|
diagnosis: `A retired idiom keeps working until the major release that drops it, and then it is an emergency inside somebody else's upgrade.`,
|
|
843
843
|
goal:
|
|
844
844
|
`Take ONE idiom, the one with the most files, and no more. Convert the files where the conversion is mechanical and the behaviour ` +
|
|
845
|
-
`is provably identical. Stop at the first file that needs a design decision
|
|
846
|
-
`semantics, an NgModule that something outside the repository imports
|
|
845
|
+
`is provably identical. Stop at the first file that needs a design decision: a class component with genuine error-boundary ` +
|
|
846
|
+
`semantics, an NgModule that something outside the repository imports: leave it, and say what it would take. Do not convert an ` +
|
|
847
847
|
`idiom the repository has deliberately kept: if the newest code uses it too, that is a choice, and reporting it as one is the ` +
|
|
848
848
|
`useful answer.`,
|
|
849
849
|
done: `Done when a re-scan reports fewer files on that idiom, the repository's type-check and tests pass, and every file you skipped has a one-line reason.`,
|
|
@@ -855,20 +855,20 @@ const frameworkIdiom: Chore = {
|
|
|
855
855
|
* code and neither knows the other exists.
|
|
856
856
|
*
|
|
857
857
|
* TWO KINDS OF EVIDENCE, and they catch opposite failures. A NAME FAMILY catches components that were written
|
|
858
|
-
* separately and never shared a line
|
|
858
|
+
* separately and never shared a line, `BaseButton.vue` and `ButtonV2.tsx` reduce to the same stem, and no clone
|
|
859
859
|
* detector will ever connect them. A CLONE PAIR catches the reverse: two components with unrelated names doing
|
|
860
860
|
* the same work, which is what jscpd is actually good at, filtered to the pairs where both sides are components
|
|
861
861
|
* so it is a finding about the UI rather than a slice of the repo-wide duplication chore.
|
|
862
862
|
*
|
|
863
863
|
* It needs jscpd rather than reading it if present. Half a measurement would let the row claim it had looked for
|
|
864
|
-
* shared logic in a repository where that sweep has never run
|
|
864
|
+
* shared logic in a repository where that sweep has never run, the exact "measured and found nothing" lie the
|
|
865
865
|
* `unavailable` state exists to make impossible. jscpd is already running weekly for the duplication chore in any
|
|
866
866
|
* Node repository, so the honest choice is also the free one. */
|
|
867
867
|
const componentOverlap: Chore = {
|
|
868
868
|
id: `component-overlap`,
|
|
869
869
|
title: `Settle on one component per job`,
|
|
870
870
|
icon: `copy`,
|
|
871
|
-
description: `Components built twice
|
|
871
|
+
description: `Components built twice, the same name in two places, or the same logic under two names.`,
|
|
872
872
|
kind: `drifting`,
|
|
873
873
|
criterion: `Two component files reduce to the same name, or a duplicated block spans two components.`,
|
|
874
874
|
applies: needsFramework,
|
|
@@ -932,12 +932,12 @@ const componentOverlap: Chore = {
|
|
|
932
932
|
`${pairs.length === 0 ? `` : `The clones: ${pairs.map((clone) => `${normalizePath(clone.first)} ↔ ${normalizePath(clone.second)}, ${clone.lines} lines`).join(`; `)}.`}`,
|
|
933
933
|
};
|
|
934
934
|
},
|
|
935
|
-
diagnosis: `A component built twice is maintained once
|
|
935
|
+
diagnosis: `A component built twice is maintained once, whichever copy the next person happens to open is the one that gets the fix.`,
|
|
936
936
|
goal:
|
|
937
937
|
`Read every file in each group before saying anything about it; a shared name is a reason to look, not a finding on its own. For ` +
|
|
938
938
|
`each group, say whether these genuinely do the same job, and if they do, name the one to keep and count the call sites that would ` +
|
|
939
|
-
`have to move. Where the answer is that the same LOGIC is duplicated rather than the whole component
|
|
940
|
-
`state, the same form validation, the same list virtualization written twice
|
|
939
|
+
`have to move. Where the answer is that the same LOGIC is duplicated rather than the whole component: the same fetch and loading ` +
|
|
940
|
+
`state, the same form validation, the same list virtualization written twice: say so, and name the hook or composable it should ` +
|
|
941
941
|
`become and where it would live. Where two components share a name and nothing else, say that too and close it: a false family is ` +
|
|
942
942
|
`worth one line, and the next reader needs to know it was considered.`,
|
|
943
943
|
done: `Done when every group has either a component to keep with a call-site count, a shared unit to extract with a home, or a reason it is fine.`,
|
|
@@ -945,7 +945,7 @@ const componentOverlap: Chore = {
|
|
|
945
945
|
|
|
946
946
|
/* TAILWIND. A design system exists to make a decision once; an arbitrary value is that decision being made again,
|
|
947
947
|
* inline, by whoever was in the file. What makes this measurable rather than a matter of taste is that Tailwind
|
|
948
|
-
* spells the bypass out loud
|
|
948
|
+
* spells the bypass out loud, `bg-[#3b82f6]` is the palette being stepped around, in the markup, in a form no
|
|
949
949
|
* reviewer can miss and no linter mentions.
|
|
950
950
|
*
|
|
951
951
|
* Deliberately NOT every arbitrary value. `grid-cols-[1fr_auto]` is the feature working as intended and there is
|
|
@@ -977,7 +977,7 @@ const tailwindBypass: Chore = {
|
|
|
977
977
|
return {
|
|
978
978
|
headline: `${plural(total, `hard-coded value`)} across ${plural(bypasses.length, `file`)}`,
|
|
979
979
|
detail: worst.map((entry) => `${entry.path} · ${plural(entry.count, `value`)}`),
|
|
980
|
-
// The worst files by identity
|
|
980
|
+
// The worst files by identity, a new file arriving at the top of this list is the event, with the
|
|
981
981
|
// spread and the total riding along bucketed, because both drift by one every time anyone writes
|
|
982
982
|
// markup and neither is worth interrupting somebody about.
|
|
983
983
|
digest: digestOf(...worst.map((entry) => entry.path).toSorted(), `files:${bucketOf(bypasses.length)}`, `total:${bucketOf(total)}`),
|
|
@@ -991,18 +991,18 @@ const tailwindBypass: Chore = {
|
|
|
991
991
|
.join(`, `)}.`,
|
|
992
992
|
};
|
|
993
993
|
},
|
|
994
|
-
diagnosis: `Every inline colour is a place the theme cannot reach
|
|
994
|
+
diagnosis: `Every inline colour is a place the theme cannot reach, a palette change lands everywhere except the files that opted out of it.`,
|
|
995
995
|
goal:
|
|
996
|
-
`Read the theme first
|
|
996
|
+
`Read the theme first: the Tailwind config, or the CSS that defines the tokens, so you know what the scale actually offers. Then ` +
|
|
997
997
|
`replace the values that have a token: an exact palette match, a spacing step, a type size. Where a value is CLOSE to a token but ` +
|
|
998
998
|
`not equal, do not round it silently; that is a visual change wearing a refactor's clothes. List those separately with both values ` +
|
|
999
|
-
`and let the owner decide. Where a value has no token and should
|
|
999
|
+
`and let the owner decide. Where a value has no token and should: a brand colour used in nine places, say that the theme is ` +
|
|
1000
1000
|
`missing an entry rather than editing nine files.`,
|
|
1001
1001
|
done: `Done when a re-scan reports fewer hard-coded values, nothing renders differently, and every value you left has a one-line reason.`,
|
|
1002
1002
|
};
|
|
1003
1003
|
|
|
1004
1004
|
/* THE SURVEYS. Chores with no measurement at all, and they are here because the absence of a measurement is not
|
|
1005
|
-
* the absence of value
|
|
1005
|
+
* the absence of value, these are the reviews a codebase silently rots without, and none of them can be detected
|
|
1006
1006
|
* by a tool. Their trigger is the calendar, and the ledger is what makes that trigger honest: a survey is due
|
|
1007
1007
|
* because it has not been done in a quarter, which is a claim the panel can show and the reader can check.
|
|
1008
1008
|
*
|
|
@@ -1011,8 +1011,8 @@ const tailwindBypass: Chore = {
|
|
|
1011
1011
|
*
|
|
1012
1012
|
* A SURVEY NEEDS ITS `applies` GATE MORE THAN A MEASURED CHORE DOES, not less, and this is the trap the shape of
|
|
1013
1013
|
* the thing sets. A measured chore is gated by its own evidence for free: no undocumented packages, no finding,
|
|
1014
|
-
* no row. A survey has no evidence to be absent
|
|
1015
|
-
* world
|
|
1014
|
+
* no row. A survey has no evidence to be absent, "90 days have passed" is true of every repository in the
|
|
1015
|
+
* world, so without a gate it fires everywhere, forever, including in the repositories where its subject does
|
|
1016
1016
|
* not exist. "Re-read the documentation against the code" in a repository with no documentation is the exact
|
|
1017
1017
|
* failure, and it is not a hypothetical: it is what this helper did before the gate existed.
|
|
1018
1018
|
*
|
|
@@ -1028,7 +1028,7 @@ interface SurveySpec {
|
|
|
1028
1028
|
readonly done: string;
|
|
1029
1029
|
readonly cadenceDays: number;
|
|
1030
1030
|
// What must exist in the repository for this review to have a subject. Required, not optional, precisely
|
|
1031
|
-
// because forgetting it is the failure mode above
|
|
1031
|
+
// because forgetting it is the failure mode above, a survey that genuinely applies everywhere still has to
|
|
1032
1032
|
// say so out loud, with `() => undefined`.
|
|
1033
1033
|
readonly applies: (signals: ChoreSignals) => string | undefined;
|
|
1034
1034
|
}
|
|
@@ -1055,7 +1055,7 @@ const survey = ({ id, title, icon, description, diagnosis, goal, done, cadenceDa
|
|
|
1055
1055
|
detail: [`Cadence · every ${cadenceDays} days`],
|
|
1056
1056
|
digest: digestOf(id, `period:${Math.floor(context.nowMs / (cadenceDays * DAY_MS))}`),
|
|
1057
1057
|
severity: `info`,
|
|
1058
|
-
why: `This is a periodic review of ${repoLabel(context.repo)}, run every ${cadenceDays} days; nothing measured it
|
|
1058
|
+
why: `This is a periodic review of ${repoLabel(context.repo)}, run every ${cadenceDays} days; nothing measured it, it is due because it has been that long.`,
|
|
1059
1059
|
}),
|
|
1060
1060
|
diagnosis,
|
|
1061
1061
|
goal,
|
|
@@ -1071,11 +1071,11 @@ const patterns = survey({
|
|
|
1071
1071
|
id: `standardize-patterns`,
|
|
1072
1072
|
title: `Standardize the cross-cutting patterns`,
|
|
1073
1073
|
icon: `sitemap`,
|
|
1074
|
-
description: `Error handling, validation, logging, configuration, retries, pagination
|
|
1074
|
+
description: `Error handling, validation, logging, configuration, retries, pagination, the things every file does slightly differently.`,
|
|
1075
1075
|
diagnosis: `Cross-cutting concerns drift one file at a time, and the cost only shows up when someone has to work across several of them.`,
|
|
1076
1076
|
goal:
|
|
1077
|
-
`Pick the cross-cutting concerns this repository actually has
|
|
1078
|
-
`pagination, serialization
|
|
1077
|
+
`Pick the cross-cutting concerns this repository actually has: error handling, input validation, logging, configuration, retries, ` +
|
|
1078
|
+
`pagination, serialization, and for each, survey how it is done. Name the dominant pattern, the outliers, and which of the ` +
|
|
1079
1079
|
`outliers are deliberate. Recommend ONE convention per concern with a file to point at as the reference implementation, and ` +
|
|
1080
1080
|
`estimate the size of the conversion. Do not convert anything.`,
|
|
1081
1081
|
done: `Done when each concern has a named convention, a reference file, and a count of the sites that diverge from it.`,
|
|
@@ -1093,7 +1093,7 @@ const deprecated = survey({
|
|
|
1093
1093
|
diagnosis: `A deprecated API works right up until the upgrade that removes it, and then it is an emergency during someone else's migration.`,
|
|
1094
1094
|
goal:
|
|
1095
1095
|
`Survey what this repository uses that its own dependencies have deprecated: read the framework and runtime versions in use, check ` +
|
|
1096
|
-
`their deprecation notices, and search for the call sites. Include the repository's OWN deprecations
|
|
1096
|
+
`their deprecation notices, and search for the call sites. Include the repository's OWN deprecations: anything its code marks ` +
|
|
1097
1097
|
`as deprecated and still calls. Rank by when each one actually breaks, not by how many call sites it has, and name the ` +
|
|
1098
1098
|
`replacement for each. Change nothing.`,
|
|
1099
1099
|
done: `Done when every deprecation has call sites cited, a replacement named, and the release it is expected to break in.`,
|
|
@@ -1104,30 +1104,30 @@ const deprecated = survey({
|
|
|
1104
1104
|
/* THE CHORE THAT NAMED THE PROBLEM. Gated on documents actually EXISTING, which is the whole reason `applies`
|
|
1105
1105
|
* exists: without it this survey fires on its cadence in every repository, including the ones with nothing to
|
|
1106
1106
|
* re-read, and the first thing an owner of a fresh workspace sees is an offer to re-read documentation they have
|
|
1107
|
-
* never written. That is not a chore being wrong about a threshold
|
|
1107
|
+
* never written. That is not a chore being wrong about a threshold, it is the surface admitting it never looked.
|
|
1108
1108
|
*
|
|
1109
1109
|
* Note which fact it gates on: the MAP, not the directory. An empty `docs/architecture/` is a directory somebody
|
|
1110
1110
|
* made and never filled, and a gate on the directory would put the chore back exactly where it started. The
|
|
1111
|
-
* survey then reads the package READMEs too
|
|
1111
|
+
* survey then reads the package READMEs too, they are the package pages, but a repo with no map has not been
|
|
1112
1112
|
* documented at all, and that is the case worth staying quiet for. */
|
|
1113
1113
|
const documentationDrift = survey({
|
|
1114
1114
|
id: `documentation-drift`,
|
|
1115
1115
|
title: `Re-read the documentation against the code`,
|
|
1116
1116
|
icon: `file`,
|
|
1117
|
-
description: `Whether what the documents claim is still what the code does
|
|
1117
|
+
description: `Whether what the documents claim is still what the code does, the drift no tool can measure.`,
|
|
1118
1118
|
diagnosis: `Documentation is trusted in proportion to how recently it was true, and a document that is quietly wrong is worse than a missing one.`,
|
|
1119
1119
|
goal:
|
|
1120
1120
|
`Read this repository's architecture documents against the code they describe. Report every claim that is no longer true, citing the ` +
|
|
1121
|
-
`document line and the file that contradicts it. Prioritise the claims someone would ACT on
|
|
1122
|
-
`what, which file to change
|
|
1121
|
+
`document line and the file that contradicts it. Prioritise the claims someone would ACT on, where a subsystem lives, what owns ` +
|
|
1122
|
+
`what, which file to change: over prose that has merely aged. Do not rewrite the documents; produce the list of what is wrong.`,
|
|
1123
1123
|
done: `Done when every architecture document has been read and every false claim is listed with both sides cited.`,
|
|
1124
1124
|
cadenceDays: 90,
|
|
1125
1125
|
applies: (signals) => (signals.shape.docs.length > 0 ? undefined : `no architecture documents`),
|
|
1126
1126
|
});
|
|
1127
1127
|
|
|
1128
|
-
/* THE TWO CHORES THAT ONLY EXIST WHERE THEIR SUBJECT DOES. Both are surveys
|
|
1128
|
+
/* THE TWO CHORES THAT ONLY EXIST WHERE THEIR SUBJECT DOES. Both are surveys, nothing here can measure whether a
|
|
1129
1129
|
* pipeline caches well or an image is bigger than it needs to be without running them, and running someone's CI
|
|
1130
|
-
* to find out would be a strange thing for a maintenance panel to do
|
|
1130
|
+
* to find out would be a strange thing for a maintenance panel to do, so both are gated on the artefact itself.
|
|
1131
1131
|
* Together they are the argument for `applies` being first-class rather than folded into `assess`: neither has
|
|
1132
1132
|
* any evidence to be absent, and in a repository with no pipeline and no image both would otherwise sit in the
|
|
1133
1133
|
* list forever, permanently due, describing work that cannot be done. */
|
|
@@ -1141,7 +1141,7 @@ const pipelines = survey({
|
|
|
1141
1141
|
`Read this repository's pipeline definitions and report what it pays for repeatedly: dependency installs with no cache key, ` +
|
|
1142
1142
|
`build outputs recomputed between jobs, steps that are serial for no reason, and matrix legs that duplicate each other's work. ` +
|
|
1143
1143
|
`For each, name the file and step, say roughly what it costs per run, and give the change that would fix it. Where a step is slow ` +
|
|
1144
|
-
`because it genuinely has to be, say so
|
|
1144
|
+
`because it genuinely has to be, say so: a pipeline that is honestly expensive is not a finding.`,
|
|
1145
1145
|
done: `Done when every finding names a file, a step, and a concrete change, and anything deliberately slow is called out as such.`,
|
|
1146
1146
|
cadenceDays: 90,
|
|
1147
1147
|
applies: (signals) => (signals.shape.ci.length > 0 ? undefined : `no CI pipeline`),
|
|
@@ -1151,13 +1151,13 @@ const images = survey({
|
|
|
1151
1151
|
id: `docker-image`,
|
|
1152
1152
|
title: `Slim the container image`,
|
|
1153
1153
|
icon: `box`,
|
|
1154
|
-
description: `Layer order, build context and final size
|
|
1154
|
+
description: `Layer order, build context and final size, what ships in the image that did not need to.`,
|
|
1155
1155
|
diagnosis: `Image size is paid on every pull and every cold start, and layer order decides how much of a build is cache hits.`,
|
|
1156
1156
|
goal:
|
|
1157
1157
|
`Read this repository's Dockerfiles and report what makes the image larger or the build slower than it needs to be: layers ordered ` +
|
|
1158
1158
|
`so that a source edit invalidates the dependency install, build-time toolchains left in the final stage, a build context that ships ` +
|
|
1159
1159
|
`the whole repository, and package caches never cleaned. For each, cite the file and line, and name the change. Do not rewrite the ` +
|
|
1160
|
-
`Dockerfiles
|
|
1160
|
+
`Dockerfiles: an image that fails to build is a much worse problem than one that is larger than ideal.`,
|
|
1161
1161
|
done: `Done when every finding cites a Dockerfile line and names the change, with the ones that would need a base-image swap called out separately.`,
|
|
1162
1162
|
cadenceDays: 90,
|
|
1163
1163
|
applies: (signals) => (signals.shape.dockerfiles.length > 0 ? undefined : `no Dockerfile`),
|
|
@@ -1167,11 +1167,11 @@ const images = survey({
|
|
|
1167
1167
|
* order these were written in. It narrows from "this is a risk you are carrying right now" to "this is worth
|
|
1168
1168
|
* thinking about this quarter".
|
|
1169
1169
|
*
|
|
1170
|
-
* This used to be a comment above a hand-sorted array
|
|
1170
|
+
* This used to be a comment above a hand-sorted array, the four kinds named in prose, the order maintained by
|
|
1171
1171
|
* whoever added the last chore, and nothing anywhere that could check the two agreed. It was also thrown away at
|
|
1172
1172
|
* render: the panel listed every chore in one flat column, so the single editorial claim this surface makes
|
|
1173
1173
|
* ("a live advisory and a quarterly re-read are not the same kind of thing") was invisible and therefore
|
|
1174
|
-
* unarguable
|
|
1174
|
+
* unarguable, on a page whose whole design is that every claim shows its working.
|
|
1175
1175
|
*
|
|
1176
1176
|
* So the kinds are data. They order the book here, they group the rows in the panel, and `caption` is the
|
|
1177
1177
|
* sentence the panel puts beside each group so the grouping argues for itself.
|
|
@@ -1186,18 +1186,18 @@ export interface ChoreKindSpec {
|
|
|
1186
1186
|
readonly kind: ChoreKind;
|
|
1187
1187
|
// Title case, because the panel renders it as a group heading rather than as a sentence.
|
|
1188
1188
|
readonly label: string;
|
|
1189
|
-
// Why these belong together, in the reader's terms
|
|
1189
|
+
// Why these belong together, in the reader's terms, what the group is CLAIMING about the rows under it.
|
|
1190
1190
|
readonly caption: string;
|
|
1191
1191
|
}
|
|
1192
1192
|
|
|
1193
1193
|
export const CHORE_KINDS: readonly ChoreKindSpec[] = [
|
|
1194
|
-
{ kind: `carrying`, label: `Carrying`, caption: `a risk this repository is running today
|
|
1194
|
+
{ kind: `carrying`, label: `Carrying`, caption: `a risk this repository is running today, someone else decides when it becomes urgent` },
|
|
1195
1195
|
{ kind: `accruing`, label: `Accruing`, caption: `cheap now, expensive later, and always getting later` },
|
|
1196
1196
|
{ kind: `drifting`, label: `Drifting`, caption: `the shape of the thing is diverging from the idea of it` },
|
|
1197
|
-
{ kind: `surveying`, label: `Surveying`, caption: `periodic reads with nothing measuring them
|
|
1197
|
+
{ kind: `surveying`, label: `Surveying`, caption: `periodic reads with nothing measuring them, due because it has been that long` },
|
|
1198
1198
|
];
|
|
1199
1199
|
|
|
1200
|
-
// Declaration order, which decides nothing but the order WITHIN a kind
|
|
1200
|
+
// Declaration order, which decides nothing but the order WITHIN a kind, the sort below is stable, so the two
|
|
1201
1201
|
// facts stay separable: this list is where a chore is written down, CHORE_KINDS is where it is ranked.
|
|
1202
1202
|
const BOOK: readonly Chore[] = [
|
|
1203
1203
|
security,
|
|
@@ -1222,7 +1222,7 @@ const BOOK: readonly Chore[] = [
|
|
|
1222
1222
|
const KIND_ORDER: readonly ChoreKind[] = CHORE_KINDS.map(({ kind }) => kind);
|
|
1223
1223
|
|
|
1224
1224
|
// Sorted rather than filtered into groups, so no chore can ever be dropped out of the book by a kind the list
|
|
1225
|
-
// above forgot
|
|
1225
|
+
// above forgot, a missing kind sorts to the front, where it is visible, instead of vanishing.
|
|
1226
1226
|
export const CHORES: readonly Chore[] = BOOK.toSorted((left, right) => KIND_ORDER.indexOf(left.kind) - KIND_ORDER.indexOf(right.kind));
|
|
1227
1227
|
|
|
1228
1228
|
export const choreById = (id: string): Chore | undefined => CHORES.find((chore) => chore.id === id);
|
|
@@ -1230,7 +1230,7 @@ export const choreById = (id: string): Chore | undefined => CHORES.find((chore)
|
|
|
1230
1230
|
// The prompt for one chore against one finding. Built here rather than in the view because the panel, the badge's
|
|
1231
1231
|
// tooltip and the automation that runs unattended must all be describing the same turn.
|
|
1232
1232
|
/* THE SCHEDULED TURN, for a chore woken by its automation rather than started from the panel. Same four parts and
|
|
1233
|
-
* the same invariants
|
|
1233
|
+
* the same invariants, a chore asks for the same work whoever started it, with the guard's own report standing
|
|
1234
1234
|
* in for the finding, because at 3am there is no verdict to quote and no reader to have checked it first.
|
|
1235
1235
|
*
|
|
1236
1236
|
* Workspace-wide rather than per repository: an automation's guard runs at the workspace root on the sandbox's
|
|
@@ -1251,7 +1251,7 @@ export const chorePrompt = (chore: Chore, finding: ChoreFinding, repo: string):
|
|
|
1251
1251
|
composeAsk({
|
|
1252
1252
|
subject: `${chore.title} in ${repoLabel(repo)}.`,
|
|
1253
1253
|
// The RULE before the numbers. An agent told only "4 majors waiting" has to infer why anyone cares; told
|
|
1254
|
-
// the criterion it was woken by, it can also tell us the criterion was wrong
|
|
1254
|
+
// the criterion it was woken by, it can also tell us the criterion was wrong, which is the single most
|
|
1255
1255
|
// useful thing a chore turn can report back, and the only way the book gets better.
|
|
1256
1256
|
why: `${finding.why} You were woken because: ${chore.criterion} ${TRIAGE_NOTE}`,
|
|
1257
1257
|
diagnosis: chore.diagnosis,
|