@massu/core 1.9.3 → 1.10.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/dist/cli.js +561 -283
- package/dist/hooks/auto-learning-pipeline.js +30 -8
- package/dist/hooks/classify-failure.js +8 -1
- package/dist/hooks/cost-tracker.js +8 -1
- package/dist/hooks/fix-detector.js +3 -3
- package/dist/hooks/incident-pipeline.js +8 -1
- package/dist/hooks/post-edit-context.js +8 -1
- package/dist/hooks/post-tool-use.js +98 -1
- package/dist/hooks/pre-compact.js +8 -1
- package/dist/hooks/pre-delete-check.js +8 -1
- package/dist/hooks/quality-event.js +8 -1
- package/dist/hooks/session-end.js +18 -2
- package/dist/hooks/session-start.js +8 -1
- package/dist/hooks/user-prompt.js +8 -1
- package/package.json +2 -2
- package/src/backfill-sessions.ts +3 -2
- package/src/cli.ts +10 -0
- package/src/cloud-sync.ts +18 -0
- package/src/commands/doctor.ts +9 -14
- package/src/commands/hook-runner.ts +145 -0
- package/src/commands/init.ts +239 -29
- package/src/commands/install-commands.ts +10 -0
- package/src/commands/install-hooks.ts +2 -1
- package/src/commands/template-engine.ts +41 -0
- package/src/config.ts +2 -1
- package/src/hooks/auto-learning-pipeline.ts +43 -10
- package/src/hooks/fix-detector.ts +3 -3
- package/src/hooks/post-tool-use.ts +91 -1
- package/src/lib/hook-registry.ts +43 -0
- package/src/lib/memory-path.ts +49 -0
- package/src/security/registry-pubkey.generated.ts +1 -1
- package/src/tool-db-needs.ts +8 -2
- package/src/tools.ts +23 -7
|
@@ -16,12 +16,18 @@
|
|
|
16
16
|
// the pipeline steps.
|
|
17
17
|
// ============================================================
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import { execFileSync } from 'child_process';
|
|
20
20
|
import { existsSync, readFileSync, unlinkSync, readdirSync, statSync } from 'fs';
|
|
21
21
|
import { tmpdir } from 'os';
|
|
22
22
|
import { join } from 'path';
|
|
23
23
|
import { getProjectRoot, getConfig } from '../config.ts';
|
|
24
24
|
|
|
25
|
+
// P-H002 (plan-stage-c-high-batch): bound git-diff reads so monorepos with
|
|
26
|
+
// 10MB+ working trees don't trigger Stop-hook timeout. Short-stat first,
|
|
27
|
+
// only read full diff body when estimated bytes <= cap. execFileSync argv
|
|
28
|
+
// form is defense-in-depth (P-001 pattern).
|
|
29
|
+
const MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024; // 2MB; ~25k lines at 80 bytes/line
|
|
30
|
+
|
|
25
31
|
interface HookInput {
|
|
26
32
|
session_id: string;
|
|
27
33
|
transcript_path: string;
|
|
@@ -67,19 +73,46 @@ async function main(): Promise<void> {
|
|
|
67
73
|
} catch { /* ignore parse errors */ }
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
// Source 2: Scan uncommitted git diff for fix patterns (language-agnostic)
|
|
76
|
+
// Source 2: Scan uncommitted git diff for fix patterns (language-agnostic).
|
|
77
|
+
// Two-stage: (1) name-only to confirm any changes, (2) shortstat to estimate
|
|
78
|
+
// bytes, (3) full diff body ONLY if estimate <= MAX_FULL_DIFF_BYTES.
|
|
71
79
|
let uncommittedFix = false;
|
|
72
80
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const nameOnly = execFileSync('git', ['diff', '--name-only'], {
|
|
82
|
+
cwd: root,
|
|
83
|
+
timeout: 3000,
|
|
84
|
+
encoding: 'utf-8',
|
|
85
|
+
maxBuffer: 1024 * 1024,
|
|
86
|
+
});
|
|
87
|
+
if (nameOnly.trim()) {
|
|
88
|
+
const shortstat = execFileSync('git', ['diff', '--shortstat'], {
|
|
89
|
+
cwd: root,
|
|
90
|
+
timeout: 2000,
|
|
91
|
+
encoding: 'utf-8',
|
|
92
|
+
maxBuffer: 64 * 1024,
|
|
93
|
+
});
|
|
94
|
+
const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? '0', 10);
|
|
95
|
+
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? '0', 10);
|
|
96
|
+
const estimatedBytes = (insertions + deletions) * 80; // ~80 bytes/line avg
|
|
97
|
+
if (estimatedBytes <= MAX_FULL_DIFF_BYTES) {
|
|
98
|
+
const fullDiff = execFileSync('git', ['diff'], {
|
|
99
|
+
cwd: root,
|
|
100
|
+
timeout: 5000,
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
maxBuffer: MAX_FULL_DIFF_BYTES,
|
|
103
|
+
});
|
|
104
|
+
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
|
|
105
|
+
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
|
|
106
|
+
if (fixPatterns > 3 || removedBroken > 1) {
|
|
107
|
+
uncommittedFix = true;
|
|
108
|
+
}
|
|
80
109
|
}
|
|
110
|
+
// else: diff exceeds cap — skip pattern scan rather than risk timeout.
|
|
111
|
+
// The fix-detector hook fires on per-Edit/Write and already populates
|
|
112
|
+
// sessionFixes for the realistic case; full-diff fallback is a safety
|
|
113
|
+
// net that we can correctly skip for huge trees.
|
|
81
114
|
}
|
|
82
|
-
} catch { /* git not available or no changes */ }
|
|
115
|
+
} catch { /* git not available or no changes or buffer overflow */ }
|
|
83
116
|
|
|
84
117
|
if (sessionFixes.length === 0 && !uncommittedFix) {
|
|
85
118
|
// Clean up flag file
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Must complete in <1000ms.
|
|
15
15
|
// ============================================================
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { execFileSync } from 'child_process';
|
|
18
18
|
import { existsSync, appendFileSync, mkdirSync, readFileSync } from 'fs';
|
|
19
19
|
import { tmpdir } from 'os';
|
|
20
20
|
import { join } from 'path';
|
|
@@ -119,9 +119,9 @@ async function main(): Promise<void> {
|
|
|
119
119
|
const root = getProjectRoot();
|
|
120
120
|
let diff = '';
|
|
121
121
|
try {
|
|
122
|
-
diff =
|
|
122
|
+
diff = execFileSync('git', ['diff', '--', filePath], { cwd: root, timeout: 3000, encoding: 'utf-8' });
|
|
123
123
|
if (!diff) {
|
|
124
|
-
diff =
|
|
124
|
+
diff = execFileSync('git', ['diff', 'HEAD', '--', filePath], { cwd: root, timeout: 3000, encoding: 'utf-8' });
|
|
125
125
|
}
|
|
126
126
|
} catch {
|
|
127
127
|
process.exit(0);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { getMemoryDb, addObservation, createSession, deduplicateFailedAttempt, addSummary } from '../memory-db.ts';
|
|
12
12
|
import { classifyRealTimeToolCall, detectPlanProgress } from '../observation-extractor.ts';
|
|
13
13
|
import { logAuditEntry } from '../audit-trail.ts';
|
|
14
|
-
import { trackModification } from '../regression-detector.ts';
|
|
14
|
+
import { trackModification, recordTestResult } from '../regression-detector.ts';
|
|
15
15
|
import { validateFile, storeValidationResult } from '../validation-engine.ts';
|
|
16
16
|
import { scoreFileSecurity, storeSecurityScore } from '../security-scorer.ts';
|
|
17
17
|
import { readFileSync, existsSync } from 'fs';
|
|
@@ -131,6 +131,42 @@ async function main(): Promise<void> {
|
|
|
131
131
|
// Best-effort: never block post-tool-use
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// P-H029 (plan-stage-c-high-batch): wire recordTestResult() into the
|
|
135
|
+
// post-tool-use hook so `feature_health` dashboard reflects real test
|
|
136
|
+
// deltas. Pre-fix: `trackModification` fired but `recordTestResult` was
|
|
137
|
+
// unit-tested yet never called; dashboard showed tests_passing=0 for
|
|
138
|
+
// every feature.
|
|
139
|
+
//
|
|
140
|
+
// Strategy: when a Bash tool call runs a test runner AND the output
|
|
141
|
+
// includes parseable pass/fail counts, call recordTestResult for every
|
|
142
|
+
// feature with `modifications_since_test > 0`. This resets their counter
|
|
143
|
+
// and updates pass/fail tallies based on the run.
|
|
144
|
+
try {
|
|
145
|
+
if (tool_name === 'Bash') {
|
|
146
|
+
const command = (tool_input.command as string) ?? '';
|
|
147
|
+
if (isTestRunnerCommand(command)) {
|
|
148
|
+
const counts = parseTestRunOutput(tool_response ?? '');
|
|
149
|
+
if (counts) {
|
|
150
|
+
const modifiedFeatures = db
|
|
151
|
+
.prepare(
|
|
152
|
+
'SELECT feature_key FROM feature_health WHERE modifications_since_test > 0',
|
|
153
|
+
)
|
|
154
|
+
.all() as Array<{ feature_key: string }>;
|
|
155
|
+
for (const row of modifiedFeatures) {
|
|
156
|
+
recordTestResult(db, row.feature_key, counts.passing, counts.failing);
|
|
157
|
+
}
|
|
158
|
+
// Also record a session-level aggregate so the dashboard has at
|
|
159
|
+
// least one row even when no features were modified.
|
|
160
|
+
if (modifiedFeatures.length === 0) {
|
|
161
|
+
recordTestResult(db, '_session_test_run', counts.passing, counts.failing);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (_testResultErr) {
|
|
167
|
+
// Best-effort: never block post-tool-use
|
|
168
|
+
}
|
|
169
|
+
|
|
134
170
|
// MEMORY.md integrity check on write
|
|
135
171
|
try {
|
|
136
172
|
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
@@ -217,6 +253,60 @@ function updatePlanProgress(db: import('better-sqlite3').Database, sessionId: st
|
|
|
217
253
|
}
|
|
218
254
|
}
|
|
219
255
|
|
|
256
|
+
/**
|
|
257
|
+
* P-H029: Detect test-runner commands. Conservative match — only commands that
|
|
258
|
+
* START with a recognized test runner so a build script invoking `npm run test`
|
|
259
|
+
* is included but a script that merely mentions "test" in a filename is not.
|
|
260
|
+
*/
|
|
261
|
+
function isTestRunnerCommand(command: string): boolean {
|
|
262
|
+
const trimmed = command.trim().toLowerCase();
|
|
263
|
+
// Strip leading `cd <dir> && ` or `(cd <dir> && ...)` prefix so we match
|
|
264
|
+
// the actual test command.
|
|
265
|
+
const stripped = trimmed
|
|
266
|
+
.replace(/^cd\s+\S+\s*(&&|;)\s*/, '')
|
|
267
|
+
.replace(/^\(\s*cd\s+\S+\s*(&&|;)\s*/, '');
|
|
268
|
+
const testRunnerPrefixes = [
|
|
269
|
+
'npm test', 'npm run test', 'npx vitest', 'npx jest', 'vitest', 'jest',
|
|
270
|
+
'pnpm test', 'pnpm run test', 'yarn test', 'pytest', 'go test', 'cargo test',
|
|
271
|
+
];
|
|
272
|
+
return testRunnerPrefixes.some((prefix) => stripped.startsWith(prefix));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* P-H029: Parse test-run output for pass/fail counts. Supports vitest
|
|
277
|
+
* (`Tests N passed (N)`, `Tests X failed | Y passed (Z)`), jest
|
|
278
|
+
* (`Tests: X failed, Y passed, Z total`), and pytest (`X passed, Y failed`).
|
|
279
|
+
* Returns null if no parseable summary line found.
|
|
280
|
+
*/
|
|
281
|
+
function parseTestRunOutput(output: string): { passing: number; failing: number } | null {
|
|
282
|
+
// vitest: " Tests 439 passed (439)" or " Tests 3 failed | 436 passed (439)"
|
|
283
|
+
const vitestSplit = output.match(/Tests?\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed/);
|
|
284
|
+
if (vitestSplit) {
|
|
285
|
+
return {
|
|
286
|
+
passing: parseInt(vitestSplit[2], 10),
|
|
287
|
+
failing: vitestSplit[1] ? parseInt(vitestSplit[1], 10) : 0,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// jest: "Tests: 1 failed, 5 passed, 6 total"
|
|
291
|
+
const jest = output.match(/Tests?:\s+(?:(\d+)\s+failed,\s+)?(\d+)\s+passed/);
|
|
292
|
+
if (jest) {
|
|
293
|
+
return {
|
|
294
|
+
passing: parseInt(jest[2], 10),
|
|
295
|
+
failing: jest[1] ? parseInt(jest[1], 10) : 0,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// pytest: "5 passed, 2 failed in 1.23s" or "5 passed in 1.23s"
|
|
299
|
+
const pytestPassed = output.match(/(\d+)\s+passed/);
|
|
300
|
+
const pytestFailed = output.match(/(\d+)\s+failed/);
|
|
301
|
+
if (pytestPassed) {
|
|
302
|
+
return {
|
|
303
|
+
passing: parseInt(pytestPassed[1], 10),
|
|
304
|
+
failing: pytestFailed ? parseInt(pytestFailed[1], 10) : 0,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
220
310
|
function readStdin(): Promise<string> {
|
|
221
311
|
return new Promise((resolve) => {
|
|
222
312
|
let data = '';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the canonical Massu hook set.
|
|
3
|
+
*
|
|
4
|
+
* P-H001 (plan-stage-c-high-batch): doctor / installer / build:hooks all
|
|
5
|
+
* consume from here. The drift-guard `hook-registry-parity.test.ts` asserts:
|
|
6
|
+
*
|
|
7
|
+
* 1. REGISTERED_HOOKS matches `src/hooks/*.ts` filenames (the build SoT).
|
|
8
|
+
* 2. REGISTERED_HOOKS matches the hook names referenced in
|
|
9
|
+
* `buildHooksConfig()` (the installer SoT).
|
|
10
|
+
* 3. REGISTERED_HOOKS matches `dist/hooks/*.js` after `npm run build:hooks`
|
|
11
|
+
* (the runtime SoT, when build artifacts are available).
|
|
12
|
+
*
|
|
13
|
+
* Adding a new hook requires touching THREE places: src/hooks/X.ts,
|
|
14
|
+
* REGISTERED_HOOKS, and buildHooksConfig() — the parity tests enforce this.
|
|
15
|
+
*/
|
|
16
|
+
export const REGISTERED_HOOKS = [
|
|
17
|
+
'auto-learning-pipeline',
|
|
18
|
+
'classify-failure',
|
|
19
|
+
'cost-tracker',
|
|
20
|
+
'fix-detector',
|
|
21
|
+
'incident-pipeline',
|
|
22
|
+
'intent-suggester',
|
|
23
|
+
'post-edit-context',
|
|
24
|
+
'post-tool-use',
|
|
25
|
+
'pre-compact',
|
|
26
|
+
'pre-delete-check',
|
|
27
|
+
'quality-event',
|
|
28
|
+
'rule-enforcement-pipeline',
|
|
29
|
+
'security-gate',
|
|
30
|
+
'session-end',
|
|
31
|
+
'session-start',
|
|
32
|
+
'user-prompt',
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export type RegisteredHook = (typeof REGISTERED_HOOKS)[number];
|
|
36
|
+
|
|
37
|
+
export function getExpectedHookFiles(): readonly string[] {
|
|
38
|
+
return REGISTERED_HOOKS.map((name) => `${name}.js`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getRegisteredHookCount(): number {
|
|
42
|
+
return REGISTERED_HOOKS.length;
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Memory directory name encoding — canonical helpers.
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth for translating a project's absolute filesystem
|
|
8
|
+
* root into the directory name used under `~/.claude/projects/<encoded-root>/memory/`.
|
|
9
|
+
*
|
|
10
|
+
* Closes the P-004 install-path drift class: the writer in `commands/init.ts`
|
|
11
|
+
* historically prepended an EXTRA leading `-` while the reader in
|
|
12
|
+
* `config.ts:getResolvedPaths()` and the backfill code path used the
|
|
13
|
+
* canonical single-dash form. Result: 100% of `massu init` runs orphaned
|
|
14
|
+
* `MEMORY.md` in a directory the reader could never find.
|
|
15
|
+
*
|
|
16
|
+
* Both encoding and decoding live here so the round-trip property is testable.
|
|
17
|
+
*
|
|
18
|
+
* Encoding rule:
|
|
19
|
+
* Replace every `/` in the absolute project root with `-`.
|
|
20
|
+
* An absolute path always starts with `/`, so the result always starts with
|
|
21
|
+
* `-` exactly once. NEVER prepend an additional `-`.
|
|
22
|
+
*
|
|
23
|
+
* Decoding rule:
|
|
24
|
+
* Replace every `-` in the directory name with `/`. This is the canonical
|
|
25
|
+
* inverse used by Claude Code's session-state plumbing. Note: project roots
|
|
26
|
+
* that contain literal `-` characters cannot be unambiguously round-tripped
|
|
27
|
+
* through this encoding — the same trade-off Claude Code's own resolver makes.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Encode an absolute project root into the directory name used under
|
|
32
|
+
* `~/.claude/projects/<dir>/memory/`.
|
|
33
|
+
*
|
|
34
|
+
* @param projectRoot Absolute filesystem path (must start with `/`).
|
|
35
|
+
* @returns Canonical encoded directory name (always begins with `-`).
|
|
36
|
+
*/
|
|
37
|
+
export function encodeMemoryDirName(projectRoot: string): string {
|
|
38
|
+
return projectRoot.replace(/\//g, '-');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Decode a memory-directory name back to its slash-separated form.
|
|
43
|
+
*
|
|
44
|
+
* @param dirname The directory name as it appears under `~/.claude/projects/`.
|
|
45
|
+
* @returns A slash-separated path (begins with `/`).
|
|
46
|
+
*/
|
|
47
|
+
export function decodeMemoryDirName(dirname: string): string {
|
|
48
|
+
return dirname.replace(/-/g, '/');
|
|
49
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T04:53:58.787Z.
|
|
2
2
|
// Source pem: packages/core/security/registry-pubkey.pem
|
|
3
3
|
// RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
|
|
4
4
|
// DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
|
package/src/tool-db-needs.ts
CHANGED
|
@@ -70,8 +70,14 @@ export const TOOL_DB_NEEDS = {
|
|
|
70
70
|
impact: ['codegraph', 'data'],
|
|
71
71
|
domains: ['codegraph', 'data'],
|
|
72
72
|
|
|
73
|
-
// `trpc_map`
|
|
74
|
-
|
|
73
|
+
// P-H009 (plan-stage-c-high-batch): `trpc_map` queries Data DB tables
|
|
74
|
+
// populated by `ensureIndexes(d, codegraphDb)` — without CodeGraph the
|
|
75
|
+
// index never builds and the flagship code-intel tool silently returns
|
|
76
|
+
// "0 procedures" on fresh installs. Declaring `codegraph` here makes the
|
|
77
|
+
// dispatcher open the CodeGraph DB so `buildTrpcIndex` can run; the
|
|
78
|
+
// handler also surfaces an actionable hint when no procedures + no
|
|
79
|
+
// codegraph (covered by `trpc-map-empty-codegraph-hint.test.ts`).
|
|
80
|
+
trpc_map: ['codegraph', 'data'],
|
|
75
81
|
|
|
76
82
|
// `schema` reads filesystem (Prisma schema files); no DB access at all.
|
|
77
83
|
schema: [],
|
package/src/tools.ts
CHANGED
|
@@ -858,13 +858,29 @@ function handleTrpcMap(args: Record<string, unknown>, dataDb: Database.Database)
|
|
|
858
858
|
const coupled = dataDb.prepare('SELECT COUNT(*) as count FROM massu_trpc_procedures WHERE has_ui_caller = 1').get() as { count: number };
|
|
859
859
|
const uncoupled = total.count - coupled.count;
|
|
860
860
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
861
|
+
// P-H009 (plan-stage-c-high-batch): when the index is empty, return an
|
|
862
|
+
// actionable remedy hint instead of bare "0" — previously fresh installs
|
|
863
|
+
// saw "Total procedures: 0" and assumed the flagship tool was broken.
|
|
864
|
+
if (total.count === 0) {
|
|
865
|
+
lines.push('## tRPC Index Empty');
|
|
866
|
+
lines.push('');
|
|
867
|
+
lines.push('No tRPC procedures indexed yet. Either this repo has no tRPC');
|
|
868
|
+
lines.push('routers, or the CodeGraph index has not been built. To rebuild:');
|
|
869
|
+
lines.push('');
|
|
870
|
+
lines.push(' npx massu sync');
|
|
871
|
+
lines.push('');
|
|
872
|
+
lines.push('If `massu sync` completes successfully but `trpc_map` still');
|
|
873
|
+
lines.push('returns 0, the repo likely has no tRPC procedures (this is');
|
|
874
|
+
lines.push('expected for non-tRPC stacks — try `schema` or `domains` tools).');
|
|
875
|
+
} else {
|
|
876
|
+
lines.push('## tRPC Procedure Summary');
|
|
877
|
+
lines.push(`- Total procedures: ${total.count}`);
|
|
878
|
+
lines.push(`- With UI callers: ${coupled.count}`);
|
|
879
|
+
lines.push(`- Without UI callers: ${uncoupled}`);
|
|
880
|
+
lines.push('');
|
|
881
|
+
lines.push('Use { router: "name" } to see details for a specific router.');
|
|
882
|
+
lines.push('Use { uncoupled: true } to see all procedures without UI callers.');
|
|
883
|
+
}
|
|
868
884
|
}
|
|
869
885
|
|
|
870
886
|
return text(lines.join('\n'));
|