@massu/core 1.5.3 → 1.5.5

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.
@@ -12,6 +12,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  if (typeof require !== "undefined") return require.apply(this, arguments);
13
13
  throw Error('Dynamic require of "' + x + '" is not supported');
14
14
  });
15
+ var __esm = (fn, res) => function __init() {
16
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ };
15
18
  var __commonJS = (cb, mod) => function __require2() {
16
19
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
20
  };
@@ -5781,6 +5784,15 @@ var require_out4 = __commonJS({
5781
5784
  }
5782
5785
  });
5783
5786
 
5787
+ // src/detect/adapters/parse-guard.ts
5788
+ var MAX_AST_FILE_BYTES;
5789
+ var init_parse_guard = __esm({
5790
+ "src/detect/adapters/parse-guard.ts"() {
5791
+ "use strict";
5792
+ MAX_AST_FILE_BYTES = 1 * 1024 * 1024;
5793
+ }
5794
+ });
5795
+
5784
5796
  // src/memory-db.ts
5785
5797
  import Database from "better-sqlite3";
5786
5798
  import { dirname as dirname2, basename } from "path";
@@ -9335,8 +9347,8 @@ function relativeTo(projectRoot, absPath) {
9335
9347
  return basename2(absPath);
9336
9348
  }
9337
9349
 
9338
- // src/detect/adapters/parse-guard.ts
9339
- var MAX_AST_FILE_BYTES = 1 * 1024 * 1024;
9350
+ // src/detect/adapters/runner.ts
9351
+ init_parse_guard();
9340
9352
 
9341
9353
  // node_modules/web-tree-sitter/tree-sitter.js
9342
9354
  var __defProp2 = Object.defineProperty;
@@ -13285,6 +13297,36 @@ var Parser = class {
13285
13297
  }
13286
13298
  };
13287
13299
 
13300
+ // src/detect/adapters/python-fastapi.ts
13301
+ init_parse_guard();
13302
+
13303
+ // src/detect/adapters/python-django.ts
13304
+ init_parse_guard();
13305
+
13306
+ // src/detect/adapters/nextjs-trpc.ts
13307
+ init_parse_guard();
13308
+
13309
+ // src/detect/adapters/swift-swiftui.ts
13310
+ init_parse_guard();
13311
+
13312
+ // src/detect/adapters/python-flask.ts
13313
+ init_parse_guard();
13314
+
13315
+ // src/detect/adapters/go-chi.ts
13316
+ init_parse_guard();
13317
+
13318
+ // src/detect/adapters/rails.ts
13319
+ init_parse_guard();
13320
+
13321
+ // src/detect/adapters/phoenix.ts
13322
+ init_parse_guard();
13323
+
13324
+ // src/detect/adapters/aspnet.ts
13325
+ init_parse_guard();
13326
+
13327
+ // src/detect/adapters/spring.ts
13328
+ init_parse_guard();
13329
+
13288
13330
  // src/detect/codebase-introspector.ts
13289
13331
  function introspect(detection, projectRoot) {
13290
13332
  const out2 = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
@@ -84,6 +84,13 @@ export interface InitOptions {
84
84
  cwd?: string;
85
85
  /** Suppress console output. */
86
86
  silent?: boolean;
87
+ /**
88
+ * Plan 1.5.4: skip AST adapter introspection that surfaces under
89
+ * `detected.<adapter-id>:` blocks. Default false (introspect runs);
90
+ * set true via `--no-introspect` for fast sync-only init or when
91
+ * the AST tier's grammar download isn't desirable.
92
+ */
93
+ skipIntrospect?: boolean;
87
94
  }
88
95
 
89
96
  export interface GenerateConfigV2Options {
@@ -1187,6 +1194,7 @@ export function parseInitArgs(argv: string[]): ParseInitArgsResult {
1187
1194
  if (a === '--ci') opts.ci = true;
1188
1195
  else if (a === '--force') opts.force = true;
1189
1196
  else if (a === '--skip-commands') opts.skipCommands = true;
1197
+ else if (a === '--no-introspect') opts.skipIntrospect = true;
1190
1198
  else if (a === '--help' || a === '-h') opts.help = true;
1191
1199
  else if (a === '--template') {
1192
1200
  const next = argv[i + 1];
@@ -1386,7 +1394,41 @@ export async function runInit(argv?: string[], overrides?: InitOptions): Promise
1386
1394
  // Phoenix / clear-Spring projects (CR-39 violation per the Plan 1.5.1
1387
1395
  // 5-fixture verification).
1388
1396
  const baseConfig = buildConfigFromDetection({ projectRoot, detection });
1389
- const config = applyVariantTemplate(baseConfig, resolveTemplatesDir());
1397
+ const withVariant = applyVariantTemplate(baseConfig, resolveTemplatesDir());
1398
+
1399
+ // Plan 1.5.4 §3: pipe AST adapter introspect output into the emitted
1400
+ // config under `detected.<adapter-id>:` blocks. introspectAsync runs
1401
+ // the AST adapter pipeline (using the real file sampler from
1402
+ // codebase-introspector.ts post-1.5.4); each adapter that returns
1403
+ // non-'none' confidence surfaces its conventions + provenance.
1404
+ // --no-introspect bypasses for users who want sync init.
1405
+ let config = withVariant;
1406
+ if (!opts.skipIntrospect) {
1407
+ try {
1408
+ const { introspectAsync } = await import('../detect/codebase-introspector.ts');
1409
+ const introspected = await introspectAsync(detection, projectRoot);
1410
+ const detectedBlocks: Record<string, unknown> = {};
1411
+ for (const [key, block] of Object.entries(introspected)) {
1412
+ // `introspected` includes language-keyed regex-fallback blocks
1413
+ // (`python`, `swift`, `typescript`) AND adapter-id-keyed AST
1414
+ // blocks. The AST blocks are the ones we want under
1415
+ // `detected.<adapter-id>:` — distinguishable by the presence of
1416
+ // a `_confidence` field on the block (the regex blocks don't
1417
+ // carry it). Filter for AST adapter outputs only.
1418
+ if (block && typeof block === 'object' && '_confidence' in (block as Record<string, unknown>)) {
1419
+ detectedBlocks[key] = block;
1420
+ }
1421
+ }
1422
+ if (Object.keys(detectedBlocks).length > 0) {
1423
+ config = { ...config, detected: detectedBlocks };
1424
+ }
1425
+ } catch (err) {
1426
+ // Non-fatal: AST introspect is enrichment, not core detection.
1427
+ // Log to stderr so operators see the warning if it matters.
1428
+ errLog(`warning: AST adapter introspection failed: ${err instanceof Error ? err.message : String(err)}`);
1429
+ }
1430
+ }
1431
+
1390
1432
  const content = renderConfigYaml(config);
1391
1433
  const writeRes = writeConfigAtomic(configPath, content);
1392
1434
  if (!writeRes.validated) {
@@ -0,0 +1,225 @@
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
+ }
@@ -46,4 +46,6 @@ export const ADAPTER_SUPPORT_FILES: ReadonlySet<string> = new Set([
46
46
  'tree-sitter-loader.ts',
47
47
  'index.ts',
48
48
  'discover.ts',
49
+ // Plan 1.5.4: file sampler (support module, not an adapter).
50
+ 'file-sampler.ts',
49
51
  ]);
@@ -48,6 +48,19 @@ import { pythonFastApiAdapter } from './adapters/python-fastapi.ts';
48
48
  import { pythonDjangoAdapter } from './adapters/python-django.ts';
49
49
  import { nextjsTrpcAdapter } from './adapters/nextjs-trpc.ts';
50
50
  import { swiftSwiftUiAdapter } from './adapters/swift-swiftui.ts';
51
+ // Plan 1.5.4 R-011 discovery: Phase 7 adapters were committed but never
52
+ // added to FIRST_PARTY_ADAPTERS. The omission was masked pre-1.5.4 by
53
+ // sampleFiles=[] (every adapter returned 'none' anyway). Now that the
54
+ // sampler works, these 6 adapters MUST be in the dispatch list or
55
+ // `npx massu init` against a Phase 7 project produces no detected.<id>:
56
+ // block (verified via debug instrumentation 2026-05-08 against the
57
+ // 1.5.4 published bundle).
58
+ import { pythonFlaskAdapter } from './adapters/python-flask.ts';
59
+ import { goChiAdapter } from './adapters/go-chi.ts';
60
+ import { railsAdapter } from './adapters/rails.ts';
61
+ import { phoenixAdapter } from './adapters/phoenix.ts';
62
+ import { aspnetAdapter } from './adapters/aspnet.ts';
63
+ import { springAdapter } from './adapters/spring.ts';
51
64
  import type { CodebaseAdapter, AdapterResolved } from './adapters/types.ts';
52
65
 
53
66
  // ============================================================
@@ -75,8 +88,14 @@ export interface DetectedConventions {
75
88
  const FIRST_PARTY_ADAPTERS: CodebaseAdapter[] = [
76
89
  pythonFastApiAdapter,
77
90
  pythonDjangoAdapter,
91
+ pythonFlaskAdapter,
78
92
  nextjsTrpcAdapter,
79
93
  swiftSwiftUiAdapter,
94
+ goChiAdapter,
95
+ railsAdapter,
96
+ phoenixAdapter,
97
+ aspnetAdapter,
98
+ springAdapter,
80
99
  ];
81
100
 
82
101
  // ============================================================
@@ -152,20 +171,21 @@ export async function introspectAsync(
152
171
  ): Promise<DetectedConventions> {
153
172
  const out: DetectedConventions = introspect(detection, projectRoot);
154
173
 
155
- // Build signals + run AST adapters
174
+ // Build signals + run AST adapters with the real file sampler (Plan
175
+ // 1.5.4 §3). Pre-1.5.4 this was a placeholder returning [], which kept
176
+ // every AST adapter at 'none' confidence in the init flow. The
177
+ // adapters themselves always worked (verified by adapter-grammar-
178
+ // strict.test.ts); the gap was purely the seam between detection +
179
+ // adapter execution. The sampler reuses EXTENSIONS / TEST_FILE_PATTERNS
180
+ // from source-dir-detector.ts so adding a new language doesn't require
181
+ // a parallel map (CR-46 self-attest #3).
156
182
  const signals = buildDetectionSignals(projectRoot);
183
+ const { sampleFilesForAdapter } = await import('./adapters/file-sampler.ts');
157
184
  let merged;
158
185
  try {
159
186
  merged = await runAdapters(FIRST_PARTY_ADAPTERS, projectRoot, signals, {
160
- sampleFiles: async (_adapter, _root) => {
161
- // Phase 1 placeholder: file sampling for adapters is wired in
162
- // dedicated harnesses (per-adapter tests inject SourceFile[] directly).
163
- // The introspector tier doesn't yet sample for AST adapters — that
164
- // wiring lands together with Phase 4 LSP enrichment so the same path
165
- // serves both. For now, returning [] keeps adapters at 'none' which
166
- // means `out` is regex-only — consistent with the sync path and the
167
- // pre-Phase-1 baseline test suite.
168
- return [];
187
+ sampleFiles: async (adapter, root) => {
188
+ return sampleFilesForAdapter(adapter, root, detection);
169
189
  },
170
190
  });
171
191
  } catch {
@@ -1,4 +1,4 @@
1
- // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-08T20:39:46.758Z.
1
+ // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-08T20:50:59.749Z.
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