@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,284 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * Plan 3b — Phase 1: AST adapter runner.
6
- *
7
- * Orchestrates: filter adapters via `matches()`, run them, isolate failures
8
- * via per-adapter try/catch (audit-iter-5 fix HH test (d)), and merge their
9
- * results.
10
- *
11
- * Confidence merge rule (spec §5):
12
- * - 'high' / 'medium' / 'low' → field is written, with per-field provenance.
13
- * - 'none' → field DROPPED (introspect's regex fallback may then emit it).
14
- *
15
- * AST-wins rule:
16
- * - When the same conventions key appears in two adapters that BOTH return
17
- * non-'none', the FIRST adapter (by source-list order) wins. This is
18
- * deterministic — adapters are listed in `runner.ts`'s static array,
19
- * never user-provided.
20
- */
21
-
22
- import { basename, relative } from 'path';
23
- import type {
24
- AdapterResolved,
25
- CodebaseAdapter,
26
- DetectionSignals,
27
- MergedAdapterOutput,
28
- Provenance,
29
- SourceFile,
30
- } from './types.ts';
31
- import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
32
-
33
- export interface RunAdaptersOptions {
34
- /**
35
- * Optional file sampler — given an adapter and the project root, returns
36
- * the SourceFile[] the adapter should consume. If omitted, the runner
37
- * passes an empty file list (useful in unit tests where the caller has
38
- * already constructed adapters that don't need files).
39
- */
40
- sampleFiles?: (adapter: CodebaseAdapter, rootDir: string) => Promise<SourceFile[]> | SourceFile[];
41
- }
42
-
43
- /**
44
- * Run a static list of adapters against a project root.
45
- *
46
- * Per-adapter try/catch isolation: a single adapter throwing MUST NOT crash
47
- * the runner. The error is captured in `errored[]` and the runner continues.
48
- *
49
- * @param adapters - Static list of first-party adapters (no user-authored
50
- * adapters at v1 — Plan 3c will add discovery).
51
- * @param rootDir - Absolute project root.
52
- * @param signals - Pre-built `DetectionSignals` (manifest reads, present
53
- * dirs/files). Adapters consume these read-only.
54
- * @param options - Hooks for testing.
55
- */
56
- export async function runAdapters(
57
- adapters: CodebaseAdapter[],
58
- rootDir: string,
59
- signals: DetectionSignals,
60
- options: RunAdaptersOptions = {},
61
- ): Promise<MergedAdapterOutput> {
62
- const out: MergedAdapterOutput = {
63
- byAdapter: {},
64
- skipped: [],
65
- errored: [],
66
- };
67
-
68
- // AST-wins / per-adapter merge:
69
- // Each adapter writes to its own `detected.<adapter.id>` namespace, so
70
- // global field collisions across adapters can't happen at the conventions
71
- // level. The "AST-wins" rule in the spec applies at the introspector tier
72
- // (regex fallback only fills fields the adapter returned 'none' for).
73
- // Within a single adapter, if `conventions` repeats a key (shouldn't, but
74
- // defensively), the first occurrence wins. For multiple adapters with the
75
- // same id (shouldn't, but defensively), the first wins.
76
-
77
- for (const adapter of adapters) {
78
- if (out.byAdapter[adapter.id] || out.skipped.includes(adapter.id)) {
79
- // Duplicate adapter id → skip the second one to preserve first-wins.
80
- continue;
81
- }
82
- let matches: boolean;
83
- try {
84
- matches = adapter.matches(signals);
85
- } catch (e) {
86
- out.errored.push({
87
- adapterId: adapter.id,
88
- error: `matches() threw: ${e instanceof Error ? e.message : String(e)}`,
89
- });
90
- continue;
91
- }
92
- if (!matches) {
93
- out.skipped.push(adapter.id);
94
- continue;
95
- }
96
-
97
- let files: SourceFile[];
98
- try {
99
- files = options.sampleFiles
100
- ? await options.sampleFiles(adapter, rootDir)
101
- : [];
102
- } catch (e) {
103
- out.errored.push({
104
- adapterId: adapter.id,
105
- error: `sampleFiles threw: ${e instanceof Error ? e.message : String(e)}`,
106
- });
107
- continue;
108
- }
109
-
110
- // Phase 3.5 fix: size + depth + control-byte gate. Drop adversarial
111
- // inputs BEFORE the adapter sees them — adapters trust this layer.
112
- // Files dropped here are logged once per drop so operators see the
113
- // signal; the adapter then runs against the surviving subset.
114
- const safeFiles: SourceFile[] = [];
115
- for (const f of files) {
116
- const skip = isParsableSource(f.content, f.size);
117
- if (skip) {
118
- process.stderr.write(
119
- `[massu/ast] WARN: skipping ${f.path} for adapter ${adapter.id}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES} bytes. (Phase 3.5 mitigation)\n`,
120
- );
121
- continue;
122
- }
123
- safeFiles.push(f);
124
- }
125
- files = safeFiles;
126
-
127
- let result;
128
- try {
129
- result = await adapter.introspect(files, rootDir);
130
- } catch (e) {
131
- out.errored.push({
132
- adapterId: adapter.id,
133
- error: `introspect() threw: ${e instanceof Error ? e.message : String(e)}`,
134
- });
135
- continue;
136
- }
137
-
138
- // 'none' confidence drops the entire adapter result. The runner records
139
- // that the adapter was attempted (in `byAdapter`) so callers can see it
140
- // ran, but with empty conventions. introspect()'s regex fallback then
141
- // takes over for the field.
142
- if (result.confidence === 'none') {
143
- out.byAdapter[adapter.id] = {
144
- conventions: {},
145
- _provenance: {},
146
- confidence: 'none',
147
- };
148
- continue;
149
- }
150
-
151
- // Merge: keep first occurrence of each field (defensive against an
152
- // adapter accidentally writing the same field twice).
153
- const conventions: Record<string, unknown> = {};
154
- const provenanceMap: Record<string, string> = {};
155
- for (const [field, value] of Object.entries(result.conventions)) {
156
- if (value === null || value === undefined) continue;
157
- if (field in conventions) continue;
158
- conventions[field] = value;
159
- }
160
- for (const p of result.provenance) {
161
- if (p.field in provenanceMap) continue;
162
- provenanceMap[p.field] = formatProvenance(p, rootDir);
163
- }
164
-
165
- const resolved: AdapterResolved = {
166
- conventions,
167
- _provenance: provenanceMap,
168
- confidence: result.confidence,
169
- };
170
- out.byAdapter[adapter.id] = resolved;
171
- }
172
-
173
- return out;
174
- }
175
-
176
- function formatProvenance(p: Provenance, rootDir: string): string {
177
- const rel = p.sourceFile.startsWith(rootDir + '/')
178
- ? relative(rootDir, p.sourceFile)
179
- : basename(p.sourceFile);
180
- return `${rel}:${p.line} :: ${p.query}`;
181
- }
182
-
183
- // ============================================================
184
- // Signal builder — used by codebase-introspector to feed the runner
185
- // ============================================================
186
-
187
- import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
188
- import { join } from 'path';
189
-
190
- /**
191
- * Build a `DetectionSignals` bundle by reading manifest files at the project
192
- * root. Cheap (one-level dir scan + a handful of file reads). Failures
193
- * degrade gracefully — a missing manifest just means that field is undefined.
194
- */
195
- export function buildDetectionSignals(rootDir: string): DetectionSignals {
196
- const presentDirs = new Set<string>();
197
- const presentFiles = new Set<string>();
198
- try {
199
- for (const entry of readdirSync(rootDir)) {
200
- if (entry.startsWith('.')) continue;
201
- try {
202
- const st = statSync(join(rootDir, entry));
203
- if (st.isDirectory()) presentDirs.add(entry);
204
- else if (st.isFile()) presentFiles.add(entry);
205
- } catch {
206
- /* ignore */
207
- }
208
- }
209
- } catch {
210
- /* unreadable root → empty signals */
211
- }
212
-
213
- return {
214
- packageJson: tryReadJson(join(rootDir, 'package.json')),
215
- pyprojectToml: tryReadToml(join(rootDir, 'pyproject.toml')),
216
- gemfile: tryReadString(join(rootDir, 'Gemfile')),
217
- cargoToml: tryReadToml(join(rootDir, 'Cargo.toml')),
218
- goMod: tryReadString(join(rootDir, 'go.mod')),
219
- mixExs: tryReadString(join(rootDir, 'mix.exs')),
220
- csproj: tryReadFirstCsproj(rootDir, presentFiles),
221
- pomXml: tryReadString(join(rootDir, 'pom.xml')),
222
- gradleBuild: tryReadGradleBuild(rootDir, presentFiles),
223
- presentDirs,
224
- presentFiles,
225
- };
226
- }
227
-
228
- /**
229
- * Find the first `.csproj` file at the project root (sorted alphabetically
230
- * for determinism) and return its raw XML content. Returns undefined if
231
- * none exist. Multi-project solutions where the root has no top-level
232
- * .csproj (the `.sln` lives at root and projects are in subdirs) get
233
- * `csproj=undefined` — adapters then degrade per their own logic.
234
- */
235
- function tryReadFirstCsproj(rootDir: string, presentFiles: Set<string>): string | undefined {
236
- const csprojNames = [...presentFiles].filter((f) => f.endsWith('.csproj')).sort();
237
- if (csprojNames.length === 0) return undefined;
238
- return tryReadString(join(rootDir, csprojNames[0]!));
239
- }
240
-
241
- /**
242
- * Read `build.gradle.kts` (Kotlin DSL — modern default per Gradle 7+) if
243
- * present; otherwise fall back to `build.gradle` (legacy Groovy DSL).
244
- * Returns undefined if neither exists.
245
- */
246
- function tryReadGradleBuild(rootDir: string, presentFiles: Set<string>): string | undefined {
247
- if (presentFiles.has('build.gradle.kts')) {
248
- return tryReadString(join(rootDir, 'build.gradle.kts'));
249
- }
250
- if (presentFiles.has('build.gradle')) {
251
- return tryReadString(join(rootDir, 'build.gradle'));
252
- }
253
- return undefined;
254
- }
255
-
256
- function tryReadString(path: string): string | undefined {
257
- if (!existsSync(path)) return undefined;
258
- try {
259
- return readFileSync(path, 'utf-8');
260
- } catch {
261
- return undefined;
262
- }
263
- }
264
-
265
- function tryReadJson(path: string): Record<string, unknown> | undefined {
266
- const txt = tryReadString(path);
267
- if (!txt) return undefined;
268
- try {
269
- const parsed = JSON.parse(txt);
270
- return typeof parsed === 'object' && parsed !== null ? (parsed as Record<string, unknown>) : undefined;
271
- } catch {
272
- return undefined;
273
- }
274
- }
275
-
276
- function tryReadToml(path: string): Record<string, unknown> | undefined {
277
- const txt = tryReadString(path);
278
- if (!txt) return undefined;
279
- // Cheap signal-only parse: we just need top-level table presence + keys.
280
- // Avoid pulling the full toml parser for this; check `[project]`/`[tool.x]`
281
- // headers and treat `tool.poetry.dependencies` etc. as opaque text-search.
282
- // Adapters that need structured data can grep `txt` themselves.
283
- return { __raw: txt };
284
- }
@@ -1,4 +0,0 @@
1
- // Plan 3c Phase 9b P-A-005: re-export shim. Source-of-truth lives at
2
- // `packages/adapter-spring/src/index.ts` (workspace package). This shim
3
- // preserves the legacy import path used by codebase-introspector + tests.
4
- export { springAdapter } from '@massu/adapter-spring';
@@ -1,171 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * Plan 3b — Phase 1: SwiftUI AST adapter.
6
- *
7
- * Extracts:
8
- * - api_client_class: identifier ending in `API` (e.g. `HedgeAPI`)
9
- * - biometric_policy: `LAPolicy.deviceOwnerAuthenticationWithBiometrics` etc.
10
- * - navigation_pattern: 'NavigationStack' | 'NavigationView' | null
11
- *
12
- * Tree-sitter Swift grammar quirks: the `tree-sitter-swift` grammar names some
13
- * nodes differently from python/typescript. We use simpler, more permissive
14
- * S-expressions that fall back to capture-text matching where needed.
15
- */
16
-
17
- import { Parser } from 'web-tree-sitter';
18
- import type { CodebaseAdapter, AdapterResult, DetectionSignals, Provenance, SourceFile } from './types.ts';
19
- import { runQuery, InvalidQueryError } from './query-helpers.ts';
20
- import { loadGrammar } from './tree-sitter-loader.ts';
21
- import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
22
-
23
- // ============================================================
24
- // Queries
25
- // ============================================================
26
-
27
- /**
28
- * Identifier that looks like an API client class. Captures any uppercase-led
29
- * identifier ending in `API`. Predicate filtering is done in JS — Swift's
30
- * grammar doesn't surface a clean class-instantiation pattern uniformly.
31
- */
32
- const API_CLASS_QUERY = `
33
- (simple_identifier) @ident
34
- `;
35
-
36
- /**
37
- * `.deviceOwnerAuthentication` / `.deviceOwnerAuthenticationWithBiometrics`
38
- * member access. Captures the property name.
39
- */
40
- const POLICY_QUERY = `
41
- (navigation_expression
42
- suffix: (navigation_suffix
43
- (simple_identifier) @policy_name))
44
- `;
45
-
46
- /**
47
- * NavigationStack / NavigationView usage. Captures any reference to either
48
- * symbol.
49
- */
50
- const NAV_QUERY = `
51
- (simple_identifier) @nav_ident
52
- `;
53
-
54
- // ============================================================
55
- // Adapter
56
- // ============================================================
57
-
58
- const POLICY_NAMES = new Set([
59
- 'deviceOwnerAuthentication',
60
- 'deviceOwnerAuthenticationWithBiometrics',
61
- ]);
62
-
63
- export const swiftSwiftUiAdapter: CodebaseAdapter = {
64
- id: 'swift-swiftui',
65
- languages: ['swift'],
66
-
67
- matches(signals: DetectionSignals): boolean {
68
- // Swift signal: presence of Package.swift, *.xcodeproj, or Sources/ dir
69
- if (signals.presentFiles.has('Package.swift')) return true;
70
- for (const dir of signals.presentDirs) {
71
- if (dir.endsWith('.xcodeproj') || dir.endsWith('.xcworkspace')) return true;
72
- if (dir === 'Sources') return true;
73
- }
74
- for (const file of signals.presentFiles) {
75
- if (file.endsWith('.swift')) return true;
76
- }
77
- return false;
78
- },
79
-
80
- async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
81
- if (files.length === 0) {
82
- return { conventions: {}, provenance: [], confidence: 'none' };
83
- }
84
-
85
- let language;
86
- try {
87
- language = await loadGrammar('swift');
88
- } catch {
89
- return { conventions: {}, provenance: [], confidence: 'none' };
90
- }
91
-
92
- const parser = new Parser();
93
- parser.setLanguage(language);
94
-
95
- const apiClasses = new Map<string, { line: number; file: string }>();
96
- const policies = new Map<string, { line: number; file: string }>();
97
- const navs = new Map<string, { line: number; file: string }>();
98
-
99
- try {
100
- for (const file of files) {
101
- const skip = isParsableSource(file.content, file.size);
102
- if (skip) {
103
- process.stderr.write(
104
- `[massu/ast] WARN: swift-swiftui skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
105
- );
106
- continue;
107
- }
108
- try {
109
- // API class names: filter via JS regex on the captured identifier
110
- for (const hit of runQuery(parser, file.content, API_CLASS_QUERY, 'swift-api-class', file.path)) {
111
- const ident = hit.captures.ident;
112
- if (ident && /^[A-Z][A-Za-z0-9_]*API$/.test(ident) && !apiClasses.has(ident)) {
113
- apiClasses.set(ident, { line: hit.line, file: file.path });
114
- }
115
- }
116
- // Biometric policy
117
- for (const hit of runQuery(parser, file.content, POLICY_QUERY, 'swift-biometric-policy', file.path)) {
118
- const name = hit.captures.policy_name;
119
- if (name && POLICY_NAMES.has(name) && !policies.has(name)) {
120
- policies.set(name, { line: hit.line, file: file.path });
121
- }
122
- }
123
- // Navigation
124
- for (const hit of runQuery(parser, file.content, NAV_QUERY, 'swift-navigation', file.path)) {
125
- const ident = hit.captures.nav_ident;
126
- if ((ident === 'NavigationStack' || ident === 'NavigationView') && !navs.has(ident)) {
127
- navs.set(ident, { line: hit.line, file: file.path });
128
- }
129
- }
130
- } catch (e) {
131
- if (e instanceof InvalidQueryError) throw e;
132
- continue;
133
- }
134
- }
135
- } finally {
136
- try { parser.delete(); } catch { /* ignore */ }
137
- }
138
-
139
- const conventions: Record<string, unknown> = {};
140
- const provenance: Provenance[] = [];
141
-
142
- if (apiClasses.size > 0) {
143
- const [name, { line, file }] = apiClasses.entries().next().value as [string, { line: number; file: string }];
144
- conventions.api_client_class = name;
145
- provenance.push({ field: 'api_client_class', sourceFile: file, line, query: 'swift-api-class' });
146
- }
147
- if (policies.size > 0) {
148
- const [name, { line, file }] = policies.entries().next().value as [string, { line: number; file: string }];
149
- conventions.biometric_policy = name;
150
- provenance.push({ field: 'biometric_policy', sourceFile: file, line, query: 'swift-biometric-policy' });
151
- }
152
- if (navs.size > 0) {
153
- const [name, { line, file }] = navs.entries().next().value as [string, { line: number; file: string }];
154
- conventions.navigation_pattern = name;
155
- provenance.push({ field: 'navigation_pattern', sourceFile: file, line, query: 'swift-navigation' });
156
- }
157
-
158
- let confidence: AdapterResult['confidence'];
159
- if (Object.keys(conventions).length === 0) {
160
- confidence = 'none';
161
- } else if (apiClasses.size === 1 && policies.size <= 1) {
162
- confidence = 'high';
163
- } else if (apiClasses.size > 1) {
164
- confidence = 'low';
165
- } else {
166
- confidence = 'medium';
167
- }
168
-
169
- return { conventions, provenance, confidence };
170
- },
171
- };