@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,4 +0,0 @@
1
- // Plan 3c Phase 9b P-A-005: re-export shim. Source-of-truth lives at
2
- // `packages/adapter-go-chi/src/index.ts` (workspace package). This shim
3
- // preserves the legacy import path used by codebase-introspector + tests.
4
- export { goChiAdapter } from '@massu/adapter-go-chi';
@@ -1,51 +0,0 @@
1
- /**
2
- * CORE-BUNDLED adapter id source of truth.
3
- *
4
- * Plan 3c Phase 5 5H + 5I deliverable. The CLI's `adapters list` consumes
5
- * this set to know which ids should be classified as CORE-BUNDLED via the
6
- * three-class trust model (security/adapter-origin.ts:getAdapterOrigin).
7
- *
8
- * CR-46 / Rule 0 drift-prevention: a static set is the wrong shape (it
9
- * inevitably drifts as Plan 3b/Phase 7 adds adapters). Instead the set
10
- * is paired with a drift-guard test
11
- * (__tests__/core-bundled-ids-drift.test.ts) that asserts CORE_BUNDLED_IDS
12
- * matches the actual filesystem state of `detect/adapters/*.ts` minus the
13
- * support modules (types, runner, query-helpers, parse-guard, tree-sitter-
14
- * loader, index, discover). A new adapter file added to the directory
15
- * without updating this set fails the drift-guard test → cannot merge.
16
- *
17
- * Plan 3b shipped 4 (next, fastapi, django, swiftui); Phase 7 ships 31
18
- * more for 35 total. Each Phase 7 commit MUST update this set in lockstep
19
- * with the new adapter file under detect/adapters/.
20
- */
21
-
22
- export const CORE_BUNDLED_IDS: ReadonlySet<string> = new Set([
23
- 'aspnet',
24
- 'go-chi',
25
- 'nextjs-trpc',
26
- 'phoenix',
27
- 'python-django',
28
- 'python-fastapi',
29
- 'python-flask',
30
- 'rails',
31
- 'spring',
32
- 'swift-swiftui',
33
- ]);
34
-
35
- /**
36
- * Filenames in `detect/adapters/` that are NOT first-party adapters but
37
- * support modules (the adapter contract types, the runner, helpers).
38
- * The drift-guard test uses this list to subtract support files from the
39
- * filesystem scan before asserting equality with CORE_BUNDLED_IDS.
40
- */
41
- export const ADAPTER_SUPPORT_FILES: ReadonlySet<string> = new Set([
42
- 'types.ts',
43
- 'runner.ts',
44
- 'query-helpers.ts',
45
- 'parse-guard.ts',
46
- 'tree-sitter-loader.ts',
47
- 'index.ts',
48
- 'discover.ts',
49
- // Plan 1.5.4: file sampler (support module, not an adapter).
50
- 'file-sampler.ts',
51
- ]);
@@ -1,166 +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: Next.js + tRPC AST adapter.
6
- *
7
- * Extracts:
8
- * - trpc_router_builder: name of router-creation call (createTRPCRouter, t.router, router)
9
- * - procedure_pattern: identifier ending in `Procedure` (publicProcedure, protectedProcedure)
10
- * - ctx_shape: 'object' | 'function' | null — based on resolver signature shape
11
- *
12
- * Looks under `server/api/routers/` or `server/trpc/` paths. The runner is
13
- * responsible for sampling files into those paths; this adapter assumes the
14
- * `files` it receives are router-shaped.
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
- * Router builder call: `createTRPCRouter({...})` or `t.router({...})`. Captures
29
- * the call's function expression so the runner can normalize it.
30
- */
31
- const ROUTER_BUILDER_QUERY = `
32
- (call_expression
33
- function: (identifier) @builder_id (#match? @builder_id "^(createTRPCRouter|router)$"))
34
-
35
- (call_expression
36
- function: (member_expression
37
- object: (identifier) @_obj
38
- property: (property_identifier) @_prop (#eq? @_prop "router"))) @member_call
39
- `;
40
-
41
- /**
42
- * Procedure usage: `publicProcedure.input(...)` / `protectedProcedure.query(...)`.
43
- * Captures any identifier that ends in `Procedure`.
44
- */
45
- const PROCEDURE_QUERY = `
46
- (member_expression
47
- object: (identifier) @procedure_id (#match? @procedure_id "Procedure$"))
48
-
49
- (call_expression
50
- function: (identifier) @procedure_call (#match? @procedure_call "Procedure$"))
51
- `;
52
-
53
- // ============================================================
54
- // Adapter
55
- // ============================================================
56
-
57
- const KNOWN_BUILDERS = new Set(['createTRPCRouter', 'router']);
58
-
59
- export const nextjsTrpcAdapter: CodebaseAdapter = {
60
- id: 'nextjs-trpc',
61
- languages: ['typescript'],
62
-
63
- matches(signals: DetectionSignals): boolean {
64
- // package.json deps include @trpc/* OR there's a server/api/routers dir
65
- const pkgJson = signals.packageJson;
66
- if (pkgJson) {
67
- const deps = pkgJson.dependencies as Record<string, unknown> | undefined;
68
- const devDeps = pkgJson.devDependencies as Record<string, unknown> | undefined;
69
- const all = { ...(deps ?? {}), ...(devDeps ?? {}) };
70
- if (Object.keys(all).some(k => k.startsWith('@trpc/'))) return true;
71
- }
72
- if (signals.presentDirs.has('server')) return true;
73
- return false;
74
- },
75
-
76
- async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
77
- if (files.length === 0) {
78
- return { conventions: {}, provenance: [], confidence: 'none' };
79
- }
80
-
81
- let language;
82
- try {
83
- language = await loadGrammar('typescript');
84
- } catch {
85
- return { conventions: {}, provenance: [], confidence: 'none' };
86
- }
87
-
88
- const parser = new Parser();
89
- parser.setLanguage(language);
90
-
91
- const builders = new Map<string, { line: number; file: string }>();
92
- const procedures = new Map<string, { line: number; file: string }>();
93
-
94
- try {
95
- for (const file of files) {
96
- const skip = isParsableSource(file.content, file.size);
97
- if (skip) {
98
- process.stderr.write(
99
- `[massu/ast] WARN: nextjs-trpc skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
100
- );
101
- continue;
102
- }
103
- try {
104
- for (const hit of runQuery(parser, file.content, ROUTER_BUILDER_QUERY, 'trpc-router-builder', file.path)) {
105
- // Either capture group `builder_id` (direct call) or
106
- // `member_call` (the whole `t.router(...)` expression).
107
- const directId = hit.captures.builder_id;
108
- const memberCall = hit.captures.member_call;
109
- let label: string | null = null;
110
- if (directId && KNOWN_BUILDERS.has(directId)) {
111
- label = directId;
112
- } else if (memberCall) {
113
- // Normalize `t.router` text (member_call captures the whole call)
114
- // into the bare `t.router` form by extracting the leading
115
- // identifier.foo pattern.
116
- const m = memberCall.match(/([A-Za-z_$][A-Za-z0-9_$]*)\.router/);
117
- if (m) label = `${m[1]}.router`;
118
- }
119
- if (label && !builders.has(label)) {
120
- builders.set(label, { line: hit.line, file: file.path });
121
- }
122
- }
123
-
124
- for (const hit of runQuery(parser, file.content, PROCEDURE_QUERY, 'trpc-procedure', file.path)) {
125
- const proc = hit.captures.procedure_id ?? hit.captures.procedure_call;
126
- if (proc && !procedures.has(proc)) {
127
- procedures.set(proc, { 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 (builders.size > 0) {
143
- const [name, { line, file }] = builders.entries().next().value as [string, { line: number; file: string }];
144
- conventions.trpc_router_builder = name;
145
- provenance.push({ field: 'trpc_router_builder', sourceFile: file, line, query: 'trpc-router-builder' });
146
- }
147
- if (procedures.size > 0) {
148
- const [name, { line, file }] = procedures.entries().next().value as [string, { line: number; file: string }];
149
- conventions.procedure_pattern = name;
150
- provenance.push({ field: 'procedure_pattern', sourceFile: file, line, query: 'trpc-procedure' });
151
- }
152
-
153
- let confidence: AdapterResult['confidence'];
154
- if (Object.keys(conventions).length === 0) {
155
- confidence = 'none';
156
- } else if (builders.size === 1 && procedures.size <= 2) {
157
- confidence = 'high';
158
- } else if (builders.size > 1) {
159
- confidence = 'low';
160
- } else {
161
- confidence = 'medium';
162
- }
163
-
164
- return { conventions, provenance, confidence };
165
- },
166
- };
@@ -1,133 +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 3.5 (security audit): centralized AST parse-time safety.
6
- *
7
- * Adapters consume oversized / pathological user source. Tree-sitter is
8
- * fast but synchronous — there is no native timeout, no native size cap.
9
- * This module provides:
10
- *
11
- * 1. `MAX_AST_FILE_BYTES` — 1MB hard cap per file (covers DoS vector
12
- * "oversized file"). Plan §1 cited 5MB; we apply a tighter bound at
13
- * the adapter tier because adapter sample size is small (≤3 files
14
- * per adapter) and 1MB is already an order of magnitude beyond
15
- * reasonable convention-defining files.
16
- * 2. `MAX_AST_PARSE_DEPTH` — 5K-deep nested-structure rejection — a
17
- * static text scan for runaway open-paren / open-brace runs that
18
- * would push Tree-sitter into deep recursion. Cheap pre-check.
19
- * 3. `parseTimeout(ms, fn)` — wraps a synchronous Tree-sitter call in
20
- * a deadline guard. Tree-sitter's pure-JS path can't be interrupted
21
- * mid-parse, but we record the elapsed time AFTER the call returns
22
- * and emit a stderr warning when budget is exceeded so daemon ops
23
- * see the abuse signal.
24
- * 4. `isParsableSource(source)` — static gate combining size + depth.
25
- * Returns `null` if accepted, or a `{ reason, detail }` object when
26
- * rejected so the adapter can record provenance and skip the file.
27
- *
28
- * Library purity: never terminates the process, no DB calls, no network.
29
- * Pure helper module.
30
- */
31
-
32
- /** Hard size cap for an individual file fed to Tree-sitter. */
33
- export const MAX_AST_FILE_BYTES = 1 * 1024 * 1024;
34
-
35
- /**
36
- * Maximum nested-bracket depth allowed in a file. Pathological inputs
37
- * with 10K-deep nesting can push Tree-sitter into runaway recursion on
38
- * some grammar versions. Cheap O(n) text scan picks them off before parse.
39
- */
40
- export const MAX_AST_PARSE_DEPTH = 5000;
41
-
42
- /**
43
- * Per-file Tree-sitter parse budget (ms). Tree-sitter parses are
44
- * synchronous in JS, so this is enforced as a post-call elapsed-time
45
- * check rather than a hard timer. The check still serves the purpose of
46
- * giving operators visibility into adversarial files.
47
- */
48
- export const MAX_AST_PARSE_MS = 2000;
49
-
50
- export type ParseSkipReason =
51
- | 'size-cap'
52
- | 'depth-cap'
53
- | 'control-bytes'
54
- | 'utf8-validation';
55
-
56
- export interface ParseSkip {
57
- reason: ParseSkipReason;
58
- detail: string;
59
- }
60
-
61
- /**
62
- * Static safety gate. Call BEFORE invoking `parser.parse()`. Returns
63
- * `null` if the source is acceptable, or a `ParseSkip` describing why
64
- * the file is rejected.
65
- *
66
- * Cheap to evaluate — single linear scan + size check.
67
- */
68
- export function isParsableSource(source: string, sizeBytes?: number): ParseSkip | null {
69
- // Size cap: reject before Tree-sitter sees the bytes. We compute
70
- // byte-length conservatively when not provided.
71
- const bytes = sizeBytes ?? Buffer.byteLength(source, 'utf-8');
72
- if (bytes > MAX_AST_FILE_BYTES) {
73
- return {
74
- reason: 'size-cap',
75
- detail: `${bytes} bytes > ${MAX_AST_FILE_BYTES} cap`,
76
- };
77
- }
78
-
79
- // Depth cap: count maximal nesting depth across `(` `[` `{` runs.
80
- // Single-pass O(n) — no regex backtracking.
81
- let depth = 0;
82
- let maxDepth = 0;
83
- // Also reject NUL bytes (Tree-sitter handles them but they're a
84
- // canary for binary-file mislabeling).
85
- for (let i = 0; i < source.length; i++) {
86
- const c = source.charCodeAt(i);
87
- if (c === 0) {
88
- return { reason: 'control-bytes', detail: 'NUL byte at offset ' + i };
89
- }
90
- // 40 = '(', 91 = '[', 123 = '{'
91
- if (c === 40 || c === 91 || c === 123) {
92
- depth++;
93
- if (depth > maxDepth) maxDepth = depth;
94
- if (depth > MAX_AST_PARSE_DEPTH) {
95
- return {
96
- reason: 'depth-cap',
97
- detail: `nesting depth exceeded ${MAX_AST_PARSE_DEPTH}`,
98
- };
99
- }
100
- } else if (c === 41 || c === 93 || c === 125) {
101
- // Close brackets — clamp to 0 (mismatched code shouldn't crash this).
102
- depth = depth > 0 ? depth - 1 : 0;
103
- }
104
- }
105
-
106
- return null;
107
- }
108
-
109
- /**
110
- * Wrap a synchronous Tree-sitter call and emit a warning when the call
111
- * exceeds the budget. Returns the call's result regardless — callers may
112
- * decide to discard it based on `elapsed`.
113
- *
114
- * Note: Tree-sitter's WASM path has no co-operative cancellation, so
115
- * this is observability rather than a hard kill. The size + depth caps
116
- * are the load-bearing mitigations; this is the third belt.
117
- */
118
- export function withParseDeadline<T>(
119
- fn: () => T,
120
- filePath: string,
121
- budgetMs: number = MAX_AST_PARSE_MS,
122
- ): { value: T; elapsedMs: number; overBudget: boolean } {
123
- const start = Date.now();
124
- const value = fn();
125
- const elapsedMs = Date.now() - start;
126
- const overBudget = elapsedMs > budgetMs;
127
- if (overBudget) {
128
- process.stderr.write(
129
- `[massu/ast] WARN: parse of ${filePath} took ${elapsedMs}ms (budget ${budgetMs}ms) — file may be adversarial. (Phase 3.5 mitigation)\n`,
130
- );
131
- }
132
- return { value, elapsedMs, overBudget };
133
- }
@@ -1,4 +0,0 @@
1
- // Plan 3c Phase 9b P-A-005: re-export shim. Source-of-truth lives at
2
- // `packages/adapter-phoenix/src/index.ts` (workspace package). This shim
3
- // preserves the legacy import path used by codebase-introspector + tests.
4
- export { phoenixAdapter } from '@massu/adapter-phoenix';
@@ -1,208 +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: Django AST adapter.
6
- *
7
- * Extracts:
8
- * - mixin_classes: class-based-view base classes inheriting LoginRequiredMixin etc.
9
- * - decorator_usage: `@login_required` or `@permission_required` decorators
10
- * - urlpatterns_shape: 'function-views' | 'class-views' | 'mixed'
11
- *
12
- * Conservative gate: returns 'none' if no `INSTALLED_APPS` Django marker is
13
- * present in the signal bundle. Adapter wants HIGH precision: false-positive
14
- * Django classification would push regex fallback aside for projects that
15
- * don't actually use Django.
16
- */
17
-
18
- import { Parser } from 'web-tree-sitter';
19
- import type { CodebaseAdapter, AdapterResult, DetectionSignals, Provenance, SourceFile } from './types.ts';
20
- import { runQuery, InvalidQueryError } from './query-helpers.ts';
21
- import { loadGrammar } from './tree-sitter-loader.ts';
22
- import { isParsableSource, MAX_AST_FILE_BYTES } from './parse-guard.ts';
23
-
24
- // ============================================================
25
- // Queries
26
- // ============================================================
27
-
28
- /**
29
- * `@login_required` / `@permission_required` decorator on a function def.
30
- * Captures the decorator name (`login_required` or any `*_required`/`*_login`).
31
- */
32
- const DECORATOR_QUERY = `
33
- (decorator
34
- (identifier) @decorator_name)
35
- `;
36
-
37
- /**
38
- * Class definition with a base list — captures bases like `LoginRequiredMixin`,
39
- * `PermissionRequiredMixin`, `View`, `ListView`. The runner filters for the
40
- * Django-specific names.
41
- */
42
- const CLASS_BASE_QUERY = `
43
- (class_definition
44
- name: (identifier) @class_name
45
- superclasses: (argument_list
46
- (identifier) @base_name))
47
- `;
48
-
49
- /**
50
- * urlpatterns assignment — captures the rhs (a list) so we can inspect what's
51
- * inside (path() / re_path() calls indicate function-view style; class names
52
- * indicate class-view style).
53
- */
54
- const URLPATTERNS_QUERY = `
55
- (assignment
56
- left: (identifier) @_target (#eq? @_target "urlpatterns")
57
- right: (list) @urlpatterns_list)
58
- `;
59
-
60
- // ============================================================
61
- // Adapter
62
- // ============================================================
63
-
64
- const DJANGO_MIXIN_NAMES = new Set([
65
- 'LoginRequiredMixin',
66
- 'PermissionRequiredMixin',
67
- 'UserPassesTestMixin',
68
- 'StaffuserRequiredMixin',
69
- ]);
70
-
71
- const DJANGO_DECORATOR_PATTERNS = [
72
- /^login_required$/,
73
- /^permission_required$/,
74
- /_required$/,
75
- /^require_/,
76
- ];
77
-
78
- export const pythonDjangoAdapter: CodebaseAdapter = {
79
- id: 'python-django',
80
- languages: ['python'],
81
-
82
- matches(signals: DetectionSignals): boolean {
83
- // Conservative gate: require an explicit Django marker.
84
- // - manage.py at root, OR
85
- // - settings.py with INSTALLED_APPS reference, OR
86
- // - pyproject.toml dep on Django
87
- if (signals.presentFiles.has('manage.py')) return true;
88
- const pyToml = signals.pyprojectToml as { __raw?: string } | undefined;
89
- if (pyToml?.__raw && /\bdjango\b/i.test(pyToml.__raw)) return true;
90
- return false;
91
- },
92
-
93
- async introspect(files: SourceFile[], _rootDir: string): Promise<AdapterResult> {
94
- if (files.length === 0) {
95
- return { conventions: {}, provenance: [], confidence: 'none' };
96
- }
97
-
98
- let language;
99
- try {
100
- language = await loadGrammar('python');
101
- } catch {
102
- return { conventions: {}, provenance: [], confidence: 'none' };
103
- }
104
-
105
- const parser = new Parser();
106
- parser.setLanguage(language);
107
-
108
- const decoratorsFound = new Map<string, { line: number; file: string }>();
109
- const mixinsFound = new Map<string, { line: number; file: string }>();
110
- const urlpatternsShape: { value: 'function-views' | 'class-views' | 'mixed' | null; line: number; file: string } = {
111
- value: null,
112
- line: 0,
113
- file: '',
114
- };
115
-
116
- try {
117
- for (const file of files) {
118
- const skip = isParsableSource(file.content, file.size);
119
- if (skip) {
120
- process.stderr.write(
121
- `[massu/ast] WARN: python-django skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)\n`,
122
- );
123
- continue;
124
- }
125
- try {
126
- // Decorators
127
- for (const hit of runQuery(parser, file.content, DECORATOR_QUERY, 'django-decorator', file.path)) {
128
- const name = hit.captures.decorator_name;
129
- if (!name) continue;
130
- // Filter to Django-shaped decorator names only.
131
- if (DJANGO_DECORATOR_PATTERNS.some(re => re.test(name)) && !decoratorsFound.has(name)) {
132
- decoratorsFound.set(name, { line: hit.line, file: file.path });
133
- }
134
- }
135
- // Mixins
136
- for (const hit of runQuery(parser, file.content, CLASS_BASE_QUERY, 'django-mixin', file.path)) {
137
- const base = hit.captures.base_name;
138
- if (base && DJANGO_MIXIN_NAMES.has(base) && !mixinsFound.has(base)) {
139
- mixinsFound.set(base, { line: hit.line, file: file.path });
140
- }
141
- }
142
- // urlpatterns shape — naive heuristic: inspect the captured text
143
- for (const hit of runQuery(parser, file.content, URLPATTERNS_QUERY, 'django-urlpatterns', file.path)) {
144
- const listText = hit.captures.urlpatterns_list ?? '';
145
- const hasFunctionForm = /\bpath\s*\(/.test(listText) || /\bre_path\s*\(/.test(listText);
146
- const hasClassForm = /\.as_view\s*\(/.test(listText);
147
- let shape: 'function-views' | 'class-views' | 'mixed' | null = null;
148
- if (hasFunctionForm && hasClassForm) shape = 'mixed';
149
- else if (hasFunctionForm) shape = 'function-views';
150
- else if (hasClassForm) shape = 'class-views';
151
- if (shape && !urlpatternsShape.value) {
152
- urlpatternsShape.value = shape;
153
- urlpatternsShape.line = hit.line;
154
- urlpatternsShape.file = file.path;
155
- }
156
- }
157
- } catch (e) {
158
- if (e instanceof InvalidQueryError) throw e;
159
- continue;
160
- }
161
- }
162
- } finally {
163
- try { parser.delete(); } catch { /* ignore */ }
164
- }
165
-
166
- const conventions: Record<string, unknown> = {};
167
- const provenance: Provenance[] = [];
168
-
169
- if (decoratorsFound.size > 0) {
170
- const list = Array.from(decoratorsFound.keys());
171
- conventions.decorator_usage = list;
172
- const [first, { line, file }] = decoratorsFound.entries().next().value as [string, { line: number; file: string }];
173
- provenance.push({ field: 'decorator_usage', sourceFile: file, line, query: 'django-decorator' });
174
- // Also emit first decorator as auth_dep proxy for compatibility with
175
- // regex-fallback's behavior — auth_dep was the regex's primary output.
176
- conventions.auth_dep = first;
177
- provenance.push({ field: 'auth_dep', sourceFile: file, line, query: 'django-decorator' });
178
- }
179
-
180
- if (mixinsFound.size > 0) {
181
- const list = Array.from(mixinsFound.keys());
182
- conventions.mixin_classes = list;
183
- const [, { line, file }] = mixinsFound.entries().next().value as [string, { line: number; file: string }];
184
- provenance.push({ field: 'mixin_classes', sourceFile: file, line, query: 'django-mixin' });
185
- }
186
-
187
- if (urlpatternsShape.value) {
188
- conventions.urlpatterns_shape = urlpatternsShape.value;
189
- provenance.push({
190
- field: 'urlpatterns_shape',
191
- sourceFile: urlpatternsShape.file,
192
- line: urlpatternsShape.line,
193
- query: 'django-urlpatterns',
194
- });
195
- }
196
-
197
- let confidence: AdapterResult['confidence'];
198
- if (Object.keys(conventions).length === 0) {
199
- confidence = 'none';
200
- } else if (decoratorsFound.size > 0 || mixinsFound.size > 0) {
201
- confidence = 'high';
202
- } else {
203
- confidence = 'medium';
204
- }
205
-
206
- return { conventions, provenance, confidence };
207
- },
208
- };