@massu/core 1.5.4 → 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.
package/dist/cli.js CHANGED
@@ -15717,6 +15717,924 @@ var init_swift_swiftui = __esm({
15717
15717
  }
15718
15718
  });
15719
15719
 
15720
+ // src/detect/adapters/python-flask.ts
15721
+ function extractPrefixBase3(prefix3) {
15722
+ if (!prefix3.startsWith("/")) return null;
15723
+ const stripped = prefix3.replace(/^\/+/, "");
15724
+ const firstSeg = stripped.split("/")[0];
15725
+ if (!firstSeg) return null;
15726
+ return "/" + firstSeg;
15727
+ }
15728
+ var AUTH_DECORATOR_QUERY, BLUEPRINT_URL_PREFIX_QUERY, APP_FACTORY_QUERY, pythonFlaskAdapter;
15729
+ var init_python_flask = __esm({
15730
+ "src/detect/adapters/python-flask.ts"() {
15731
+ "use strict";
15732
+ init_tree_sitter();
15733
+ init_query_helpers();
15734
+ init_tree_sitter_loader();
15735
+ init_parse_guard();
15736
+ AUTH_DECORATOR_QUERY = `
15737
+ (decorator
15738
+ (identifier) @auth_decorator (#match? @auth_decorator "_required$"))
15739
+ `;
15740
+ BLUEPRINT_URL_PREFIX_QUERY = `
15741
+ (call
15742
+ function: (identifier) @_callee (#eq? @_callee "Blueprint")
15743
+ arguments: (argument_list
15744
+ (keyword_argument
15745
+ name: (identifier) @_kw (#eq? @_kw "url_prefix")
15746
+ value: (string) @url_prefix)))
15747
+ `;
15748
+ APP_FACTORY_QUERY = `
15749
+ (function_definition
15750
+ name: (identifier) @factory_name (#match? @factory_name "^create_")
15751
+ body: (block
15752
+ (expression_statement
15753
+ (assignment
15754
+ right: (call
15755
+ function: (identifier) @_flask_call (#eq? @_flask_call "Flask"))))))
15756
+ `;
15757
+ pythonFlaskAdapter = {
15758
+ id: "python-flask",
15759
+ languages: ["python"],
15760
+ matches(signals) {
15761
+ const pyToml = signals.pyprojectToml;
15762
+ if (pyToml?.__raw && /\bflask\b/i.test(pyToml.__raw)) return true;
15763
+ if (signals.presentDirs.has("app") && signals.presentFiles.has("app.py")) return true;
15764
+ if (signals.presentDirs.has("app") && signals.presentFiles.has("wsgi.py")) return true;
15765
+ return false;
15766
+ },
15767
+ async introspect(files, _rootDir) {
15768
+ if (files.length === 0) {
15769
+ return { conventions: {}, provenance: [], confidence: "none" };
15770
+ }
15771
+ let language;
15772
+ try {
15773
+ language = await loadGrammar("python");
15774
+ } catch (e2) {
15775
+ return { conventions: {}, provenance: [], confidence: "none" };
15776
+ }
15777
+ const parser = new Parser();
15778
+ parser.setLanguage(language);
15779
+ const authDecorators = /* @__PURE__ */ new Map();
15780
+ const urlPrefixes = /* @__PURE__ */ new Map();
15781
+ const appFactories = /* @__PURE__ */ new Map();
15782
+ try {
15783
+ for (const file of files) {
15784
+ const skip = isParsableSource(file.content, file.size);
15785
+ if (skip) {
15786
+ process.stderr.write(
15787
+ `[massu/ast] WARN: python-flask skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
15788
+ `
15789
+ );
15790
+ continue;
15791
+ }
15792
+ try {
15793
+ for (const hit of runQuery(parser, file.content, AUTH_DECORATOR_QUERY, "flask-auth-decorator", file.path)) {
15794
+ const name2 = hit.captures.auth_decorator;
15795
+ if (name2 && !authDecorators.has(name2)) {
15796
+ authDecorators.set(name2, { line: hit.line, file: file.path });
15797
+ }
15798
+ }
15799
+ for (const hit of runQuery(parser, file.content, BLUEPRINT_URL_PREFIX_QUERY, "flask-blueprint-url-prefix", file.path)) {
15800
+ const raw = hit.captures.url_prefix;
15801
+ if (!raw) continue;
15802
+ const literal = raw.replace(/^['"]/, "").replace(/['"]$/, "");
15803
+ const base = extractPrefixBase3(literal);
15804
+ if (base && !urlPrefixes.has(base)) {
15805
+ urlPrefixes.set(base, { line: hit.line, file: file.path });
15806
+ }
15807
+ }
15808
+ for (const hit of runQuery(parser, file.content, APP_FACTORY_QUERY, "flask-app-factory", file.path)) {
15809
+ const name2 = hit.captures.factory_name;
15810
+ if (name2 && !appFactories.has(name2)) {
15811
+ appFactories.set(name2, { line: hit.line, file: file.path });
15812
+ }
15813
+ }
15814
+ } catch (e2) {
15815
+ if (e2 instanceof InvalidQueryError) {
15816
+ throw e2;
15817
+ }
15818
+ continue;
15819
+ }
15820
+ }
15821
+ } finally {
15822
+ try {
15823
+ parser.delete();
15824
+ } catch {
15825
+ }
15826
+ }
15827
+ const conventions = {};
15828
+ const provenance = [];
15829
+ if (authDecorators.size === 1) {
15830
+ const [name2, { line, file }] = authDecorators.entries().next().value;
15831
+ conventions.auth_decorator = name2;
15832
+ provenance.push({ field: "auth_decorator", sourceFile: file, line, query: "flask-auth-decorator" });
15833
+ } else if (authDecorators.size >= 2) {
15834
+ const [name2, { line, file }] = authDecorators.entries().next().value;
15835
+ conventions.auth_decorator = name2;
15836
+ provenance.push({ field: "auth_decorator", sourceFile: file, line, query: "flask-auth-decorator" });
15837
+ }
15838
+ if (urlPrefixes.size >= 1) {
15839
+ const [base, { line, file }] = urlPrefixes.entries().next().value;
15840
+ conventions.blueprint_url_prefix = base;
15841
+ provenance.push({ field: "blueprint_url_prefix", sourceFile: file, line, query: "flask-blueprint-url-prefix" });
15842
+ }
15843
+ if (appFactories.size >= 1) {
15844
+ const [name2, { line, file }] = appFactories.entries().next().value;
15845
+ conventions.app_factory = name2;
15846
+ provenance.push({ field: "app_factory", sourceFile: file, line, query: "flask-app-factory" });
15847
+ }
15848
+ let confidence;
15849
+ if (Object.keys(conventions).length === 0) {
15850
+ confidence = "none";
15851
+ } else if (authDecorators.size === 1) {
15852
+ confidence = "high";
15853
+ } else if (authDecorators.size >= 2) {
15854
+ confidence = "low";
15855
+ } else {
15856
+ confidence = "medium";
15857
+ }
15858
+ return { conventions, provenance, confidence };
15859
+ }
15860
+ };
15861
+ }
15862
+ });
15863
+
15864
+ // src/detect/adapters/go-chi.ts
15865
+ function extractPrefixBase4(prefix3) {
15866
+ if (!prefix3.startsWith("/")) return null;
15867
+ const stripped = prefix3.replace(/^\/+/, "");
15868
+ const firstSeg = stripped.split("/")[0];
15869
+ if (!firstSeg) return null;
15870
+ return "/" + firstSeg;
15871
+ }
15872
+ var ROUTE_METHOD_QUERY, MOUNT_PREFIX_QUERY, MIDDLEWARE_USE_QUERY, goChiAdapter;
15873
+ var init_go_chi = __esm({
15874
+ "src/detect/adapters/go-chi.ts"() {
15875
+ "use strict";
15876
+ init_tree_sitter();
15877
+ init_query_helpers();
15878
+ init_tree_sitter_loader();
15879
+ init_parse_guard();
15880
+ ROUTE_METHOD_QUERY = `
15881
+ (call_expression
15882
+ function: (selector_expression
15883
+ field: (field_identifier) @method (#match? @method "^(Get|Post|Put|Delete|Patch|Head|Options|Connect|Trace)$"))
15884
+ arguments: (argument_list
15885
+ .
15886
+ (interpreted_string_literal) @route_path))
15887
+ `;
15888
+ MOUNT_PREFIX_QUERY = `
15889
+ (call_expression
15890
+ function: (selector_expression
15891
+ field: (field_identifier) @_field (#eq? @_field "Mount"))
15892
+ arguments: (argument_list
15893
+ .
15894
+ (interpreted_string_literal) @mount_path))
15895
+ `;
15896
+ MIDDLEWARE_USE_QUERY = `
15897
+ (call_expression
15898
+ function: (selector_expression
15899
+ field: (field_identifier) @_use (#eq? @_use "Use"))
15900
+ arguments: (argument_list
15901
+ .
15902
+ (selector_expression
15903
+ operand: (identifier) @_pkg (#eq? @_pkg "middleware")
15904
+ field: (field_identifier) @middleware_name)))
15905
+ `;
15906
+ goChiAdapter = {
15907
+ id: "go-chi",
15908
+ languages: ["go"],
15909
+ matches(signals) {
15910
+ if (!signals.goMod) return false;
15911
+ if (/github\.com\/go-chi\/chi/i.test(signals.goMod)) return true;
15912
+ return false;
15913
+ },
15914
+ async introspect(files, _rootDir) {
15915
+ if (files.length === 0) {
15916
+ return { conventions: {}, provenance: [], confidence: "none" };
15917
+ }
15918
+ let language;
15919
+ try {
15920
+ language = await loadGrammar("go");
15921
+ } catch (e2) {
15922
+ return { conventions: {}, provenance: [], confidence: "none" };
15923
+ }
15924
+ const parser = new Parser();
15925
+ parser.setLanguage(language);
15926
+ const routeMethods = /* @__PURE__ */ new Map();
15927
+ const mountBases = /* @__PURE__ */ new Map();
15928
+ const middlewareNames = /* @__PURE__ */ new Map();
15929
+ try {
15930
+ for (const file of files) {
15931
+ const skip = isParsableSource(file.content, file.size);
15932
+ if (skip) {
15933
+ process.stderr.write(
15934
+ `[massu/ast] WARN: go-chi skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
15935
+ `
15936
+ );
15937
+ continue;
15938
+ }
15939
+ try {
15940
+ for (const hit of runQuery(parser, file.content, ROUTE_METHOD_QUERY, "chi-route-method", file.path)) {
15941
+ const method = hit.captures.method;
15942
+ if (method && !routeMethods.has(method)) {
15943
+ routeMethods.set(method, { line: hit.line, file: file.path });
15944
+ }
15945
+ }
15946
+ for (const hit of runQuery(parser, file.content, MOUNT_PREFIX_QUERY, "chi-mount-prefix", file.path)) {
15947
+ const raw = hit.captures.mount_path;
15948
+ if (!raw) continue;
15949
+ const literal = raw.replace(/^["`]/, "").replace(/["`]$/, "");
15950
+ const base = extractPrefixBase4(literal);
15951
+ if (base && !mountBases.has(base)) {
15952
+ mountBases.set(base, { line: hit.line, file: file.path });
15953
+ }
15954
+ }
15955
+ for (const hit of runQuery(parser, file.content, MIDDLEWARE_USE_QUERY, "chi-middleware-use", file.path)) {
15956
+ const name2 = hit.captures.middleware_name;
15957
+ if (name2 && !middlewareNames.has(name2)) {
15958
+ middlewareNames.set(name2, { line: hit.line, file: file.path });
15959
+ }
15960
+ }
15961
+ } catch (e2) {
15962
+ if (e2 instanceof InvalidQueryError) {
15963
+ throw e2;
15964
+ }
15965
+ continue;
15966
+ }
15967
+ }
15968
+ } finally {
15969
+ try {
15970
+ parser.delete();
15971
+ } catch {
15972
+ }
15973
+ }
15974
+ const conventions = {};
15975
+ const provenance = [];
15976
+ if (routeMethods.size === 1) {
15977
+ const [name2, { line, file }] = routeMethods.entries().next().value;
15978
+ conventions.route_method = name2;
15979
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "chi-route-method" });
15980
+ } else if (routeMethods.size >= 2) {
15981
+ const [name2, { line, file }] = routeMethods.entries().next().value;
15982
+ conventions.route_method = name2;
15983
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "chi-route-method" });
15984
+ }
15985
+ if (mountBases.size >= 1) {
15986
+ const [base, { line, file }] = mountBases.entries().next().value;
15987
+ conventions.mount_prefix_base = base;
15988
+ provenance.push({ field: "mount_prefix_base", sourceFile: file, line, query: "chi-mount-prefix" });
15989
+ }
15990
+ if (middlewareNames.size >= 1) {
15991
+ const [name2, { line, file }] = middlewareNames.entries().next().value;
15992
+ conventions.middleware_name = name2;
15993
+ provenance.push({ field: "middleware_name", sourceFile: file, line, query: "chi-middleware-use" });
15994
+ }
15995
+ let confidence;
15996
+ if (Object.keys(conventions).length === 0) {
15997
+ confidence = "none";
15998
+ } else if (routeMethods.size === 1) {
15999
+ confidence = "high";
16000
+ } else if (routeMethods.size >= 2) {
16001
+ confidence = "low";
16002
+ } else {
16003
+ confidence = "medium";
16004
+ }
16005
+ return { conventions, provenance, confidence };
16006
+ }
16007
+ };
16008
+ }
16009
+ });
16010
+
16011
+ // src/detect/adapters/rails.ts
16012
+ function extractRootController(target) {
16013
+ const idx = target.indexOf("#");
16014
+ if (idx <= 0) return null;
16015
+ const controller = target.slice(0, idx).trim();
16016
+ return controller || null;
16017
+ }
16018
+ var ROUTE_METHOD_QUERY2, NAMESPACE_QUERY, ROOT_ROUTE_QUERY, railsAdapter;
16019
+ var init_rails = __esm({
16020
+ "src/detect/adapters/rails.ts"() {
16021
+ "use strict";
16022
+ init_tree_sitter();
16023
+ init_query_helpers();
16024
+ init_tree_sitter_loader();
16025
+ init_parse_guard();
16026
+ ROUTE_METHOD_QUERY2 = `
16027
+ (call
16028
+ method: (identifier) @method (#match? @method "^(get|post|put|patch|delete|options|head)$")
16029
+ arguments: (argument_list
16030
+ .
16031
+ (string) @route_path))
16032
+ `;
16033
+ NAMESPACE_QUERY = `
16034
+ (call
16035
+ method: (identifier) @_method (#eq? @_method "namespace")
16036
+ arguments: (argument_list
16037
+ .
16038
+ [
16039
+ (simple_symbol) @namespace_symbol
16040
+ (string) @namespace_string
16041
+ ]))
16042
+ `;
16043
+ ROOT_ROUTE_QUERY = `
16044
+ (call
16045
+ method: (identifier) @_method (#eq? @_method "root")
16046
+ arguments: (argument_list
16047
+ .
16048
+ (string) @root_target))
16049
+
16050
+ (call
16051
+ method: (identifier) @_method (#eq? @_method "root")
16052
+ arguments: (argument_list
16053
+ (pair
16054
+ key: (hash_key_symbol) @_key (#eq? @_key "to")
16055
+ value: (string) @root_target)))
16056
+ `;
16057
+ railsAdapter = {
16058
+ id: "rails",
16059
+ languages: ["ruby"],
16060
+ matches(signals) {
16061
+ if (!signals.gemfile) return false;
16062
+ return /^\s*gem\s+['"]rails['"]/im.test(signals.gemfile);
16063
+ },
16064
+ async introspect(files, _rootDir) {
16065
+ if (files.length === 0) {
16066
+ return { conventions: {}, provenance: [], confidence: "none" };
16067
+ }
16068
+ let language;
16069
+ try {
16070
+ language = await loadGrammar("ruby");
16071
+ } catch (e2) {
16072
+ return { conventions: {}, provenance: [], confidence: "none" };
16073
+ }
16074
+ const parser = new Parser();
16075
+ parser.setLanguage(language);
16076
+ const routeMethods = /* @__PURE__ */ new Map();
16077
+ const namespaces = /* @__PURE__ */ new Map();
16078
+ const rootControllers = /* @__PURE__ */ new Map();
16079
+ try {
16080
+ for (const file of files) {
16081
+ const skip = isParsableSource(file.content, file.size);
16082
+ if (skip) {
16083
+ process.stderr.write(
16084
+ `[massu/ast] WARN: rails skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
16085
+ `
16086
+ );
16087
+ continue;
16088
+ }
16089
+ try {
16090
+ for (const hit of runQuery(parser, file.content, ROUTE_METHOD_QUERY2, "rails-route-method", file.path)) {
16091
+ const method = hit.captures.method;
16092
+ if (method && !routeMethods.has(method)) {
16093
+ routeMethods.set(method, { line: hit.line, file: file.path });
16094
+ }
16095
+ }
16096
+ for (const hit of runQuery(parser, file.content, NAMESPACE_QUERY, "rails-namespace", file.path)) {
16097
+ const symbolRaw = hit.captures.namespace_symbol;
16098
+ const stringRaw = hit.captures.namespace_string;
16099
+ const name2 = symbolRaw ? symbolRaw.replace(/^:/, "") : stringRaw ? stringRaw.replace(/^['"]/, "").replace(/['"]$/, "") : null;
16100
+ if (!name2) continue;
16101
+ const path = "/" + name2;
16102
+ if (!namespaces.has(path)) {
16103
+ namespaces.set(path, { line: hit.line, file: file.path });
16104
+ }
16105
+ }
16106
+ for (const hit of runQuery(parser, file.content, ROOT_ROUTE_QUERY, "rails-root", file.path)) {
16107
+ const raw = hit.captures.root_target;
16108
+ if (!raw) continue;
16109
+ const literal = raw.replace(/^['"]/, "").replace(/['"]$/, "");
16110
+ const controller = extractRootController(literal);
16111
+ if (controller && !rootControllers.has(controller)) {
16112
+ rootControllers.set(controller, { line: hit.line, file: file.path });
16113
+ }
16114
+ }
16115
+ } catch (e2) {
16116
+ if (e2 instanceof InvalidQueryError) {
16117
+ throw e2;
16118
+ }
16119
+ continue;
16120
+ }
16121
+ }
16122
+ } finally {
16123
+ try {
16124
+ parser.delete();
16125
+ } catch {
16126
+ }
16127
+ }
16128
+ const conventions = {};
16129
+ const provenance = [];
16130
+ if (routeMethods.size === 1) {
16131
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16132
+ conventions.route_method = name2;
16133
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "rails-route-method" });
16134
+ } else if (routeMethods.size >= 2) {
16135
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16136
+ conventions.route_method = name2;
16137
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "rails-route-method" });
16138
+ }
16139
+ if (namespaces.size >= 1) {
16140
+ const [path, { line, file }] = namespaces.entries().next().value;
16141
+ conventions.api_namespace = path;
16142
+ provenance.push({ field: "api_namespace", sourceFile: file, line, query: "rails-namespace" });
16143
+ }
16144
+ if (rootControllers.size >= 1) {
16145
+ const [name2, { line, file }] = rootControllers.entries().next().value;
16146
+ conventions.root_controller = name2;
16147
+ provenance.push({ field: "root_controller", sourceFile: file, line, query: "rails-root" });
16148
+ }
16149
+ let confidence;
16150
+ if (Object.keys(conventions).length === 0) {
16151
+ confidence = "none";
16152
+ } else if (routeMethods.size === 1) {
16153
+ confidence = "high";
16154
+ } else if (routeMethods.size >= 2) {
16155
+ confidence = "low";
16156
+ } else {
16157
+ confidence = "medium";
16158
+ }
16159
+ return { conventions, provenance, confidence };
16160
+ }
16161
+ };
16162
+ }
16163
+ });
16164
+
16165
+ // src/detect/adapters/phoenix.ts
16166
+ function extractPrefixBase5(prefix3) {
16167
+ if (!prefix3.startsWith("/")) return null;
16168
+ const stripped = prefix3.replace(/^\/+/, "");
16169
+ const firstSeg = stripped.split("/")[0];
16170
+ if (!firstSeg) return null;
16171
+ return "/" + firstSeg;
16172
+ }
16173
+ var ROUTE_METHOD_QUERY3, SCOPE_PATH_QUERY, ROUTER_MODULE_QUERY, phoenixAdapter;
16174
+ var init_phoenix = __esm({
16175
+ "src/detect/adapters/phoenix.ts"() {
16176
+ "use strict";
16177
+ init_tree_sitter();
16178
+ init_query_helpers();
16179
+ init_tree_sitter_loader();
16180
+ init_parse_guard();
16181
+ ROUTE_METHOD_QUERY3 = `
16182
+ (call
16183
+ (identifier) @method (#match? @method "^(get|post|put|patch|delete|options|head)$")
16184
+ (arguments
16185
+ .
16186
+ (string) @route_path))
16187
+ `;
16188
+ SCOPE_PATH_QUERY = `
16189
+ (call
16190
+ (identifier) @_method (#eq? @_method "scope")
16191
+ (arguments
16192
+ .
16193
+ (string) @scope_path)
16194
+ (do_block))
16195
+ `;
16196
+ ROUTER_MODULE_QUERY = `
16197
+ (call
16198
+ (identifier) @_method (#eq? @_method "defmodule")
16199
+ (arguments
16200
+ .
16201
+ (alias) @module_name (#match? @module_name "Router$"))
16202
+ (do_block))
16203
+ `;
16204
+ phoenixAdapter = {
16205
+ id: "phoenix",
16206
+ languages: ["elixir"],
16207
+ matches(signals) {
16208
+ if (!signals.mixExs) return false;
16209
+ return /\{\s*:phoenix\b(?!_)/.test(signals.mixExs);
16210
+ },
16211
+ async introspect(files, _rootDir) {
16212
+ if (files.length === 0) {
16213
+ return { conventions: {}, provenance: [], confidence: "none" };
16214
+ }
16215
+ let language;
16216
+ try {
16217
+ language = await loadGrammar("elixir");
16218
+ } catch (e2) {
16219
+ return { conventions: {}, provenance: [], confidence: "none" };
16220
+ }
16221
+ const parser = new Parser();
16222
+ parser.setLanguage(language);
16223
+ const routeMethods = /* @__PURE__ */ new Map();
16224
+ const scopePaths = /* @__PURE__ */ new Map();
16225
+ const routerModules = /* @__PURE__ */ new Map();
16226
+ try {
16227
+ for (const file of files) {
16228
+ const skip = isParsableSource(file.content, file.size);
16229
+ if (skip) {
16230
+ process.stderr.write(
16231
+ `[massu/ast] WARN: phoenix skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
16232
+ `
16233
+ );
16234
+ continue;
16235
+ }
16236
+ try {
16237
+ for (const hit of runQuery(parser, file.content, ROUTE_METHOD_QUERY3, "phoenix-route-method", file.path)) {
16238
+ const method = hit.captures.method;
16239
+ if (method && !routeMethods.has(method)) {
16240
+ routeMethods.set(method, { line: hit.line, file: file.path });
16241
+ }
16242
+ }
16243
+ for (const hit of runQuery(parser, file.content, SCOPE_PATH_QUERY, "phoenix-scope-path", file.path)) {
16244
+ const raw = hit.captures.scope_path;
16245
+ if (!raw) continue;
16246
+ const literal = raw.replace(/^["']/, "").replace(/["']$/, "");
16247
+ const base = extractPrefixBase5(literal);
16248
+ if (base && !scopePaths.has(base)) {
16249
+ scopePaths.set(base, { line: hit.line, file: file.path });
16250
+ }
16251
+ }
16252
+ for (const hit of runQuery(parser, file.content, ROUTER_MODULE_QUERY, "phoenix-router-module", file.path)) {
16253
+ const name2 = hit.captures.module_name;
16254
+ if (name2 && !routerModules.has(name2)) {
16255
+ routerModules.set(name2, { line: hit.line, file: file.path });
16256
+ }
16257
+ }
16258
+ } catch (e2) {
16259
+ if (e2 instanceof InvalidQueryError) {
16260
+ throw e2;
16261
+ }
16262
+ continue;
16263
+ }
16264
+ }
16265
+ } finally {
16266
+ try {
16267
+ parser.delete();
16268
+ } catch {
16269
+ }
16270
+ }
16271
+ const conventions = {};
16272
+ const provenance = [];
16273
+ if (routeMethods.size === 1) {
16274
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16275
+ conventions.route_method = name2;
16276
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "phoenix-route-method" });
16277
+ } else if (routeMethods.size >= 2) {
16278
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16279
+ conventions.route_method = name2;
16280
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "phoenix-route-method" });
16281
+ }
16282
+ if (scopePaths.size >= 1) {
16283
+ const [base, { line, file }] = scopePaths.entries().next().value;
16284
+ conventions.scope_prefix_base = base;
16285
+ provenance.push({ field: "scope_prefix_base", sourceFile: file, line, query: "phoenix-scope-path" });
16286
+ }
16287
+ if (routerModules.size >= 1) {
16288
+ const [name2, { line, file }] = routerModules.entries().next().value;
16289
+ conventions.router_module = name2;
16290
+ provenance.push({ field: "router_module", sourceFile: file, line, query: "phoenix-router-module" });
16291
+ }
16292
+ let confidence;
16293
+ if (Object.keys(conventions).length === 0) {
16294
+ confidence = "none";
16295
+ } else if (routeMethods.size === 1) {
16296
+ confidence = "high";
16297
+ } else if (routeMethods.size >= 2) {
16298
+ confidence = "low";
16299
+ } else {
16300
+ confidence = "medium";
16301
+ }
16302
+ return { conventions, provenance, confidence };
16303
+ }
16304
+ };
16305
+ }
16306
+ });
16307
+
16308
+ // src/detect/adapters/aspnet.ts
16309
+ function extractPrefixBase6(prefix3) {
16310
+ const stripped = prefix3.replace(/^\/+/, "");
16311
+ const firstSeg = stripped.split("/")[0];
16312
+ if (!firstSeg) return null;
16313
+ return "/" + firstSeg;
16314
+ }
16315
+ var MAP_VERB_QUERY, HTTP_ATTR_QUERY, ROUTE_ATTR_QUERY, CONTROLLER_CLASS_QUERY, aspnetAdapter;
16316
+ var init_aspnet = __esm({
16317
+ "src/detect/adapters/aspnet.ts"() {
16318
+ "use strict";
16319
+ init_tree_sitter();
16320
+ init_query_helpers();
16321
+ init_tree_sitter_loader();
16322
+ init_parse_guard();
16323
+ MAP_VERB_QUERY = `
16324
+ (invocation_expression
16325
+ function: (member_access_expression
16326
+ name: (identifier) @method (#match? @method "^Map(Get|Post|Put|Patch|Delete|Head|Options)$"))
16327
+ arguments: (argument_list
16328
+ .
16329
+ (argument (string_literal) @route_path)))
16330
+ `;
16331
+ HTTP_ATTR_QUERY = `
16332
+ (attribute
16333
+ name: (identifier) @attr_name (#match? @attr_name "^Http(Get|Post|Put|Patch|Delete|Head|Options)$"))
16334
+ `;
16335
+ ROUTE_ATTR_QUERY = `
16336
+ (attribute
16337
+ name: (identifier) @_attr_name (#eq? @_attr_name "Route")
16338
+ (attribute_argument_list
16339
+ (attribute_argument (string_literal) @route_template)))
16340
+ `;
16341
+ CONTROLLER_CLASS_QUERY = `
16342
+ (class_declaration
16343
+ name: (identifier) @class_name (#match? @class_name "Controller$"))
16344
+ `;
16345
+ aspnetAdapter = {
16346
+ id: "aspnet",
16347
+ languages: ["csharp"],
16348
+ matches(signals) {
16349
+ if (!signals.csproj) return false;
16350
+ if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(signals.csproj)) return true;
16351
+ if (/Microsoft\.AspNetCore\.App/i.test(signals.csproj)) return true;
16352
+ return false;
16353
+ },
16354
+ async introspect(files, _rootDir) {
16355
+ if (files.length === 0) {
16356
+ return { conventions: {}, provenance: [], confidence: "none" };
16357
+ }
16358
+ let language;
16359
+ try {
16360
+ language = await loadGrammar("csharp");
16361
+ } catch (e2) {
16362
+ return { conventions: {}, provenance: [], confidence: "none" };
16363
+ }
16364
+ const parser = new Parser();
16365
+ parser.setLanguage(language);
16366
+ const routeMethods = /* @__PURE__ */ new Map();
16367
+ const prefixBases = /* @__PURE__ */ new Map();
16368
+ const controllerClasses = /* @__PURE__ */ new Map();
16369
+ try {
16370
+ for (const file of files) {
16371
+ const skip = isParsableSource(file.content, file.size);
16372
+ if (skip) {
16373
+ process.stderr.write(
16374
+ `[massu/ast] WARN: aspnet skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
16375
+ `
16376
+ );
16377
+ continue;
16378
+ }
16379
+ try {
16380
+ for (const hit of runQuery(parser, file.content, MAP_VERB_QUERY, "aspnet-map-verb", file.path)) {
16381
+ const methodRaw = hit.captures.method;
16382
+ if (!methodRaw) continue;
16383
+ const verb = methodRaw.replace(/^Map/, "");
16384
+ if (!routeMethods.has(verb)) {
16385
+ routeMethods.set(verb, { line: hit.line, file: file.path });
16386
+ }
16387
+ const pathRaw = hit.captures.route_path;
16388
+ if (pathRaw) {
16389
+ const literal = pathRaw.replace(/^["']/, "").replace(/["']$/, "");
16390
+ const base = extractPrefixBase6(literal);
16391
+ if (base && !prefixBases.has(base)) {
16392
+ prefixBases.set(base, { line: hit.line, file: file.path });
16393
+ }
16394
+ }
16395
+ }
16396
+ for (const hit of runQuery(parser, file.content, HTTP_ATTR_QUERY, "aspnet-http-attr", file.path)) {
16397
+ const attrRaw = hit.captures.attr_name;
16398
+ if (!attrRaw) continue;
16399
+ const verb = attrRaw.replace(/^Http/, "");
16400
+ if (!routeMethods.has(verb)) {
16401
+ routeMethods.set(verb, { line: hit.line, file: file.path });
16402
+ }
16403
+ }
16404
+ for (const hit of runQuery(parser, file.content, ROUTE_ATTR_QUERY, "aspnet-route-attr", file.path)) {
16405
+ const tplRaw = hit.captures.route_template;
16406
+ if (!tplRaw) continue;
16407
+ const literal = tplRaw.replace(/^["']/, "").replace(/["']$/, "");
16408
+ const base = extractPrefixBase6(literal);
16409
+ if (base && !prefixBases.has(base)) {
16410
+ prefixBases.set(base, { line: hit.line, file: file.path });
16411
+ }
16412
+ }
16413
+ for (const hit of runQuery(parser, file.content, CONTROLLER_CLASS_QUERY, "aspnet-controller-class", file.path)) {
16414
+ const name2 = hit.captures.class_name;
16415
+ if (name2 && !controllerClasses.has(name2)) {
16416
+ controllerClasses.set(name2, { line: hit.line, file: file.path });
16417
+ }
16418
+ }
16419
+ } catch (e2) {
16420
+ if (e2 instanceof InvalidQueryError) {
16421
+ throw e2;
16422
+ }
16423
+ continue;
16424
+ }
16425
+ }
16426
+ } finally {
16427
+ try {
16428
+ parser.delete();
16429
+ } catch {
16430
+ }
16431
+ }
16432
+ const conventions = {};
16433
+ const provenance = [];
16434
+ if (routeMethods.size === 1) {
16435
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16436
+ conventions.route_method = name2;
16437
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "aspnet-map-verb" });
16438
+ } else if (routeMethods.size >= 2) {
16439
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16440
+ conventions.route_method = name2;
16441
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "aspnet-map-verb" });
16442
+ }
16443
+ if (prefixBases.size >= 1) {
16444
+ const [base, { line, file }] = prefixBases.entries().next().value;
16445
+ conventions.route_prefix_base = base;
16446
+ provenance.push({ field: "route_prefix_base", sourceFile: file, line, query: "aspnet-route-prefix" });
16447
+ }
16448
+ if (controllerClasses.size >= 1) {
16449
+ const [name2, { line, file }] = controllerClasses.entries().next().value;
16450
+ conventions.controller_class = name2;
16451
+ provenance.push({ field: "controller_class", sourceFile: file, line, query: "aspnet-controller-class" });
16452
+ }
16453
+ let confidence;
16454
+ if (Object.keys(conventions).length === 0) {
16455
+ confidence = "none";
16456
+ } else if (routeMethods.size === 1) {
16457
+ confidence = "high";
16458
+ } else if (routeMethods.size >= 2) {
16459
+ confidence = "low";
16460
+ } else {
16461
+ confidence = "medium";
16462
+ }
16463
+ return { conventions, provenance, confidence };
16464
+ }
16465
+ };
16466
+ }
16467
+ });
16468
+
16469
+ // src/detect/adapters/spring.ts
16470
+ function extractPrefixBase7(prefix3) {
16471
+ const stripped = prefix3.replace(/^\/+/, "");
16472
+ const firstSeg = stripped.split("/")[0];
16473
+ if (!firstSeg) return null;
16474
+ return "/" + firstSeg;
16475
+ }
16476
+ var HTTP_MAPPING_QUERY, HTTP_MAPPING_NO_ARGS_QUERY, REQUEST_MAPPING_QUERY, CONTROLLER_CLASS_QUERY2, springAdapter;
16477
+ var init_spring = __esm({
16478
+ "src/detect/adapters/spring.ts"() {
16479
+ "use strict";
16480
+ init_tree_sitter();
16481
+ init_query_helpers();
16482
+ init_tree_sitter_loader();
16483
+ init_parse_guard();
16484
+ HTTP_MAPPING_QUERY = `
16485
+ (annotation
16486
+ name: (identifier) @method (#match? @method "^(Get|Post|Put|Patch|Delete|Head|Options)Mapping$")
16487
+ arguments: (annotation_argument_list
16488
+ (string_literal) @route_path))
16489
+ `;
16490
+ HTTP_MAPPING_NO_ARGS_QUERY = `
16491
+ (marker_annotation
16492
+ name: (identifier) @method (#match? @method "^(Get|Post|Put|Patch|Delete|Head|Options)Mapping$"))
16493
+ `;
16494
+ REQUEST_MAPPING_QUERY = `
16495
+ (annotation
16496
+ name: (identifier) @_name (#eq? @_name "RequestMapping")
16497
+ arguments: (annotation_argument_list
16498
+ (string_literal) @route_template))
16499
+ `;
16500
+ CONTROLLER_CLASS_QUERY2 = `
16501
+ (class_declaration
16502
+ (modifiers
16503
+ (marker_annotation
16504
+ name: (identifier) @_anno (#match? @_anno "^(RestController|Controller)$")))
16505
+ name: (identifier) @class_name)
16506
+
16507
+ (class_declaration
16508
+ (modifiers
16509
+ (annotation
16510
+ name: (identifier) @_anno (#match? @_anno "^(RestController|Controller)$")))
16511
+ name: (identifier) @class_name)
16512
+ `;
16513
+ springAdapter = {
16514
+ id: "spring",
16515
+ languages: ["java"],
16516
+ matches(signals) {
16517
+ if (signals.pomXml && /\bspring-boot-starter[\w-]*\b/.test(signals.pomXml)) {
16518
+ return true;
16519
+ }
16520
+ if (signals.gradleBuild && /\bspring-boot-starter[\w-]*\b/.test(signals.gradleBuild)) {
16521
+ return true;
16522
+ }
16523
+ if (signals.pomXml && /\borg\.springframework\b/.test(signals.pomXml)) {
16524
+ return true;
16525
+ }
16526
+ if (signals.gradleBuild && /\borg\.springframework\b/.test(signals.gradleBuild)) {
16527
+ return true;
16528
+ }
16529
+ return false;
16530
+ },
16531
+ async introspect(files, _rootDir) {
16532
+ if (files.length === 0) {
16533
+ return { conventions: {}, provenance: [], confidence: "none" };
16534
+ }
16535
+ let language;
16536
+ try {
16537
+ language = await loadGrammar("java");
16538
+ } catch (e2) {
16539
+ return { conventions: {}, provenance: [], confidence: "none" };
16540
+ }
16541
+ const parser = new Parser();
16542
+ parser.setLanguage(language);
16543
+ const routeMethods = /* @__PURE__ */ new Map();
16544
+ const prefixBases = /* @__PURE__ */ new Map();
16545
+ const controllerClasses = /* @__PURE__ */ new Map();
16546
+ try {
16547
+ for (const file of files) {
16548
+ const skip = isParsableSource(file.content, file.size);
16549
+ if (skip) {
16550
+ process.stderr.write(
16551
+ `[massu/ast] WARN: spring skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
16552
+ `
16553
+ );
16554
+ continue;
16555
+ }
16556
+ try {
16557
+ for (const hit of runQuery(parser, file.content, HTTP_MAPPING_QUERY, "spring-http-mapping", file.path)) {
16558
+ const methodRaw = hit.captures.method;
16559
+ if (!methodRaw) continue;
16560
+ const verb = methodRaw.replace(/Mapping$/, "");
16561
+ if (!routeMethods.has(verb)) {
16562
+ routeMethods.set(verb, { line: hit.line, file: file.path });
16563
+ }
16564
+ }
16565
+ for (const hit of runQuery(parser, file.content, HTTP_MAPPING_NO_ARGS_QUERY, "spring-http-mapping-marker", file.path)) {
16566
+ const methodRaw = hit.captures.method;
16567
+ if (!methodRaw) continue;
16568
+ const verb = methodRaw.replace(/Mapping$/, "");
16569
+ if (!routeMethods.has(verb)) {
16570
+ routeMethods.set(verb, { line: hit.line, file: file.path });
16571
+ }
16572
+ }
16573
+ for (const hit of runQuery(parser, file.content, REQUEST_MAPPING_QUERY, "spring-request-mapping", file.path)) {
16574
+ const tplRaw = hit.captures.route_template;
16575
+ if (!tplRaw) continue;
16576
+ const literal = tplRaw.replace(/^["']/, "").replace(/["']$/, "");
16577
+ const base = extractPrefixBase7(literal);
16578
+ if (base && !prefixBases.has(base)) {
16579
+ prefixBases.set(base, { line: hit.line, file: file.path });
16580
+ }
16581
+ }
16582
+ for (const hit of runQuery(parser, file.content, CONTROLLER_CLASS_QUERY2, "spring-controller-class", file.path)) {
16583
+ const name2 = hit.captures.class_name;
16584
+ if (name2 && !controllerClasses.has(name2)) {
16585
+ controllerClasses.set(name2, { line: hit.line, file: file.path });
16586
+ }
16587
+ }
16588
+ } catch (e2) {
16589
+ if (e2 instanceof InvalidQueryError) {
16590
+ throw e2;
16591
+ }
16592
+ continue;
16593
+ }
16594
+ }
16595
+ } finally {
16596
+ try {
16597
+ parser.delete();
16598
+ } catch {
16599
+ }
16600
+ }
16601
+ const conventions = {};
16602
+ const provenance = [];
16603
+ if (routeMethods.size === 1) {
16604
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16605
+ conventions.route_method = name2;
16606
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "spring-http-mapping" });
16607
+ } else if (routeMethods.size >= 2) {
16608
+ const [name2, { line, file }] = routeMethods.entries().next().value;
16609
+ conventions.route_method = name2;
16610
+ provenance.push({ field: "route_method", sourceFile: file, line, query: "spring-http-mapping" });
16611
+ }
16612
+ if (prefixBases.size >= 1) {
16613
+ const [base, { line, file }] = prefixBases.entries().next().value;
16614
+ conventions.route_prefix_base = base;
16615
+ provenance.push({ field: "route_prefix_base", sourceFile: file, line, query: "spring-request-mapping" });
16616
+ }
16617
+ if (controllerClasses.size >= 1) {
16618
+ const [name2, { line, file }] = controllerClasses.entries().next().value;
16619
+ conventions.controller_class = name2;
16620
+ provenance.push({ field: "controller_class", sourceFile: file, line, query: "spring-controller-class" });
16621
+ }
16622
+ let confidence;
16623
+ if (Object.keys(conventions).length === 0) {
16624
+ confidence = "none";
16625
+ } else if (routeMethods.size === 1) {
16626
+ confidence = "high";
16627
+ } else if (routeMethods.size >= 2) {
16628
+ confidence = "low";
16629
+ } else {
16630
+ confidence = "medium";
16631
+ }
16632
+ return { conventions, provenance, confidence };
16633
+ }
16634
+ };
16635
+ }
16636
+ });
16637
+
15720
16638
  // src/detect/adapters/file-sampler.ts
15721
16639
  var file_sampler_exports = {};
15722
16640
  __export(file_sampler_exports, {
@@ -15940,11 +16858,23 @@ var init_codebase_introspector = __esm({
15940
16858
  init_python_django();
15941
16859
  init_nextjs_trpc();
15942
16860
  init_swift_swiftui();
16861
+ init_python_flask();
16862
+ init_go_chi();
16863
+ init_rails();
16864
+ init_phoenix();
16865
+ init_aspnet();
16866
+ init_spring();
15943
16867
  FIRST_PARTY_ADAPTERS = [
15944
16868
  pythonFastApiAdapter,
15945
16869
  pythonDjangoAdapter,
16870
+ pythonFlaskAdapter,
15946
16871
  nextjsTrpcAdapter,
15947
- swiftSwiftUiAdapter
16872
+ swiftSwiftUiAdapter,
16873
+ goChiAdapter,
16874
+ railsAdapter,
16875
+ phoenixAdapter,
16876
+ aspnetAdapter,
16877
+ springAdapter
15948
16878
  ];
15949
16879
  }
15950
16880
  });
@@ -13309,6 +13309,24 @@ init_parse_guard();
13309
13309
  // src/detect/adapters/swift-swiftui.ts
13310
13310
  init_parse_guard();
13311
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
+
13312
13330
  // src/detect/codebase-introspector.ts
13313
13331
  function introspect(detection, projectRoot) {
13314
13332
  const out2 = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.5.4",
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",
@@ -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
  // ============================================================
@@ -1,4 +1,4 @@
1
- // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-08T20:46:40.928Z.
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