@massu/core 1.10.7 → 1.12.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 (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
@@ -1,469 +0,0 @@
1
- /**
2
- * Adapter discovery — Plan 3c Phase 5 5H deliverable.
3
- *
4
- * Scans three source classes per the three-class trust model
5
- * (CORE-BUNDLED + REGISTRY-VERIFIED + LOCAL-EXPLICIT, see security/
6
- * adapter-origin.ts), classifies each candidate, and returns the
7
- * deduplicated AdapterDescriptor[] the CLI + loader consume.
8
- *
9
- * Three sources scanned:
10
- * 1. CORE-BUNDLED: caller-provided id set (typically built from a static
11
- * list of bundled adapter filenames in @massu/core itself). The
12
- * discovery module does NOT enumerate the filesystem for these — that
13
- * enumeration is done at @massu/core build time and shipped as a
14
- * constant. Discovery just classifies the ids.
15
- * 2. REGISTRY-VERIFIED: walk node_modules/@massu/adapter-* directories
16
- * + any node_modules/<pkg>/ where package.json declares
17
- * "massu-adapter": true. Cross-reference each candidate against the
18
- * cached registry manifest's adapters[] list — only entries that
19
- * appear in the manifest are accepted (CR-46 Rule 0 single-source-of-
20
- * truth: the registry manifest IS the authoritative allowlist).
21
- * 3. LOCAL-EXPLICIT: read getConfig().adapters?.local entries. Each entry
22
- * is a POSIX-normalized relative path (already validated +
23
- * normalized at config-parse time per AdapterLocalPathSchema in
24
- * config.ts). Discovery resolves the path relative to the project
25
- * root and confirms the file exists.
26
- *
27
- * The discovery surface returns warnings (not errors) for candidate-
28
- * classification refusals — a malformed package.json or a missing local
29
- * file does NOT abort the whole scan; those candidates are simply not
30
- * loaded, and the warning is surfaced to the CLI for operator awareness.
31
- *
32
- * What this module does NOT do (deferred to follow-up commits, all
33
- * in-flight Phase 5 deliverables):
34
- * - Install-time + load-time sha256 of installed adapter package
35
- * contents (gap-37 install-tracking + tarball verification). This
36
- * requires sha256 of the package's dist/ directory recursively + the
37
- * ~/.massu/adapter-manifest-installed.json sidecar file.
38
- * - adapters.local fingerprint check (gap-32 postinstall-poisoning).
39
- * This requires a sha256 of the canonical adapters.local entry list
40
- * stored in ~/.massu/adapters-local-fingerprint.json.
41
- * - User-installed adapter scan at ~/.massu/adapters/ (CLI install
42
- * path). Populated by `massu adapters install`, also Phase 5
43
- * follow-up.
44
- *
45
- * Loading the actual adapter code (importing the JS module + invoking
46
- * detect/extract) is the loader's job (Plan 3b runner.ts). Discovery
47
- * just enumerates + classifies.
48
- */
49
- import { existsSync, readdirSync, readFileSync, lstatSync } from 'node:fs';
50
- import { resolve, isAbsolute } from 'node:path';
51
- import { z } from 'zod';
52
- import {
53
- getAdapterOrigin,
54
- type AdapterDescriptor,
55
- type AdapterOriginInput,
56
- } from '../../security/adapter-origin.js';
57
- import { PrintableAsciiStringSchema, type Envelope, type AdapterEntry } from '../../security/manifest-schema.js';
58
- import { checkFingerprintDrift, FINGERPRINT_PATH } from '../../security/local-fingerprint.js';
59
- import {
60
- verifyInstalledIntegrity,
61
- containsHiddenDirs,
62
- INSTALLED_MANIFEST_PATH,
63
- } from '../../security/install-tracking.js';
64
-
65
- /**
66
- * Minimal shape of a node_modules package.json that we care about for
67
- * adapter discovery. Strict enough to reject malformed packages at parse
68
- * time, loose enough (passthrough) to ignore unrelated keys.
69
- */
70
- const AdapterPackageJsonSchema = z.object({
71
- // CR-9 iter-4 audit LOW-NEW4-2 fix: name/version are rendered in stderr
72
- // warnings via `${pkg.name}@${pkg.version}` strings; without the
73
- // printable-ASCII regex, a malicious local node_modules/<pkg>/package.json
74
- // could embed ANSI escapes to log-inject. Postinstall scripts have the
75
- // write access needed to mount this attack.
76
- name: PrintableAsciiStringSchema,
77
- version: PrintableAsciiStringSchema,
78
- // Plan 3c gap-31 + gap-50: `"massu-adapter": true` is the explicit opt-in
79
- // marker. @massu/adapter-* packages also declare this.
80
- 'massu-adapter': z.union([z.boolean(), z.literal(undefined)]).optional(),
81
- // Plan 3c gap-31: api version. Loader refuses incompatible major (caller-side).
82
- 'massu-adapter-api-version': z.union([z.string(), z.number(), z.literal(undefined)]).optional(),
83
- }).passthrough();
84
-
85
- export interface DiscoverOptions {
86
- /**
87
- * Absolute path to the project root where node_modules/ lives. Caller
88
- * passes from getProjectRoot() or equivalent.
89
- */
90
- projectRoot: string;
91
- /**
92
- * Set of adapter ids that are CORE-BUNDLED in @massu/core itself.
93
- * Built from a static const at @massu/core build time. Pass an empty
94
- * set in tests when not exercising CORE-BUNDLED classification.
95
- */
96
- coreBundledIds: ReadonlySet<string>;
97
- /**
98
- * The verified registry manifest envelope (from manifest-cache.getManifest).
99
- * Discovery uses envelope.manifest.adapters[] as the REGISTRY-VERIFIED
100
- * allowlist. Pass undefined if running offline + cache absent — discovery
101
- * will then refuse all REGISTRY-VERIFIED candidates with a clear warning.
102
- */
103
- manifestEnvelope: Envelope | undefined;
104
- /**
105
- * POSIX-normalized relative paths from getConfig().adapters?.local.
106
- * Each entry must already pass AdapterLocalPathSchema (config.ts). Pass
107
- * empty array if no local adapters configured.
108
- */
109
- configLocalPaths: ReadonlyArray<string>;
110
- /**
111
- * Plan 3c gap-1 kill switch (CR-9 audit C1 fix). When false, ONLY
112
- * CORE-BUNDLED adapters are emitted; REGISTRY-VERIFIED + LOCAL-EXPLICIT
113
- * scans short-circuit immediately. Source-of-truth is
114
- * `getConfig().adapters?.enabled === true`. Defaults to false at the
115
- * config schema layer so operators MUST opt-in to third-party adapter
116
- * loading — security-critical. Pass the flag through from
117
- * commands/adapters.ts:runAdaptersList; a missing argument here would
118
- * silently default to enabled, defeating the kill switch.
119
- */
120
- adaptersEnabled: boolean;
121
- /**
122
- * Override the on-disk fingerprint sentinel path for testing
123
- * (gap-32 postinstall-poisoning check). Production callsite uses
124
- * the default `~/.massu/adapters-local-fingerprint.json`.
125
- */
126
- fingerprintSentinelPath?: string;
127
- /**
128
- * Override the on-disk install-tracking sidecar path for testing
129
- * (gap-37 install-time + load-time sha256 check). Production callsite
130
- * uses the default `~/.massu/adapter-manifest-installed.json`.
131
- */
132
- installedManifestPath?: string;
133
- /**
134
- * Skip the gap-37 load-time sha256 integrity check. Default: false
135
- * (always check). Setting this to `true` is a test seam ONLY — it
136
- * exists so unit tests that don't materialize real package directories
137
- * can still exercise the classification logic. Production callsites
138
- * MUST NOT pass `true`; per CR-46 this is the most-robust posture.
139
- */
140
- skipInstalledIntegrityCheck?: boolean;
141
- }
142
-
143
- export interface DiscoveryResult {
144
- adapters: AdapterDescriptor[];
145
- warnings: string[];
146
- }
147
-
148
- /**
149
- * Walk node_modules for @massu/adapter-* directories AND any other package
150
- * declaring "massu-adapter": true. Returns the parsed package.json and
151
- * absolute package directory for each candidate. Skips malformed packages
152
- * with a warning.
153
- *
154
- * Walks ONLY one level of node_modules — does NOT descend into nested
155
- * node_modules (transitive deps' adapter packages). Adapters are operator-
156
- * installed top-level dependencies, not transitive.
157
- */
158
- function walkNodeModules(projectRoot: string, warnings: string[]): Array<{
159
- packageDir: string;
160
- pkg: z.infer<typeof AdapterPackageJsonSchema>;
161
- }> {
162
- const nodeModulesDir = resolve(projectRoot, 'node_modules');
163
- if (!existsSync(nodeModulesDir)) {
164
- return [];
165
- }
166
- const candidates: Array<{ packageDir: string; pkg: z.infer<typeof AdapterPackageJsonSchema> }> = [];
167
- let topLevelEntries: string[];
168
- try {
169
- topLevelEntries = readdirSync(nodeModulesDir);
170
- } catch (err) {
171
- warnings.push(`failed to read node_modules: ${err instanceof Error ? err.message : String(err)}`);
172
- return [];
173
- }
174
-
175
- for (const entry of topLevelEntries) {
176
- if (entry.startsWith('.')) continue;
177
- const entryPath = resolve(nodeModulesDir, entry);
178
- let entryStat;
179
- try {
180
- // CR-9 audit HIGH-NEW-1 fix: lstatSync (not statSync). statSync
181
- // followed symlinks, so a malicious package shipping
182
- // `node_modules/evil-link -> /elsewhere` would walk into /elsewhere.
183
- // The same fix was applied in install-tracking.ts (audit H1) but
184
- // discover's node_modules walk was missed at the time.
185
- entryStat = lstatSync(entryPath);
186
- } catch {
187
- continue;
188
- }
189
- if (entryStat.isSymbolicLink()) {
190
- warnings.push(
191
- `skipping ${entryPath}: top-level node_modules entry is a symlink. ` +
192
- `Adapter packages must be real directories (npm-installed); refusing to walk symlinks ` +
193
- `as a defense against postinstall scripts that link to attacker-controlled paths.`,
194
- );
195
- continue;
196
- }
197
- if (!entryStat.isDirectory()) continue;
198
-
199
- if (entry.startsWith('@')) {
200
- // Scoped namespace — e.g., node_modules/@massu/. Walk one more level.
201
- let scopedEntries: string[];
202
- try {
203
- scopedEntries = readdirSync(entryPath);
204
- } catch {
205
- continue;
206
- }
207
- for (const sub of scopedEntries) {
208
- const subPath = resolve(entryPath, sub);
209
- // Same lstatSync discipline at the inner level — a malicious
210
- // postinstall could plant `node_modules/@massu/evil-link -> ...`.
211
- let subStat;
212
- try {
213
- subStat = lstatSync(subPath);
214
- } catch {
215
- continue;
216
- }
217
- if (subStat.isSymbolicLink() || !subStat.isDirectory()) continue;
218
- const result = tryReadAdapterPackage(subPath, warnings);
219
- if (result) candidates.push(result);
220
- }
221
- } else {
222
- const result = tryReadAdapterPackage(entryPath, warnings);
223
- if (result) candidates.push(result);
224
- }
225
- }
226
- return candidates;
227
- }
228
-
229
- function tryReadAdapterPackage(packageDir: string, warnings: string[]): {
230
- packageDir: string;
231
- pkg: z.infer<typeof AdapterPackageJsonSchema>;
232
- } | null {
233
- const pkgJsonPath = resolve(packageDir, 'package.json');
234
- if (!existsSync(pkgJsonPath)) return null;
235
- let raw: unknown;
236
- try {
237
- raw = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
238
- } catch (err) {
239
- warnings.push(
240
- `skipping ${packageDir}: package.json parse failed (${err instanceof Error ? err.message : String(err)})`,
241
- );
242
- return null;
243
- }
244
- const parsed = AdapterPackageJsonSchema.safeParse(raw);
245
- if (!parsed.success) {
246
- warnings.push(`skipping ${packageDir}: package.json shape invalid`);
247
- return null;
248
- }
249
- const pkg = parsed.data;
250
-
251
- // Filter: only consider candidates that EITHER match @massu/adapter-*
252
- // glob OR declare "massu-adapter": true. Other npm packages are ignored
253
- // (vast majority of node_modules entries).
254
- const isMassuAdapterGlob = /^@massu\/adapter-[a-z][a-z0-9-]*$/.test(pkg.name);
255
- const declaresMassuAdapter = pkg['massu-adapter'] === true;
256
- if (!isMassuAdapterGlob && !declaresMassuAdapter) {
257
- return null;
258
- }
259
- return { packageDir, pkg };
260
- }
261
-
262
- /**
263
- * Run discovery across all three trust classes. Returns the combined
264
- * descriptor list + a list of human-readable warnings the CLI prints.
265
- */
266
- export function discoverAdapters(opts: DiscoverOptions): DiscoveryResult {
267
- const warnings: string[] = [];
268
- const adapters: AdapterDescriptor[] = [];
269
- const seenIds = new Set<string>();
270
-
271
- // 1. CORE-BUNDLED — pass-through. Each id in coreBundledIds becomes a
272
- // descriptor with origin='core-bundled'. No verification needed (trust
273
- // derives from @massu/core itself).
274
- for (const id of opts.coreBundledIds) {
275
- const origin = getAdapterOrigin({ id, coreBundledIds: opts.coreBundledIds });
276
- if (origin !== 'core-bundled') {
277
- // This shouldn't happen — coreBundledIds + getAdapterOrigin is a
278
- // closed loop. If it does, it's a programmer bug worth surfacing.
279
- warnings.push(`expected core-bundled classification for id=${id}, got ${origin ?? 'null'}`);
280
- continue;
281
- }
282
- adapters.push({ id, origin: 'core-bundled' });
283
- seenIds.add(id);
284
- }
285
-
286
- // Plan 3c gap-1 kill switch (CR-9 audit C1): when adapters.enabled=false,
287
- // ONLY CORE-BUNDLED adapters load. Skip REGISTRY-VERIFIED + LOCAL-EXPLICIT
288
- // entirely. Operators rely on this kill switch documented in SECURITY.md;
289
- // bypassing it would silently load third-party code in violation of the
290
- // documented contract.
291
- if (!opts.adaptersEnabled) {
292
- if (opts.configLocalPaths.length > 0) {
293
- warnings.push(
294
- `adapters.enabled=false — refusing all REGISTRY-VERIFIED + LOCAL-EXPLICIT adapters. ` +
295
- `Set massu.config.yaml > adapters.enabled: true to opt in.`,
296
- );
297
- }
298
- return { adapters, warnings };
299
- }
300
-
301
- // 2. REGISTRY-VERIFIED — walk node_modules. For each candidate, look
302
- // up the manifest entry; refuse if not in the allowlist.
303
- const manifestEntries = opts.manifestEnvelope?.manifest.adapters ?? [];
304
- const manifestByName = new Map<string, AdapterEntry>(
305
- manifestEntries.map((e) => [e.package, e]),
306
- );
307
- const npmCandidates = walkNodeModules(opts.projectRoot, warnings);
308
- for (const { packageDir, pkg } of npmCandidates) {
309
- if (seenIds.has(pkg.name)) continue;
310
- const manifestEntry = manifestByName.get(pkg.name);
311
- if (!manifestEntry) {
312
- if (!opts.manifestEnvelope) {
313
- warnings.push(
314
- `cannot verify ${pkg.name}@${pkg.version}: registry manifest unavailable. ` +
315
- `Refusing to load. Run \`massu adapters refresh\` when online.`,
316
- );
317
- } else {
318
- warnings.push(
319
- `refusing ${pkg.name}@${pkg.version}: not in the signed registry manifest. ` +
320
- `If you authored this adapter, submit a PR to the registry per AUTHORING-ADAPTERS.md.`,
321
- );
322
- }
323
- continue;
324
- }
325
- if (manifestEntry.unpublished === true) {
326
- warnings.push(
327
- `refusing ${pkg.name}@${pkg.version}: registry marks this package as unpublished. ` +
328
- `Remove via: npm uninstall ${pkg.name}`,
329
- );
330
- continue;
331
- }
332
- if (manifestEntry.deprecated) {
333
- warnings.push(
334
- `${pkg.name}@${pkg.version} is deprecated since ${manifestEntry.deprecated.since}: ` +
335
- `${manifestEntry.deprecated.reason}. Replacement: ${manifestEntry.deprecated.replacement ?? '(none listed)'}.`,
336
- );
337
- // Adapter still loads despite deprecation — gap-57.
338
- }
339
- if (manifestEntry.version !== pkg.version) {
340
- warnings.push(
341
- `${pkg.name}@${pkg.version} version mismatch with manifest entry ${manifestEntry.version}. ` +
342
- `Loading the installed version; the gap-37 sha256 integrity check below will catch tampering.`,
343
- );
344
- }
345
-
346
- // gap-37 LOAD-time integrity check: re-compute sha256OfDir on the
347
- // installed package and compare to the install-time hash recorded in
348
- // ~/.massu/adapter-manifest-installed.json. Missing sidecar entry →
349
- // refuse (operator must run `massu adapters install <pkg>`); drift →
350
- // refuse (post-install tampering). Caller can suppress for tests via
351
- // skipInstalledIntegrityCheck=true.
352
- // iter-2 MED-NEW-2 fix: load-time hidden-dir refusal. A postinstall
353
- // script that adds `.git/payload.js` to a registered package does NOT
354
- // change the load-time hash (sha256OfDir excludes hidden dirs), so
355
- // the integrity check passes — but the legitimate adapter could
356
- // require() the smuggled payload at runtime. Refusing at load time
357
- // closes the gap.
358
- if (!opts.skipInstalledIntegrityCheck) {
359
- const hiddenDir = containsHiddenDirs(packageDir);
360
- if (hiddenDir !== null) {
361
- warnings.push(
362
- `refusing ${pkg.name}@${pkg.version}: contains '${hiddenDir}' subdirectory. ` +
363
- `Published adapter packages must not ship hidden directories. ` +
364
- `Recover via \`npm uninstall ${pkg.name} && npm install ${pkg.name} && ` +
365
- `massu adapters install ${pkg.name}\`.`,
366
- );
367
- continue;
368
- }
369
- const integrity = verifyInstalledIntegrity(
370
- pkg.name,
371
- packageDir,
372
- opts.installedManifestPath ?? INSTALLED_MANIFEST_PATH,
373
- );
374
- if (integrity.kind !== 'ok') {
375
- warnings.push(`refusing ${pkg.name}@${pkg.version}: ${integrity.reason}`);
376
- continue;
377
- }
378
- // CR-9 audit M4 fix: the sidecar can be tampered post-write (mode
379
- // 0o600 stops other users but NOT same-user processes running
380
- // as the operator). Cross-check the install-time hash recorded in
381
- // the sidecar against the SIGNATURE-VERIFIED manifest entry's
382
- // sha256. If they diverge, the sidecar was modified after install
383
- // — refuse to load. This closes the "attacker writes both the
384
- // package AND the sidecar" gap.
385
- if (integrity.entry.installed_sha256 !== manifestEntry.sha256) {
386
- warnings.push(
387
- `refusing ${pkg.name}@${pkg.version}: install-tracking sidecar's installed_sha256 ` +
388
- `(${integrity.entry.installed_sha256.slice(0, 16)}...) does not match the signed ` +
389
- `manifest entry's sha256 (${manifestEntry.sha256.slice(0, 16)}...). The sidecar ` +
390
- `appears tampered post-install. Recover via \`npm uninstall ${pkg.name} && ` +
391
- `npm install ${pkg.name} && massu adapters install ${pkg.name}\`.`,
392
- );
393
- continue;
394
- }
395
- }
396
-
397
- const origin = getAdapterOrigin({
398
- id: pkg.name,
399
- coreBundledIds: opts.coreBundledIds,
400
- npmPackage: { name: pkg.name, version: pkg.version, massuAdapter: pkg['massu-adapter'] === true },
401
- });
402
- if (origin !== 'registry-verified') {
403
- warnings.push(`expected registry-verified classification for ${pkg.name}, got ${origin ?? 'null'}`);
404
- continue;
405
- }
406
- adapters.push({
407
- id: pkg.name,
408
- origin: 'registry-verified',
409
- version: pkg.version,
410
- packageDir,
411
- });
412
- seenIds.add(pkg.name);
413
- }
414
-
415
- // 3. LOCAL-EXPLICIT — read configLocalPaths. Each entry is already POSIX-
416
- // normalized + path-validated by AdapterLocalPathSchema (config.ts).
417
- //
418
- // Plan 3c gap-32 postinstall-poisoning defense: BEFORE classifying any
419
- // local adapter, check the fingerprint sentinel. If the current
420
- // adapters.local content's fingerprint does NOT match the last
421
- // operator-acknowledged sentinel, REFUSE to load any local adapter and
422
- // surface the drift in warnings. The operator must run
423
- // `massu adapters resync-local-fingerprint` (or add-local/remove-local)
424
- // to re-acknowledge before discovery accepts local adapters again.
425
- const fingerprintCheck = opts.configLocalPaths.length === 0
426
- ? { kind: 'match' as const }
427
- : checkFingerprintDrift(
428
- opts.configLocalPaths,
429
- opts.projectRoot,
430
- opts.fingerprintSentinelPath ?? FINGERPRINT_PATH,
431
- );
432
- if (fingerprintCheck.kind !== 'match') {
433
- if (opts.configLocalPaths.length > 0) {
434
- warnings.push(
435
- `refusing all LOCAL-EXPLICIT adapters: ${fingerprintCheck.reason}`,
436
- );
437
- }
438
- // Skip the LOCAL-EXPLICIT loop entirely.
439
- return { adapters, warnings };
440
- }
441
- const localSet = new Set(opts.configLocalPaths);
442
- for (const localPath of opts.configLocalPaths) {
443
- if (seenIds.has(localPath)) continue;
444
- const absPath = isAbsolute(localPath) ? localPath : resolve(opts.projectRoot, localPath);
445
- if (!existsSync(absPath)) {
446
- warnings.push(
447
- `local adapter file not found: ${localPath} (resolved to ${absPath}). ` +
448
- `Remove via: massu adapters remove-local ${localPath}`,
449
- );
450
- continue;
451
- }
452
- const origin = getAdapterOrigin({
453
- id: localPath,
454
- coreBundledIds: opts.coreBundledIds,
455
- configLocalPaths: localSet,
456
- });
457
- if (origin !== 'local-explicit') {
458
- warnings.push(`expected local-explicit classification for ${localPath}, got ${origin ?? 'null'}`);
459
- continue;
460
- }
461
- adapters.push({
462
- id: localPath,
463
- origin: 'local-explicit',
464
- });
465
- seenIds.add(localPath);
466
- }
467
-
468
- return { adapters, warnings };
469
- }
@@ -1,225 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * Per-adapter file sampler — Plan 1.5.4 §3 deliverable.
6
- *
7
- * Replaces the `sampleFiles: async () => []` placeholder at
8
- * `codebase-introspector.ts:160` that prevented AST adapter introspect
9
- * output from reaching the user-facing config emission. The adapters
10
- * themselves have always worked (verified by `adapter-grammar-strict.test.ts`
11
- * with 10/10 fixtures returning 'high' confidence); the gap was that
12
- * production `introspectAsync()` handed each adapter `SourceFile[] = []`
13
- * so adapters never saw any actual code.
14
- *
15
- * Per Plan 1.5.4 §0 self-attest #3: REUSES `EXTENSIONS` and
16
- * `TEST_FILE_PATTERNS` from `source-dir-detector.ts:84-104` rather than
17
- * duplicating them. Adding a new language to those maps automatically
18
- * enables sampling for any adapter declaring that language.
19
- *
20
- * Algorithm:
21
- * 1. For each language in `adapter.languages`, collect candidate
22
- * source dirs from `detection.sourceDirs[<lang>].dirs` (already
23
- * computed by Tier 0 detection per `source-dir-detector.ts`).
24
- * Fall back to walking common roots (`<projectRoot>` + first-level
25
- * subdirs) if detection didn't find any.
26
- * 2. Walk each source dir up to `MAX_DEPTH` (default 3) levels deep,
27
- * filtering by `EXTENSIONS[<lang>]` and excluding files matching
28
- * any pattern in `TEST_FILE_PATTERNS[<lang>]`.
29
- * 3. Skip `IGNORED_DIRS` (node_modules, .git, dist, etc.) at every
30
- * depth (already enumerated in `source-dir-detector.ts`).
31
- * 4. Cap per-adapter sample count to `MAX_FILES_PER_ADAPTER` (default
32
- * 50) — sufficient for AST queries to find canonical patterns
33
- * without blowing the introspect time budget.
34
- * 5. Per-file size cap inherited from `parse-guard.ts:MAX_AST_FILE_BYTES`
35
- * (256 KB) — files above this limit are dropped here so they don't
36
- * reach the adapter's parse path.
37
- * 6. Return `SourceFile[]` typed per `detect/adapters/types.ts`.
38
- *
39
- * Errors during the walk (permission-denied, broken symlinks, etc.) are
40
- * non-fatal: the sampler logs a single stderr warning and proceeds with
41
- * whatever files it has already collected.
42
- */
43
-
44
- import { readdirSync, readFileSync, statSync, lstatSync } from 'node:fs';
45
- import { join, extname } from 'node:path';
46
- import type { CodebaseAdapter, SourceFile, TreeSitterLanguage } from './types.ts';
47
- import type { DetectionResult } from '../index.ts';
48
- import { MAX_AST_FILE_BYTES } from './parse-guard.ts';
49
-
50
- /**
51
- * Language → file extensions. Sourced from `source-dir-detector.ts`'s
52
- * `EXTENSIONS` map. Re-exported here as the canonical type for AST
53
- * adapter sampling. Keys are `TreeSitterLanguage` so any adapter that
54
- * targets a language not in this map fails the drift test (see
55
- * `sample-files-coverage.test.ts`).
56
- */
57
- export const SAMPLE_EXTENSIONS: Record<TreeSitterLanguage, readonly string[]> = {
58
- python: ['py'],
59
- typescript: ['ts', 'tsx'],
60
- javascript: ['js', 'jsx', 'mjs', 'cjs'],
61
- swift: ['swift'],
62
- rust: ['rs'],
63
- go: ['go'],
64
- ruby: ['rb'],
65
- php: ['php'],
66
- java: ['java', 'kt'],
67
- kotlin: ['kt', 'kts'],
68
- elixir: ['ex', 'exs'],
69
- erlang: ['erl', 'hrl'],
70
- csharp: ['cs'],
71
- cpp: ['cpp', 'cc', 'cxx', 'h', 'hpp'],
72
- haskell: ['hs', 'lhs'],
73
- ocaml: ['ml', 'mli'],
74
- };
75
-
76
- /**
77
- * Test-file patterns to exclude per language. Mirrors
78
- * `source-dir-detector.ts:TEST_FILE_PATTERNS`. Adapters extracting
79
- * "production" routing/auth/etc. conventions don't want to be misled by
80
- * test fixtures.
81
- */
82
- export const SAMPLE_TEST_FILE_PATTERNS: Record<TreeSitterLanguage, readonly RegExp[]> = {
83
- python: [/_test\.py$/, /test_[^/]*\.py$/, /\/tests?\//],
84
- typescript: [/\.test\.tsx?$/, /\.spec\.tsx?$/, /\/__tests__\//],
85
- javascript: [/\.test\.[mc]?jsx?$/, /\.spec\.[mc]?jsx?$/, /\/__tests__\//],
86
- swift: [/Tests\//],
87
- rust: [/tests\/.*\.rs$/],
88
- go: [/_test\.go$/],
89
- ruby: [/_spec\.rb$/, /_test\.rb$/, /\/spec\//],
90
- php: [/Test\.php$/, /\/tests?\//i],
91
- java: [/Test[^/]*\.(java|kt)$/, /[^/]*Test\.(java|kt)$/, /\/test\//],
92
- kotlin: [/Test[^/]*\.kt$/, /[^/]*Test\.kt$/],
93
- elixir: [/_test\.exs$/, /\/test\//],
94
- erlang: [/_SUITE\.erl$/],
95
- csharp: [/Tests?\.cs$/, /\.Tests?\//],
96
- cpp: [/_test\.(cpp|cc)$/i, /\/tests?\//i],
97
- haskell: [/Spec\.hs$/, /\/test\//],
98
- ocaml: [/_test\.ml$/, /\/test\//],
99
- };
100
-
101
- const IGNORED_DIRS = new Set([
102
- 'node_modules', '.venv', 'venv', '__pycache__', 'dist', 'build', '.build',
103
- 'target', '.next', '.nuxt', 'coverage', '.git', '.massu', '.turbo',
104
- '.cache', '.pytest_cache', '.mypy_cache', 'DerivedData', 'Pods',
105
- '_build', 'deps', 'priv', 'cover', '.elixir_ls', // Elixir/Phoenix
106
- 'bin', 'obj', '.vs', 'packages', 'publish', 'TestResults', // .NET
107
- ]);
108
-
109
- export interface SampleFilesOptions {
110
- /** Maximum directory depth to walk from each source dir. Default 3. */
111
- maxDepth?: number;
112
- /** Maximum total files to return per adapter. Default 50. */
113
- maxFilesPerAdapter?: number;
114
- }
115
-
116
- const DEFAULT_MAX_DEPTH = 3;
117
- const DEFAULT_MAX_FILES = 50;
118
-
119
- /**
120
- * Sample source files for an AST adapter.
121
- *
122
- * @param adapter - the adapter declaring `languages: TreeSitterLanguage[]`
123
- * @param projectRoot - absolute project root
124
- * @param detection - existing detection result (provides per-language
125
- * source dirs from `source-dir-detector`)
126
- * @param options - depth and count caps
127
- */
128
- export function sampleFilesForAdapter(
129
- adapter: CodebaseAdapter,
130
- projectRoot: string,
131
- detection: DetectionResult,
132
- options: SampleFilesOptions = {},
133
- ): SourceFile[] {
134
- const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
135
- const maxFiles = options.maxFilesPerAdapter ?? DEFAULT_MAX_FILES;
136
-
137
- const out: SourceFile[] = [];
138
- const seen = new Set<string>(); // dedup by absolute path
139
-
140
- for (const lang of adapter.languages) {
141
- if (out.length >= maxFiles) break;
142
- const exts = SAMPLE_EXTENSIONS[lang];
143
- if (!exts || exts.length === 0) continue;
144
- const testPatterns = SAMPLE_TEST_FILE_PATTERNS[lang] ?? [];
145
-
146
- // Determine candidate dirs. Prefer detection.sourceDirs[lang] when
147
- // present; otherwise walk projectRoot itself.
148
- // (lang here is `TreeSitterLanguage`; sourceDirs is keyed by
149
- // `SupportedLanguage`. The intersection is structural — every
150
- // SupportedLanguage is a TreeSitterLanguage. We coerce via
151
- // `Record<string, …>` lookup which is type-safe in a Record<string>.)
152
- const langKey = lang as unknown as keyof typeof detection.sourceDirs;
153
- const langDetection = detection.sourceDirs[langKey];
154
- const candidateDirs: string[] = [];
155
- if (langDetection?.source_dirs && langDetection.source_dirs.length > 0) {
156
- candidateDirs.push(...langDetection.source_dirs.map((d: string) => join(projectRoot, d)));
157
- } else {
158
- candidateDirs.push(projectRoot);
159
- }
160
-
161
- for (const dir of candidateDirs) {
162
- if (out.length >= maxFiles) break;
163
- walkDir(dir, exts, testPatterns, lang, maxDepth, 0, out, seen, maxFiles);
164
- }
165
- }
166
-
167
- return out;
168
- }
169
-
170
- function walkDir(
171
- dir: string,
172
- exts: readonly string[],
173
- testPatterns: readonly RegExp[],
174
- lang: TreeSitterLanguage,
175
- maxDepth: number,
176
- curDepth: number,
177
- out: SourceFile[],
178
- seen: Set<string>,
179
- maxFiles: number,
180
- ): void {
181
- if (curDepth > maxDepth) return;
182
- if (out.length >= maxFiles) return;
183
- let entries: string[];
184
- try {
185
- entries = readdirSync(dir);
186
- } catch {
187
- return;
188
- }
189
- for (const entry of entries) {
190
- if (out.length >= maxFiles) return;
191
- if (entry.startsWith('.')) continue; // hidden
192
- if (IGNORED_DIRS.has(entry)) continue;
193
- const fullPath = join(dir, entry);
194
- let st;
195
- try {
196
- st = lstatSync(fullPath);
197
- } catch {
198
- continue;
199
- }
200
- if (st.isSymbolicLink()) continue; // refuse symlinks (security)
201
- if (st.isDirectory()) {
202
- walkDir(fullPath, exts, testPatterns, lang, maxDepth, curDepth + 1, out, seen, maxFiles);
203
- continue;
204
- }
205
- if (!st.isFile()) continue;
206
- if (st.size > MAX_AST_FILE_BYTES) continue; // size cap (parse-guard)
207
- const ext = extname(entry).slice(1);
208
- if (!exts.includes(ext)) continue;
209
- if (testPatterns.some((p) => p.test(fullPath))) continue;
210
- if (seen.has(fullPath)) continue;
211
- seen.add(fullPath);
212
- let content: string;
213
- try {
214
- content = readFileSync(fullPath, 'utf-8');
215
- } catch {
216
- continue;
217
- }
218
- out.push({
219
- path: fullPath,
220
- content,
221
- language: lang,
222
- size: st.size,
223
- });
224
- }
225
- }