@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.
Files changed (165) hide show
  1. package/README.md +14 -14
  2. package/dist/agent-catalog.js +3 -3
  3. package/dist/agent-catalog.js.map +1 -1
  4. package/dist/chores/chores.js +43 -43
  5. package/dist/chores/chores.js.map +1 -1
  6. package/dist/chores/extension-update.js +2 -2
  7. package/dist/chores/extension-update.js.map +1 -1
  8. package/dist/chores/fix-deps.js +1 -1
  9. package/dist/chores/fix-deps.js.map +1 -1
  10. package/dist/chores/probes.js +1 -1
  11. package/dist/chores/probes.js.map +1 -1
  12. package/dist/chores/prompt.d.ts.map +1 -1
  13. package/dist/chores/prompt.js +3 -3
  14. package/dist/chores/prompt.js.map +1 -1
  15. package/dist/chores/verdict.js +2 -2
  16. package/dist/chores/verdict.js.map +1 -1
  17. package/dist/contracts/capabilities.contract.d.ts +41 -0
  18. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  19. package/dist/contracts/exit.contract.d.ts +80 -0
  20. package/dist/contracts/exit.contract.d.ts.map +1 -0
  21. package/dist/contracts/exit.contract.js +13 -0
  22. package/dist/contracts/exit.contract.js.map +1 -0
  23. package/dist/contracts/ports.contract.d.ts +11 -0
  24. package/dist/contracts/ports.contract.d.ts.map +1 -1
  25. package/dist/contracts/settings.contract.d.ts +12 -0
  26. package/dist/contracts/settings.contract.d.ts.map +1 -1
  27. package/dist/events.d.ts +4 -4
  28. package/dist/events.d.ts.map +1 -1
  29. package/dist/events.js +5 -5
  30. package/dist/events.js.map +1 -1
  31. package/dist/fast-tier.d.ts +9 -0
  32. package/dist/fast-tier.d.ts.map +1 -0
  33. package/dist/fast-tier.js +19 -0
  34. package/dist/fast-tier.js.map +1 -0
  35. package/dist/history-state.js +3 -3
  36. package/dist/history-state.js.map +1 -1
  37. package/dist/index.d.ts +214 -68
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/model-order.d.ts +1 -0
  42. package/dist/model-order.d.ts.map +1 -1
  43. package/dist/model-order.js +5 -0
  44. package/dist/model-order.js.map +1 -1
  45. package/dist/output-fields.d.ts.map +1 -1
  46. package/dist/output-fields.js +2 -2
  47. package/dist/output-fields.js.map +1 -1
  48. package/dist/prompt-complexity.d.ts +19 -0
  49. package/dist/prompt-complexity.d.ts.map +1 -0
  50. package/dist/prompt-complexity.js +83 -0
  51. package/dist/prompt-complexity.js.map +1 -0
  52. package/dist/publish-drafts.js +2 -2
  53. package/dist/publish-drafts.js.map +1 -1
  54. package/dist/schemas.d.ts +236 -0
  55. package/dist/schemas.d.ts.map +1 -1
  56. package/dist/schemas.js +116 -2
  57. package/dist/schemas.js.map +1 -1
  58. package/dist/workflow-faults.js +3 -3
  59. package/dist/workflow-faults.js.map +1 -1
  60. package/dist/workspace-state.d.ts +20 -20
  61. package/dist/workspace-state.d.ts.map +1 -1
  62. package/dist/workspace-state.js +20 -20
  63. package/dist/workspace-state.js.map +1 -1
  64. package/package.json +5 -5
  65. package/src/agent-catalog.test.ts +25 -25
  66. package/src/agent-catalog.ts +81 -81
  67. package/src/agent-run-model.test.ts +3 -3
  68. package/src/agent-run-model.ts +8 -8
  69. package/src/capability-env.ts +3 -3
  70. package/src/capability-ledger.test.ts +13 -13
  71. package/src/capability-secrets.ts +5 -5
  72. package/src/chores/chores.test.ts +1 -1
  73. package/src/chores/chores.ts +109 -109
  74. package/src/chores/digest.test.ts +1 -1
  75. package/src/chores/digest.ts +3 -3
  76. package/src/chores/extension-update.ts +5 -5
  77. package/src/chores/fix-deps.ts +4 -4
  78. package/src/chores/probes.test.ts +6 -6
  79. package/src/chores/probes.ts +17 -17
  80. package/src/chores/prompt.ts +9 -9
  81. package/src/chores/stack.test.ts +3 -3
  82. package/src/chores/stack.ts +17 -17
  83. package/src/chores/verdict.test.ts +20 -20
  84. package/src/chores/verdict.ts +25 -25
  85. package/src/contract-lock.test.ts +1 -1
  86. package/src/contract-lock.ts +7 -7
  87. package/src/contracts/activity.contract.ts +1 -1
  88. package/src/contracts/agent.contract.ts +2 -2
  89. package/src/contracts/agents.contract.ts +11 -11
  90. package/src/contracts/automations.contract.ts +5 -5
  91. package/src/contracts/capabilities.contract.ts +7 -7
  92. package/src/contracts/chores.contract.ts +3 -3
  93. package/src/contracts/ci.contract.ts +2 -2
  94. package/src/contracts/claude.contract.ts +3 -3
  95. package/src/contracts/drafts.contract.ts +1 -1
  96. package/src/contracts/endpoints.contract.ts +2 -2
  97. package/src/contracts/exit.contract.ts +42 -0
  98. package/src/contracts/extensions.contract.ts +8 -8
  99. package/src/contracts/git.contract.ts +6 -6
  100. package/src/contracts/grok.contract.ts +4 -4
  101. package/src/contracts/host.contract.ts +5 -5
  102. package/src/contracts/intentic.contract.ts +2 -2
  103. package/src/contracts/logs.contract.ts +1 -1
  104. package/src/contracts/loops.contract.ts +8 -8
  105. package/src/contracts/personas.contract.ts +8 -8
  106. package/src/contracts/ports.contract.ts +1 -1
  107. package/src/contracts/prepush.contract.ts +2 -2
  108. package/src/contracts/providers.contract.ts +4 -4
  109. package/src/contracts/public.contract.ts +1 -1
  110. package/src/contracts/push.contract.ts +2 -2
  111. package/src/contracts/secrets.contract.ts +2 -2
  112. package/src/contracts/sessions.contract.ts +2 -2
  113. package/src/contracts/settings.contract.ts +3 -3
  114. package/src/contracts/share.contract.ts +1 -1
  115. package/src/contracts/skills.contract.ts +2 -2
  116. package/src/contracts/system.contract.ts +9 -9
  117. package/src/contracts/usage.contract.ts +1 -1
  118. package/src/contracts/vpn.contract.ts +5 -5
  119. package/src/contracts/workflows.contract.ts +12 -12
  120. package/src/contracts/workspace.contract.ts +13 -13
  121. package/src/conversation-ids.ts +8 -8
  122. package/src/events.test.ts +3 -3
  123. package/src/events.ts +142 -142
  124. package/src/fast-tier.test.ts +88 -0
  125. package/src/fast-tier.ts +72 -0
  126. package/src/history-state.ts +15 -15
  127. package/src/host-protocol.ts +7 -7
  128. package/src/hostnames.test.ts +1 -1
  129. package/src/hostnames.ts +15 -15
  130. package/src/index.ts +13 -8
  131. package/src/listener-protocol.ts +12 -12
  132. package/src/model-order.test.ts +11 -11
  133. package/src/model-order.ts +52 -30
  134. package/src/output-fields.ts +11 -11
  135. package/src/path-refs.test.ts +4 -4
  136. package/src/path-refs.ts +6 -6
  137. package/src/prompt-complexity.test.ts +160 -0
  138. package/src/prompt-complexity.ts +271 -0
  139. package/src/publish-drafts.ts +8 -8
  140. package/src/quick-model.test.ts +11 -11
  141. package/src/quick-model.ts +15 -15
  142. package/src/routes.test.ts +11 -5
  143. package/src/routes.ts +19 -19
  144. package/src/runtime-state.test.ts +1 -1
  145. package/src/runtime-state.ts +22 -22
  146. package/src/schemas.test.ts +8 -8
  147. package/src/schemas.ts +1238 -921
  148. package/src/search-globs.test.ts +2 -2
  149. package/src/search-globs.ts +6 -6
  150. package/src/session-names.ts +5 -5
  151. package/src/share-paths.test.ts +1 -1
  152. package/src/share-paths.ts +7 -7
  153. package/src/sse.ts +1 -1
  154. package/src/state-portability.ts +8 -8
  155. package/src/terminal-protocol.ts +3 -3
  156. package/src/title.test.ts +9 -9
  157. package/src/title.ts +21 -21
  158. package/src/tunnel-ids.test.ts +3 -3
  159. package/src/tunnel-ids.ts +7 -7
  160. package/src/versions.test.ts +3 -3
  161. package/src/versions.ts +6 -6
  162. package/src/workflow-faults.test.ts +6 -6
  163. package/src/workflow-faults.ts +14 -14
  164. package/src/workspace-state.test.ts +32 -32
  165. package/src/workspace-state.ts +139 -139
@@ -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 what routine maintenance a repository is owed, and what has to be TRUE before we say so.
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 any linter will hand you a thousand findings it is deciding which of them is worth
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 which packages, which
18
- * advisories, which files and the rail speaks when that set changes, not while it is merely non-empty. The
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 with one deliberate exception (duplication's 5%, which
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 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
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" see CHORE_KINDS at the foot of this file,
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 what the panel lists under the row, and what makes the headline
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 a live advisory, a runtime past its EOL. Everything
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 the agent may recount them.
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 what has to be true for this chore to be due, stated so a reader can check it against
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 the tests below
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 returns undefined when it is, and what
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 it is one that will never apply here, and
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 "no Dockerfile", never "this repository ships no Dockerfile, so there is no image to slim".
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 which chores it costs is the list
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 it selects the invariants block, and a
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 what the Automations page offers as a
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 there is no verdict at fire time and instead it carries a GUARD: a shell one-liner that
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 the "Why:" line, in place of a finding.
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 never a design and `done` is falsifiable by the agent itself.
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 and deliberately the SAME paths the
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" so it is spelled out here, once, rather than at every call site that builds a prompt.
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 never run, unavailable, failed reads as absent, so
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 a moderate advisory in a build-time-only tool badging red is how `warning` stops
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} ${advisory.title}${advisory.patched === undefined ? ` (no patch yet)` : ``}`,
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 badly, because pnpm audit is slow and it
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)} ${production.length} reaching a production dependency path, ${patchable.length} with a published patched range: ` +
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 a transitive dependency of a ` +
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 so the digest is
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 the minors and patches are a
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 those are what the lockfile can absorb without argument. Then take the majors ` +
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 this measurement is hours old and the tree has moved. Then check each finding against how the ` +
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 extracting is a design call.`,
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 and only some of it does.`,
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 generated files, deliberately repetitive tests, and lookalikes owned by different subsystems so the ` +
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 which IS its architecture document in this
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 new ones first.`,
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 read them first, they are not optional and they are not generic. For ` +
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 there is ALWAYS a top of a hotspot ranking, and
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 load-bearing a hotspot that is also a key module: every edit ripples outward.
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 where edits are slow and ripple outward.`,
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} ${hotspot.commits} commits, ${reason(hotspot.path, hotspot.complexity)}`),
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 the worst of them and no more. Read it first. If the rest of the repository imports it, separate the stable ` +
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 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.`,
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 but it does mean this table is
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 telling someone the day security patches stop is telling them too late to do anything but scramble.
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 which of those pins have to change, in what order, and what is likely to ` +
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 not, say, two test runners, which is an ordinary migration. */
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 both shipped, both maintained, one picked at random.`,
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 load-bearing how many call sites each has, whether one is a ` +
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 and split across the reading order in CHORES, since where a row belongs is decided by what KIND of
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 which is what a
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 you read one component at a time, and the tenth copy of a button looks
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 it is the second exception to the book's leader-relative
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 that IS its entire download.
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 two files cannot tell you anything about how a build is
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 `assets/vendor-DlAUqK2U.js` becomes `assets/vendor.js`.
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 a source map comment, a big JSON blob is not the download this is about.
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 ${Math.round(share)}%. ` +
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 the repository's own bundler can report this, and a ` +
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 so nothing ever forces the rest.
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 and a row
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 the case that
816
- * caught this the book's own rule table quoting its own patterns back at it. What the repository
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 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 ` +
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 `BaseButton.vue` and `ButtonV2.tsx` reduce to the same stem, and no clone
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 the exact "measured and found nothing" lie the
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 the same name in two places, or the same logic under two names.`,
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 whichever copy the next person happens to open is the one that gets the fix.`,
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 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 ` +
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 `bg-[#3b82f6]` is the palette being stepped around, in the markup, in a form no
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 a new file arriving at the top of this list is the event with the
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 a palette change lands everywhere except the files that opted out of it.`,
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 the Tailwind config, or the CSS that defines the tokens so you know what the scale actually offers. Then ` +
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 a brand colour used in nine places say that the theme is ` +
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 these are the reviews a codebase silently rots without, and none of them can be detected
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 "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
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 a survey that genuinely applies everywhere still has to
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 it is due because it has been that long.`,
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 the things every file does slightly differently.`,
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 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 ` +
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 anything its code marks ` +
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 it is the surface admitting it never looked.
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 they are the package pages but a repo with no map has not been
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 the drift no tool can measure.`,
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 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.`,
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 nothing here can measure whether a
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 so both are gated on the artefact itself.
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 a pipeline that is honestly expensive is not a finding.`,
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 what ships in the image that did not need to.`,
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 an image that fails to build is a much worse problem than one that is larger than ideal.`,
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 the four kinds named in prose, the order maintained by
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 on a page whose whole design is that every claim shows its working.
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 what the group is CLAIMING about the rows under it.
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 someone else decides when it becomes urgent` },
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 due because it has been that long` },
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 the sort below is stable, so the two
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 a missing kind sorts to the front, where it is visible, instead of vanishing.
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 a chore asks for the same work whoever started it with the guard's own report standing
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 which is the single most
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,