@facilis.dev/analyze 0.0.0-stage → 0.1.1

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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/dist/analyze.d.ts +17 -0
  4. package/dist/analyze.js +99 -0
  5. package/dist/ast/node.d.ts +29 -0
  6. package/dist/ast/node.js +85 -0
  7. package/dist/ast/walk.d.ts +8 -0
  8. package/dist/ast/walk.js +40 -0
  9. package/dist/diagnostics.d.ts +5 -0
  10. package/dist/diagnostics.js +12 -0
  11. package/dist/fs.d.ts +13 -0
  12. package/dist/fs.js +51 -0
  13. package/dist/graph-edges.d.ts +3 -0
  14. package/dist/graph-edges.js +148 -0
  15. package/dist/graph-ids.d.ts +6 -0
  16. package/dist/graph-ids.js +22 -0
  17. package/dist/graph.d.ts +15 -0
  18. package/dist/graph.js +162 -0
  19. package/dist/index.d.ts +24 -0
  20. package/dist/index.js +17 -0
  21. package/dist/json.d.ts +8 -0
  22. package/dist/json.js +31 -0
  23. package/dist/module-graph.d.ts +33 -0
  24. package/dist/module-graph.js +86 -0
  25. package/dist/module-info.d.ts +27 -0
  26. package/dist/module-info.js +143 -0
  27. package/dist/node.d.ts +16 -0
  28. package/dist/node.js +41 -0
  29. package/dist/parser.d.ts +16 -0
  30. package/dist/parser.js +1 -0
  31. package/dist/passes/clients.d.ts +22 -0
  32. package/dist/passes/clients.js +167 -0
  33. package/dist/passes/config.d.ts +16 -0
  34. package/dist/passes/config.js +229 -0
  35. package/dist/passes/file-facts.d.ts +13 -0
  36. package/dist/passes/file-facts.js +130 -0
  37. package/dist/passes/i18n.d.ts +14 -0
  38. package/dist/passes/i18n.js +55 -0
  39. package/dist/passes/job-relations-diagnostics.d.ts +9 -0
  40. package/dist/passes/job-relations-diagnostics.js +38 -0
  41. package/dist/passes/job-relations.d.ts +18 -0
  42. package/dist/passes/job-relations.js +52 -0
  43. package/dist/passes/jobs.d.ts +20 -0
  44. package/dist/passes/jobs.js +133 -0
  45. package/dist/passes/jsx-text.d.ts +28 -0
  46. package/dist/passes/jsx-text.js +121 -0
  47. package/dist/passes/jsx.d.ts +24 -0
  48. package/dist/passes/jsx.js +135 -0
  49. package/dist/passes/path-expression.d.ts +26 -0
  50. package/dist/passes/path-expression.js +265 -0
  51. package/dist/passes/route-config.d.ts +24 -0
  52. package/dist/passes/route-config.js +152 -0
  53. package/dist/passes/route-description.d.ts +13 -0
  54. package/dist/passes/route-description.js +23 -0
  55. package/dist/passes/route-elevation.d.ts +18 -0
  56. package/dist/passes/route-elevation.js +136 -0
  57. package/dist/passes/route-handler.d.ts +27 -0
  58. package/dist/passes/route-handler.js +87 -0
  59. package/dist/passes/route-location.d.ts +16 -0
  60. package/dist/passes/route-location.js +58 -0
  61. package/dist/passes/route-relation-resolve.d.ts +51 -0
  62. package/dist/passes/route-relation-resolve.js +172 -0
  63. package/dist/passes/route-relations-diagnostics.d.ts +15 -0
  64. package/dist/passes/route-relations-diagnostics.js +79 -0
  65. package/dist/passes/route-relations-follow.d.ts +16 -0
  66. package/dist/passes/route-relations-follow.js +116 -0
  67. package/dist/passes/route-relations-usage.d.ts +50 -0
  68. package/dist/passes/route-relations-usage.js +44 -0
  69. package/dist/passes/route-relations-walk.d.ts +25 -0
  70. package/dist/passes/route-relations-walk.js +199 -0
  71. package/dist/passes/route-relations.d.ts +17 -0
  72. package/dist/passes/route-relations.js +43 -0
  73. package/dist/passes/route-table.d.ts +21 -0
  74. package/dist/passes/route-table.js +104 -0
  75. package/dist/passes/route-tables.d.ts +10 -0
  76. package/dist/passes/route-tables.js +42 -0
  77. package/dist/passes/routes.d.ts +18 -0
  78. package/dist/passes/routes.js +152 -0
  79. package/dist/passes/schema-column-types.d.ts +9 -0
  80. package/dist/passes/schema-column-types.js +55 -0
  81. package/dist/passes/schema-columns.d.ts +35 -0
  82. package/dist/passes/schema-columns.js +197 -0
  83. package/dist/passes/schema-dsl.d.ts +24 -0
  84. package/dist/passes/schema-dsl.js +117 -0
  85. package/dist/passes/schema-file-links-ddl.d.ts +18 -0
  86. package/dist/passes/schema-file-links-ddl.js +106 -0
  87. package/dist/passes/schema-file-links.d.ts +19 -0
  88. package/dist/passes/schema-file-links.js +144 -0
  89. package/dist/passes/schema-foreign-keys.d.ts +21 -0
  90. package/dist/passes/schema-foreign-keys.js +80 -0
  91. package/dist/passes/schema-policies.d.ts +15 -0
  92. package/dist/passes/schema-policies.js +201 -0
  93. package/dist/passes/schema-re-exports.d.ts +5 -0
  94. package/dist/passes/schema-re-exports.js +77 -0
  95. package/dist/passes/schema-relations.d.ts +8 -0
  96. package/dist/passes/schema-relations.js +182 -0
  97. package/dist/passes/schema-scope.d.ts +7 -0
  98. package/dist/passes/schema-scope.js +20 -0
  99. package/dist/passes/schema-tables.d.ts +13 -0
  100. package/dist/passes/schema-tables.js +149 -0
  101. package/dist/passes/schema.d.ts +9 -0
  102. package/dist/passes/schema.js +55 -0
  103. package/dist/passes/screen-components.d.ts +22 -0
  104. package/dist/passes/screen-components.js +143 -0
  105. package/dist/passes/screen-intrinsics.d.ts +18 -0
  106. package/dist/passes/screen-intrinsics.js +21 -0
  107. package/dist/passes/screen-preview.d.ts +29 -0
  108. package/dist/passes/screen-preview.js +174 -0
  109. package/dist/passes/screens.d.ts +12 -0
  110. package/dist/passes/screens.js +278 -0
  111. package/dist/passes/static-read.d.ts +47 -0
  112. package/dist/passes/static-read.js +151 -0
  113. package/dist/passes/views.d.ts +20 -0
  114. package/dist/passes/views.js +101 -0
  115. package/dist/passes/workflow-tables.d.ts +18 -0
  116. package/dist/passes/workflow-tables.js +71 -0
  117. package/dist/passes/workflows.d.ts +8 -0
  118. package/dist/passes/workflows.js +160 -0
  119. package/dist/path-resolver.d.ts +20 -0
  120. package/dist/path-resolver.js +134 -0
  121. package/dist/revision.d.ts +2 -0
  122. package/dist/revision.js +23 -0
  123. package/dist/source.d.ts +4 -0
  124. package/dist/source.js +13 -0
  125. package/dist/static-eval.d.ts +36 -0
  126. package/dist/static-eval.js +215 -0
  127. package/dist/types-graph.d.ts +144 -0
  128. package/dist/types-graph.js +4 -0
  129. package/dist/types-jobs.d.ts +28 -0
  130. package/dist/types-jobs.js +14 -0
  131. package/dist/types-relations.d.ts +49 -0
  132. package/dist/types-relations.js +11 -0
  133. package/dist/types-workflows.d.ts +36 -0
  134. package/dist/types-workflows.js +3 -0
  135. package/dist/types.d.ts +226 -0
  136. package/dist/types.js +1 -0
  137. package/package.json +49 -4
  138. package/src/analyze.ts +119 -0
  139. package/src/ast/node.ts +102 -0
  140. package/src/ast/walk.ts +37 -0
  141. package/src/diagnostics.ts +22 -0
  142. package/src/fs.ts +59 -0
  143. package/src/graph-edges.ts +167 -0
  144. package/src/graph-ids.ts +31 -0
  145. package/src/graph.ts +194 -0
  146. package/src/index.ts +26 -0
  147. package/src/json.ts +34 -0
  148. package/src/module-graph.ts +117 -0
  149. package/src/module-info.ts +162 -0
  150. package/src/node.ts +54 -0
  151. package/src/parser.ts +19 -0
  152. package/src/passes/clients.ts +233 -0
  153. package/src/passes/config.ts +278 -0
  154. package/src/passes/file-facts.ts +142 -0
  155. package/src/passes/i18n.ts +66 -0
  156. package/src/passes/job-relations-diagnostics.ts +60 -0
  157. package/src/passes/job-relations.ts +76 -0
  158. package/src/passes/jobs.ts +181 -0
  159. package/src/passes/jsx-text.ts +126 -0
  160. package/src/passes/jsx.ts +142 -0
  161. package/src/passes/path-expression.ts +297 -0
  162. package/src/passes/route-config.ts +188 -0
  163. package/src/passes/route-description.ts +33 -0
  164. package/src/passes/route-elevation.ts +161 -0
  165. package/src/passes/route-handler.ts +105 -0
  166. package/src/passes/route-location.ts +62 -0
  167. package/src/passes/route-relation-resolve.ts +228 -0
  168. package/src/passes/route-relations-diagnostics.ts +115 -0
  169. package/src/passes/route-relations-follow.ts +146 -0
  170. package/src/passes/route-relations-usage.ts +98 -0
  171. package/src/passes/route-relations-walk.ts +255 -0
  172. package/src/passes/route-relations.ts +62 -0
  173. package/src/passes/route-table.ts +123 -0
  174. package/src/passes/route-tables.ts +50 -0
  175. package/src/passes/routes.ts +188 -0
  176. package/src/passes/schema-column-types.ts +62 -0
  177. package/src/passes/schema-columns.ts +287 -0
  178. package/src/passes/schema-dsl.ts +136 -0
  179. package/src/passes/schema-file-links-ddl.ts +125 -0
  180. package/src/passes/schema-file-links.ts +213 -0
  181. package/src/passes/schema-foreign-keys.ts +121 -0
  182. package/src/passes/schema-policies.ts +270 -0
  183. package/src/passes/schema-re-exports.ts +95 -0
  184. package/src/passes/schema-relations.ts +239 -0
  185. package/src/passes/schema-scope.ts +29 -0
  186. package/src/passes/schema-tables.ts +216 -0
  187. package/src/passes/schema.ts +67 -0
  188. package/src/passes/screen-components.ts +162 -0
  189. package/src/passes/screen-intrinsics.ts +29 -0
  190. package/src/passes/screen-preview.ts +295 -0
  191. package/src/passes/screens.ts +291 -0
  192. package/src/passes/static-read.ts +196 -0
  193. package/src/passes/views.ts +125 -0
  194. package/src/passes/workflow-tables.ts +87 -0
  195. package/src/passes/workflows.ts +213 -0
  196. package/src/path-resolver.ts +146 -0
  197. package/src/revision.ts +25 -0
  198. package/src/source.ts +15 -0
  199. package/src/static-eval.ts +242 -0
  200. package/src/types-graph.ts +176 -0
  201. package/src/types-jobs.ts +40 -0
  202. package/src/types-relations.ts +64 -0
  203. package/src/types-workflows.ts +42 -0
  204. package/src/types.ts +299 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 facilis-dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # @facilis.dev/analyze
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Facilis AST analyzer: static facts for the project DSL (routes, schema, relations, pages) with an injected parser.
4
+
5
+ Part of [Facilis](https://github.com/facilis-dev/facilis), a browser-first low-code framework: the Studio
6
+ runs a generated project in a WebContainer where vite dev is the only dev server
7
+ and Nitro v3 runs as a Vite plugin. Framework contracts live in
8
+ [docs/SPEC.md](https://github.com/facilis-dev/facilis/blob/main/docs/SPEC.md) and architectural decisions in
9
+ [docs/DECISIONS.md](https://github.com/facilis-dev/facilis/blob/main/docs/DECISIONS.md).
10
+
11
+ ```sh
12
+ npm install @facilis.dev/analyze
13
+ ```
14
+
15
+ The package publishes its TypeScript sources and its compiled dist/; the exports map
16
+ of the published manifest points at dist/, so Node and bundlers load JavaScript and
17
+ the bundled .d.ts files.
18
+
19
+ Licensed under the MIT License.
@@ -0,0 +1,17 @@
1
+ import { type TransportSpec } from "./passes/clients.js";
2
+ import type { PathAlias } from "./path-resolver.js";
3
+ import type { Parser } from "./parser.js";
4
+ import type { Analysis } from "./types.js";
5
+ export interface AnalyzeInput {
6
+ /** Project-relative path -> source. The analyzer never touches the filesystem itself. */
7
+ readonly files: ReadonlyMap<string, string>;
8
+ readonly parser: Parser;
9
+ /** Project root inside the file map (defaults to the map root). */
10
+ readonly rootDir?: string;
11
+ readonly aliases?: readonly PathAlias[];
12
+ readonly transport?: TransportSpec;
13
+ }
14
+ /** The sanctioned client transport (docs/findings/ast-dsl-contract.md §3.4). */
15
+ export declare const DEFAULT_TRANSPORT: TransportSpec;
16
+ /** Analyze a project snapshot into the fact model + project graph. Pure and synchronous. */
17
+ export declare function analyzeProject(input: AnalyzeInput): Analysis;
@@ -0,0 +1,99 @@
1
+ import { buildPageFacts } from "./passes/clients.js";
2
+ import { buildConfigFacts } from "./passes/config.js";
3
+ import { buildFileFacts } from "./passes/file-facts.js";
4
+ import { readLocaleCatalogs } from "./passes/i18n.js";
5
+ import { buildJobFacts } from "./passes/jobs.js";
6
+ import { buildJobRelationFacts } from "./passes/job-relations.js";
7
+ import { buildRouteFacts } from "./passes/routes.js";
8
+ import { buildRouteRelationFacts } from "./passes/route-relations.js";
9
+ import { routeSystemTableDiagnostics } from "./passes/route-tables.js";
10
+ import { buildSchemaFacts } from "./passes/schema.js";
11
+ import { buildScreens } from "./passes/screens.js";
12
+ import { buildViewFacts } from "./passes/views.js";
13
+ import { buildWorkflowFacts } from "./passes/workflows.js";
14
+ import { buildWorkflowTableFacts } from "./passes/workflow-tables.js";
15
+ import { buildProjectGraph } from "./graph.js";
16
+ import { createModuleGraph } from "./module-graph.js";
17
+ import { computeRevision } from "./revision.js";
18
+ import { diagnostic } from "./diagnostics.js";
19
+ /** The sanctioned client transport (docs/findings/ast-dsl-contract.md §3.4). */
20
+ export const DEFAULT_TRANSPORT = {
21
+ module: "src/lib/api.ts",
22
+ exportName: "api",
23
+ };
24
+ /** Analyze a project snapshot into the fact model + project graph. Pure and synchronous. */
25
+ export function analyzeProject(input) {
26
+ const graph = createModuleGraph({
27
+ files: input.files,
28
+ parser: input.parser,
29
+ root: input.rootDir ?? "",
30
+ ...(input.aliases === undefined ? {} : { aliases: input.aliases }),
31
+ });
32
+ const config = buildConfigFacts(graph);
33
+ const routes = buildRouteFacts(graph, config.facts);
34
+ const schema = buildSchemaFacts(graph, config.facts);
35
+ const views = buildViewFacts(graph);
36
+ // D-157: the declaration check runs after the schema and the views, because a route's
37
+ // `dbTables` and its usage are compared against both (tables and views).
38
+ const routeRelations = buildRouteRelationFacts(graph, routes.routes, schema.tables, views.views);
39
+ // D-157 applies to jobs too: their `run` sees the system-scoped db client, tables and
40
+ // views alike, and the relation index is the same one the route pass builds its cases on.
41
+ const jobs = buildJobFacts(graph);
42
+ const jobRelations = buildJobRelationFacts(graph, jobs.jobs, schema.tables, views.views);
43
+ const workflows = buildWorkflowFacts(graph);
44
+ // A workflow declares no tables: its surface is derived from the jobs its steps reference.
45
+ const workflowTables = buildWorkflowTableFacts(workflows.workflows, jobs.jobs);
46
+ const locales = readLocaleCatalogs(graph);
47
+ const pages = buildPageFacts(graph, routes.routes, input.transport ?? DEFAULT_TRANSPORT);
48
+ const screens = buildScreens(graph, pages.pages);
49
+ const fileFacts = buildFileFacts(graph);
50
+ const diagnostics = [];
51
+ for (const file of graph.files.values()) {
52
+ for (const error of file.errors) {
53
+ diagnostics.push(diagnostic("parse-error", "error", error.message, error.source));
54
+ }
55
+ }
56
+ diagnostics.push(...[
57
+ ...config.diagnostics,
58
+ ...routes.diagnostics,
59
+ ...schema.diagnostics,
60
+ ...views.diagnostics,
61
+ ...routeRelations.diagnostics,
62
+ ...jobs.diagnostics,
63
+ ...jobRelations.diagnostics,
64
+ ...locales.diagnostics,
65
+ ...pages.diagnostics,
66
+ ...routeSystemTableDiagnostics(routes.routes, schema.tables),
67
+ ...workflows.diagnostics,
68
+ ...workflowTables.diagnostics,
69
+ ].filter(
70
+ // Framework sources are shipped code: their facts stay, but their lint-style
71
+ // diagnostics are not the project's. Project files start outside node_modules/.
72
+ (entry) => entry.source === null || !entry.source.file.startsWith("node_modules/")));
73
+ return {
74
+ revision: computeRevision(input.files),
75
+ files: [...graph.files.keys()].toSorted((left, right) => left.localeCompare(right)),
76
+ config: config.facts,
77
+ routes: routeRelations.routes,
78
+ tables: schema.tables,
79
+ relations: schema.relations,
80
+ views: views.views,
81
+ pages: pages.pages,
82
+ workflows: workflows.workflows,
83
+ jobs: jobs.jobs,
84
+ jobRelations: jobRelations.relations,
85
+ workflowTables: workflowTables.facts,
86
+ fileFacts,
87
+ locales: locales.locales,
88
+ diagnostics,
89
+ graph: buildProjectGraph({
90
+ routes: routes.routes,
91
+ tables: schema.tables,
92
+ relations: schema.relations,
93
+ pages: pages.pages,
94
+ views: views.views,
95
+ screens,
96
+ workflows: workflows.workflows,
97
+ }),
98
+ };
99
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Minimal ESTree narrowing helpers shared by every analyzer pass.
3
+ *
4
+ * The AST shape is parser-independent: oxc-parser (Node, browser) and any future adapter
5
+ * produce plain ESTree / TS-ESTree nodes. Passes never import a parser package.
6
+ */
7
+ export interface AstNode {
8
+ type: string;
9
+ start?: number;
10
+ end?: number;
11
+ [key: string]: unknown;
12
+ }
13
+ export declare function isAstNode(value: unknown): value is AstNode;
14
+ export declare function asNode(value: unknown): AstNode | undefined;
15
+ export declare function nodeArray(node: AstNode, key: string): AstNode[];
16
+ export declare function identifierName(node: unknown): string | undefined;
17
+ export declare function literalString(node: unknown): string | undefined;
18
+ /** Name of an object-literal property, or undefined for computed/spread entries. */
19
+ export declare function propertyName(property: AstNode): string | undefined;
20
+ export declare function propertyValue(property: AstNode): AstNode | undefined;
21
+ /** Object-literal entries, including spread elements. */
22
+ export declare function objectEntries(node: AstNode): AstNode[];
23
+ export declare function isCallTo(node: unknown, calleeName: string): node is AstNode;
24
+ export declare function callArguments(node: AstNode): AstNode[];
25
+ /** Strip parentheses and TypeScript-only wrappers from an expression. */
26
+ export declare function unwrapExpression(value: unknown): AstNode | undefined;
27
+ /** A call expression after unwrapping parentheses/TS wrappers. */
28
+ export declare function callExpression(value: unknown): AstNode | undefined;
29
+ export declare function memberName(node: AstNode): string | undefined;
@@ -0,0 +1,85 @@
1
+ export function isAstNode(value) {
2
+ if (typeof value !== "object" || value === null)
3
+ return false;
4
+ return typeof value.type === "string";
5
+ }
6
+ export function asNode(value) {
7
+ return isAstNode(value) ? value : undefined;
8
+ }
9
+ export function nodeArray(node, key) {
10
+ const value = node[key];
11
+ if (!Array.isArray(value))
12
+ return [];
13
+ return value.filter(isAstNode);
14
+ }
15
+ export function identifierName(node) {
16
+ const candidate = asNode(node);
17
+ if (!candidate || candidate.type !== "Identifier")
18
+ return undefined;
19
+ return typeof candidate.name === "string" ? candidate.name : undefined;
20
+ }
21
+ export function literalString(node) {
22
+ const candidate = asNode(node);
23
+ if (!candidate || candidate.type !== "Literal")
24
+ return undefined;
25
+ return typeof candidate.value === "string" ? candidate.value : undefined;
26
+ }
27
+ /** Name of an object-literal property, or undefined for computed/spread entries. */
28
+ export function propertyName(property) {
29
+ if (property.type !== "Property")
30
+ return undefined;
31
+ const key = asNode(property.key);
32
+ if (!key)
33
+ return undefined;
34
+ if (key.type === "Identifier") {
35
+ return property.computed === true ? undefined : identifierName(key);
36
+ }
37
+ if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) {
38
+ return String(key.value);
39
+ }
40
+ return undefined;
41
+ }
42
+ export function propertyValue(property) {
43
+ return asNode(property.value);
44
+ }
45
+ /** Object-literal entries, including spread elements. */
46
+ export function objectEntries(node) {
47
+ return nodeArray(node, "properties");
48
+ }
49
+ export function isCallTo(node, calleeName) {
50
+ const candidate = asNode(node);
51
+ if (!candidate || candidate.type !== "CallExpression")
52
+ return false;
53
+ return identifierName(candidate.callee) === calleeName;
54
+ }
55
+ export function callArguments(node) {
56
+ return nodeArray(node, "arguments");
57
+ }
58
+ const UNWRAPPED_EXPRESSIONS = new Set([
59
+ "ParenthesizedExpression",
60
+ "TSAsExpression",
61
+ "TSSatisfiesExpression",
62
+ "TSNonNullExpression",
63
+ "TSInstantiationExpression",
64
+ "ChainExpression",
65
+ ]);
66
+ /** Strip parentheses and TypeScript-only wrappers from an expression. */
67
+ export function unwrapExpression(value) {
68
+ let node = asNode(value);
69
+ while (node && UNWRAPPED_EXPRESSIONS.has(node.type)) {
70
+ node = asNode(node.expression);
71
+ }
72
+ return node;
73
+ }
74
+ /** A call expression after unwrapping parentheses/TS wrappers. */
75
+ export function callExpression(value) {
76
+ const node = unwrapExpression(value);
77
+ return node?.type === "CallExpression" ? node : undefined;
78
+ }
79
+ export function memberName(node) {
80
+ if (node.type !== "MemberExpression")
81
+ return undefined;
82
+ if (node.computed === true)
83
+ return literalString(node.property);
84
+ return identifierName(node.property);
85
+ }
@@ -0,0 +1,8 @@
1
+ import { type AstNode } from "./node.js";
2
+ /**
3
+ * Depth-first walk over an ESTree node. The AST is plain data (no cycles), so a generic
4
+ * recursion is enough and keeps the analyzer independent of a parser-specific visitor.
5
+ */
6
+ export declare function walk(node: AstNode, visit: (node: AstNode) => void): void;
7
+ /** Visit descendants of a node, not the node itself. */
8
+ export declare function walkChildren(node: AstNode, visit: (node: AstNode) => void): void;
@@ -0,0 +1,40 @@
1
+ import { isAstNode } from "./node.js";
2
+ const SKIPPED_KEYS = new Set(["type", "start", "end", "range"]);
3
+ /**
4
+ * Depth-first walk over an ESTree node. The AST is plain data (no cycles), so a generic
5
+ * recursion is enough and keeps the analyzer independent of a parser-specific visitor.
6
+ */
7
+ export function walk(node, visit) {
8
+ visit(node);
9
+ for (const key of Object.keys(node)) {
10
+ if (SKIPPED_KEYS.has(key))
11
+ continue;
12
+ const value = node[key];
13
+ if (Array.isArray(value)) {
14
+ for (const child of value) {
15
+ if (isAstNode(child))
16
+ walk(child, visit);
17
+ }
18
+ continue;
19
+ }
20
+ if (isAstNode(value))
21
+ walk(value, visit);
22
+ }
23
+ }
24
+ /** Visit descendants of a node, not the node itself. */
25
+ export function walkChildren(node, visit) {
26
+ for (const key of Object.keys(node)) {
27
+ if (SKIPPED_KEYS.has(key))
28
+ continue;
29
+ const value = node[key];
30
+ if (Array.isArray(value)) {
31
+ for (const child of value) {
32
+ if (isAstNode(child))
33
+ walk(child, visit);
34
+ }
35
+ continue;
36
+ }
37
+ if (isAstNode(value))
38
+ walk(value, visit);
39
+ }
40
+ }
@@ -0,0 +1,5 @@
1
+ import type { Diagnostic, Severity, SourceRef } from "./types.js";
2
+ export declare function diagnostic(code: string, severity: Severity, message: string, source: SourceRef | null): Diagnostic;
3
+ export declare function error(code: string, message: string, source: SourceRef | null): Diagnostic;
4
+ export declare function warning(code: string, message: string, source: SourceRef | null): Diagnostic;
5
+ export declare function info(code: string, message: string, source: SourceRef | null): Diagnostic;
@@ -0,0 +1,12 @@
1
+ export function diagnostic(code, severity, message, source) {
2
+ return { code, severity, message, source };
3
+ }
4
+ export function error(code, message, source) {
5
+ return diagnostic(code, "error", message, source);
6
+ }
7
+ export function warning(code, message, source) {
8
+ return diagnostic(code, "warning", message, source);
9
+ }
10
+ export function info(code, message, source) {
11
+ return diagnostic(code, "info", message, source);
12
+ }
package/dist/fs.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Node filesystem loader for the analyzer (exported as `@facilis.dev/analyze/fs`).
3
+ *
4
+ * Kept apart from the `./node` parser adapter: a Vite config that runs in the browser VM
5
+ * needs to read a project directory but must not load a native/wasm parser. The loader only
6
+ * reads and filters the file map; the caller injects the parser (D-092).
7
+ */
8
+ export interface LoadProjectOptions {
9
+ /** Package scopes allowed under node_modules (default: @facilis.dev). */
10
+ readonly nodeModuleScopes?: readonly string[];
11
+ }
12
+ /** Read a project directory into the analyzer's path -> source map. */
13
+ export declare function loadProjectFiles(root: string, options?: LoadProjectOptions): Map<string, string>;
package/dist/fs.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Node filesystem loader for the analyzer (exported as `@facilis.dev/analyze/fs`).
3
+ *
4
+ * Kept apart from the `./node` parser adapter: a Vite config that runs in the browser VM
5
+ * needs to read a project directory but must not load a native/wasm parser. The loader only
6
+ * reads and filters the file map; the caller injects the parser (D-092).
7
+ */
8
+ import { readdirSync, readFileSync, statSync } from "node:fs";
9
+ import { join, sep } from "node:path";
10
+ const SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".json"];
11
+ const ALWAYS_SKIPPED = new Set([".git", "dist", "coverage", ".turbo", ".facilis", ".output"]);
12
+ /** Read a project directory into the analyzer's path -> source map. */
13
+ export function loadProjectFiles(root, options = {}) {
14
+ const scopes = options.nodeModuleScopes ?? ["@facilis.dev"];
15
+ const files = new Map();
16
+ const walk = (directory, relativeDirectory) => {
17
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
18
+ if (ALWAYS_SKIPPED.has(entry.name))
19
+ continue;
20
+ const full = join(directory, entry.name);
21
+ const path = relativeDirectory.length === 0 ? entry.name : relativeDirectory + "/" + entry.name;
22
+ const stat = entry.isSymbolicLink() ? statSync(full) : null;
23
+ const isDirectory = stat === null ? entry.isDirectory() : stat.isDirectory();
24
+ const isFile = stat === null ? entry.isFile() : stat.isFile();
25
+ if (isDirectory) {
26
+ if (entry.name === "node_modules") {
27
+ for (const scope of scopes) {
28
+ const scopeDirectory = join(full, scope);
29
+ try {
30
+ readdirSync(scopeDirectory);
31
+ }
32
+ catch {
33
+ continue;
34
+ }
35
+ walk(scopeDirectory, path + "/" + scope);
36
+ }
37
+ continue;
38
+ }
39
+ walk(full, path);
40
+ continue;
41
+ }
42
+ if (!isFile)
43
+ continue;
44
+ if (!SOURCE_EXTENSIONS.some((extension) => entry.name.endsWith(extension)))
45
+ continue;
46
+ files.set(path.split(sep).join("/"), readFileSync(full, "utf8"));
47
+ }
48
+ };
49
+ walk(root, "");
50
+ return files;
51
+ }
@@ -0,0 +1,3 @@
1
+ import type { GraphEdge } from "./types.js";
2
+ import type { GraphInput } from "./graph.js";
3
+ export declare function collectEdges(input: GraphInput, nodeIds: ReadonlySet<string>): GraphEdge[];
@@ -0,0 +1,148 @@
1
+ import { pageId, routeId, tableId, viewId } from "./graph-ids.js";
2
+ function fkLabel(table, targetTable, targetColumns, onDelete) {
3
+ let label = table.name + " -> " + targetTable + "." + targetColumns.join(", ");
4
+ if (onDelete !== null)
5
+ label += " on delete " + onDelete;
6
+ return label;
7
+ }
8
+ function relationLabel(cardinality) {
9
+ return cardinality;
10
+ }
11
+ function buildTableLookupMap(tables) {
12
+ const byName = new Map();
13
+ for (const table of tables) {
14
+ if (!byName.has(table.name))
15
+ byName.set(table.name, table);
16
+ }
17
+ return byName;
18
+ }
19
+ function elevatedTables(route) {
20
+ const tables = new Set();
21
+ for (const elevation of route.elevations) {
22
+ for (const table of elevation.tables)
23
+ tables.add(table);
24
+ }
25
+ return tables;
26
+ }
27
+ export function collectEdges(input, nodeIds) {
28
+ const edges = new Map();
29
+ const tablesByName = buildTableLookupMap(input.tables);
30
+ // Tables and views share the SQL namespace, so a relation name is at most one of the two.
31
+ const viewNames = new Set(input.views.map((view) => view.name));
32
+ const add = (edge) => {
33
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target))
34
+ return;
35
+ if (edges.has(edge.id))
36
+ return;
37
+ edges.set(edge.id, edge);
38
+ };
39
+ for (const table of input.tables) {
40
+ for (const foreignKey of table.foreignKeys) {
41
+ add({
42
+ id: "fk:" +
43
+ table.name +
44
+ ":" +
45
+ foreignKey.columns.join(",") +
46
+ ":" +
47
+ foreignKey.targetTable +
48
+ ":" +
49
+ foreignKey.targetColumns.join(","),
50
+ kind: "fk",
51
+ source: tableId(table.name),
52
+ target: tableId(foreignKey.targetTable),
53
+ label: fkLabel(table, foreignKey.targetTable, foreignKey.targetColumns, foreignKey.onDelete),
54
+ });
55
+ }
56
+ }
57
+ for (const relation of input.relations) {
58
+ const id = ["relation", relation.from, relation.to, relation.cardinality];
59
+ if (relation.fields.length > 0)
60
+ id.push(relation.fields.join(","));
61
+ if (relation.references.length > 0)
62
+ id.push(relation.references.join(","));
63
+ add({
64
+ id: id.join(":"),
65
+ kind: "relation",
66
+ source: tableId(relation.from),
67
+ target: tableId(relation.to),
68
+ label: relationLabel(relation.cardinality),
69
+ });
70
+ }
71
+ // A view depends on the base tables its declaration names (metadata only, D-097). The target
72
+ // is a view when a view declares another view as its base, so the edge lands on the node that
73
+ // exists rather than on a phantom table.
74
+ for (const view of input.views) {
75
+ const source = viewId(view.name);
76
+ for (const base of view.tables) {
77
+ if (base === view.name)
78
+ continue;
79
+ add({
80
+ id: "view-base:" + view.name + ":" + base,
81
+ kind: "view-base",
82
+ source,
83
+ target: viewNames.has(base) ? viewId(base) : tableId(base),
84
+ label: null,
85
+ });
86
+ }
87
+ }
88
+ for (const route of input.routes) {
89
+ const routeNodeId = routeId(route);
90
+ const elevated = elevatedTables(route);
91
+ // A route declares every relation it touches in dbTables (D-157), tables and views alike:
92
+ // the target kind is decided by the relation the name denotes.
93
+ for (const name of route.dbTables ?? []) {
94
+ const isView = viewNames.has(name);
95
+ add({
96
+ id: (isView ? "route-view:" : "route-table:") + routeNodeId + ":dbTables:" + name,
97
+ kind: isView ? "route-view" : "route-table",
98
+ source: routeNodeId,
99
+ target: isView ? viewId(name) : tableId(name),
100
+ label: "dbTables",
101
+ ...(elevated.has(name) ? { access: "elevated" } : {}),
102
+ });
103
+ }
104
+ for (const entry of route.dbColumns ?? []) {
105
+ const separator = entry.indexOf(".");
106
+ if (separator <= 0)
107
+ continue;
108
+ const tableName = entry.slice(0, separator);
109
+ const columnName = entry.slice(separator + 1);
110
+ const table = tablesByName.get(tableName);
111
+ const knownColumn = table?.columns.some((column) => column.property === columnName);
112
+ if (knownColumn !== true)
113
+ continue;
114
+ add({
115
+ id: "route-table:" + routeNodeId + ":dbColumns:" + tableName,
116
+ kind: "route-table",
117
+ source: routeNodeId,
118
+ target: tableId(tableName),
119
+ label: "dbColumns",
120
+ ...(elevated.has(tableName) ? { access: "elevated" } : {}),
121
+ });
122
+ }
123
+ }
124
+ for (const page of input.pages) {
125
+ const source = pageId(page.path);
126
+ for (const call of page.calls) {
127
+ for (const matchedRouteId of call.matchedRouteIds) {
128
+ add({
129
+ id: "page-route:" + page.path + ":" + matchedRouteId,
130
+ kind: "page-route",
131
+ source,
132
+ target: matchedRouteId,
133
+ label: null,
134
+ });
135
+ }
136
+ }
137
+ }
138
+ for (const link of input.screens?.links ?? []) {
139
+ add({
140
+ id: "page-link:" + link.from + ":" + link.to + ":" + link.kind + ":" + link.toPath,
141
+ kind: "page-link",
142
+ source: pageId(link.from),
143
+ target: pageId(link.to),
144
+ label: link.toPath,
145
+ });
146
+ }
147
+ return [...edges.values()];
148
+ }
@@ -0,0 +1,6 @@
1
+ import type { RouteFact } from "./types.js";
2
+ export declare function tableId(name: string): string;
3
+ export declare function viewId(name: string): string;
4
+ export declare function routeId(route: RouteFact): string;
5
+ export declare function pageId(path: string): string;
6
+ export declare function workflowId(name: string): string;
@@ -0,0 +1,22 @@
1
+ const NODE_PREFIX = {
2
+ page: "page:",
3
+ route: "route:",
4
+ table: "table:",
5
+ view: "view:",
6
+ workflow: "workflow:",
7
+ };
8
+ export function tableId(name) {
9
+ return NODE_PREFIX.table + name;
10
+ }
11
+ export function viewId(name) {
12
+ return NODE_PREFIX.view + name;
13
+ }
14
+ export function routeId(route) {
15
+ return NODE_PREFIX.route + route.method + " " + route.path;
16
+ }
17
+ export function pageId(path) {
18
+ return NODE_PREFIX.page + path;
19
+ }
20
+ export function workflowId(name) {
21
+ return NODE_PREFIX.workflow + name;
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { PageFact, ProjectGraph, RelationFact, RouteFact, ScreenFacts, TableFact, ViewFact, WorkflowFact } from "./types.js";
2
+ export interface GraphInput {
3
+ readonly routes: readonly RouteFact[];
4
+ readonly tables: readonly TableFact[];
5
+ readonly relations: readonly RelationFact[];
6
+ readonly pages: readonly PageFact[];
7
+ /** Declarative views (D-097/D-098): nodes plus view-base edges since D-160. */
8
+ readonly views: readonly ViewFact[];
9
+ /** Route paths, bounded page previews and explicit page links. */
10
+ readonly screens?: ScreenFacts;
11
+ /** Workflow definitions read from `server/workflows/**` (D-135 W3). */
12
+ readonly workflows?: readonly WorkflowFact[];
13
+ }
14
+ /** Build the single project graph from facts. Ids and ordering are stable across runs. */
15
+ export declare function buildProjectGraph(input: GraphInput): ProjectGraph;