@facilis.dev/ai 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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/dist/context.d.ts +12 -0
  4. package/dist/context.js +104 -0
  5. package/dist/index.d.ts +16 -0
  6. package/dist/index.js +15 -0
  7. package/dist/manifest-facts.d.ts +16 -0
  8. package/dist/manifest-facts.js +45 -0
  9. package/dist/node.d.ts +19 -0
  10. package/dist/node.js +60 -0
  11. package/dist/overlay.d.ts +41 -0
  12. package/dist/overlay.js +91 -0
  13. package/dist/paths.d.ts +41 -0
  14. package/dist/paths.js +70 -0
  15. package/dist/rules/blocks.d.ts +12 -0
  16. package/dist/rules/blocks.js +64 -0
  17. package/dist/rules/config-sync.d.ts +9 -0
  18. package/dist/rules/config-sync.js +64 -0
  19. package/dist/rules/database.d.ts +24 -0
  20. package/dist/rules/database.js +105 -0
  21. package/dist/rules/i18n.d.ts +9 -0
  22. package/dist/rules/i18n.js +40 -0
  23. package/dist/rules/imports.d.ts +15 -0
  24. package/dist/rules/imports.js +194 -0
  25. package/dist/rules/job-relations.d.ts +23 -0
  26. package/dist/rules/job-relations.js +84 -0
  27. package/dist/rules/route-relations.d.ts +20 -0
  28. package/dist/rules/route-relations.js +113 -0
  29. package/dist/rules/routes-description.d.ts +14 -0
  30. package/dist/rules/routes-description.js +86 -0
  31. package/dist/rules/routes-metadata.d.ts +16 -0
  32. package/dist/rules/routes-metadata.js +169 -0
  33. package/dist/rules/routes-security.d.ts +22 -0
  34. package/dist/rules/routes-security.js +52 -0
  35. package/dist/rules/structural.d.ts +16 -0
  36. package/dist/rules/structural.js +41 -0
  37. package/dist/rules/ts-parse.d.ts +16 -0
  38. package/dist/rules/ts-parse.js +62 -0
  39. package/dist/syntax-runner.d.ts +8 -0
  40. package/dist/syntax-runner.js +25 -0
  41. package/dist/testing/fixtures.d.ts +48 -0
  42. package/dist/testing/fixtures.js +153 -0
  43. package/dist/types.d.ts +146 -0
  44. package/dist/types.js +12 -0
  45. package/dist/validator.d.ts +13 -0
  46. package/dist/validator.js +62 -0
  47. package/package.json +43 -4
  48. package/src/context.ts +117 -0
  49. package/src/index.ts +44 -0
  50. package/src/manifest-facts.ts +54 -0
  51. package/src/node.ts +75 -0
  52. package/src/overlay.ts +131 -0
  53. package/src/paths.ts +80 -0
  54. package/src/rules/blocks.ts +67 -0
  55. package/src/rules/config-sync.ts +72 -0
  56. package/src/rules/database.ts +139 -0
  57. package/src/rules/i18n.ts +42 -0
  58. package/src/rules/imports.ts +227 -0
  59. package/src/rules/job-relations.ts +126 -0
  60. package/src/rules/route-relations.ts +162 -0
  61. package/src/rules/routes-description.ts +113 -0
  62. package/src/rules/routes-metadata.ts +241 -0
  63. package/src/rules/routes-security.ts +90 -0
  64. package/src/rules/structural.ts +83 -0
  65. package/src/rules/ts-parse.ts +71 -0
  66. package/src/syntax-runner.ts +33 -0
  67. package/src/testing/fixtures.ts +215 -0
  68. package/src/types.ts +179 -0
  69. package/src/validator.ts +81 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shared fixtures for the validator tests: a realistic todo-app project analyzed with
3
+ * @facilis.dev/analyze, plus the patch overlay applied to its file map.
4
+ *
5
+ * Tests build the effective (post-patch) Analysis exactly as the IDE does — project the
6
+ * patch onto the pre-patch file map and analyze the result — so every rule runs against
7
+ * AST facts instead of a tolerant text scan.
8
+ */
9
+ import { type FilePatch, type ImportAlias, type PatchContext, type SyntaxCheck, type ValidatePatchOptions, type ValidationResult } from "../index.js";
10
+ export declare const CONFIG = "import { defineConfig } from \"@facilis.dev/core\";\n\nexport default defineConfig({\n release: { ref: \"origin/main\" },\n db: { mode: \"pglite\", migrationsDir: \"db/migrations\" },\n i18n: { defaultLocale: \"en\", locales: [\"en\"] },\n auth: {\n permissions: [\"todos:read\", \"todos:write\"],\n roles: { admin: [\"*\"], user: [\"todos:read\", \"todos:write\"] },\n defaultRole: \"user\",\n },\n});\n";
11
+ export declare const CONFIG_IT: string;
12
+ export declare const SCHEMA = "import { boolean, pgTable, text, uuid } from \"drizzle-orm/pg-core\";\nimport { ownerIsolationPolicy, tenantColumn, tenantIsolationPolicy } from \"@facilis.dev/db\";\n\nexport const todos = pgTable(\n \"todos\",\n {\n id: uuid(\"id\").primaryKey().defaultRandom(),\n tenantId: tenantColumn(),\n ownerId: uuid(\"owner_id\").notNull(),\n title: text(\"title\").notNull(),\n done: boolean(\"done\").notNull().default(false),\n },\n () => [tenantIsolationPolicy(), ownerIsolationPolicy()],\n).enableRLS();\n";
13
+ export declare const ROUTE_SOURCE = "import { z } from \"zod\";\nimport { defineAuthorizedHandler } from \"@facilis.dev/core\";\nimport { todos } from \"~/db/schema\";\n\nexport default defineAuthorizedHandler(\n {\n permission: [\"todos:read\"],\n query: z.object({ includeDone: z.enum([\"true\", \"false\"]).optional() }),\n dbTables: [\"todos\"],\n dbColumns: [\"todos.title\", \"todos.done\"],\n },\n async (_event, { query, db }) => db.select().from(todos),\n);\n";
14
+ /** Registered blocks and a manifest-only catalog key (the fallback merge test reads it). */
15
+ export declare function manifest(): unknown;
16
+ export declare const BASE_SOURCES: Readonly<Record<string, string>>;
17
+ /** Project the patch set onto a file map, like the IDE's apply-to-VFS projection. */
18
+ export declare function applyPatches(base: ReadonlyMap<string, string>, patches: readonly FilePatch[]): Map<string, string>;
19
+ export interface ContextOptions {
20
+ /** Base-source overrides (the pre-patch project). */
21
+ sources?: Readonly<Record<string, string>>;
22
+ /** Base files to drop (e.g. locales/en.json, to exercise the locale fallback). */
23
+ omit?: readonly string[];
24
+ /** Pre-patch file list; defaults to the base source keys. */
25
+ files?: readonly string[];
26
+ manifest?: unknown;
27
+ syntax?: SyntaxCheck;
28
+ packages?: readonly string[];
29
+ aliases?: readonly ImportAlias[];
30
+ }
31
+ /** Build the effective PatchContext for one patch set. */
32
+ export declare function makeContext(patches?: readonly FilePatch[], options?: ContextOptions): PatchContext;
33
+ export interface ValidateOptions extends ContextOptions {
34
+ requiresConfirmation?: boolean;
35
+ allowedRootFiles?: readonly string[];
36
+ writePolicy?: ValidatePatchOptions["writePolicy"];
37
+ }
38
+ /** Build the context for the patch set, then validate it (the IDE's whole round). */
39
+ export declare function validate(patches: readonly FilePatch[], options?: ValidateOptions): {
40
+ result: ValidationResult;
41
+ context: PatchContext;
42
+ };
43
+ export declare function create(path: string, content: string): FilePatch;
44
+ export declare function update(path: string, content: string): FilePatch;
45
+ export declare function remove(path: string): FilePatch;
46
+ export declare function rulesOf(entries: readonly {
47
+ rule: string;
48
+ }[]): string[];
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Shared fixtures for the validator tests: a realistic todo-app project analyzed with
3
+ * @facilis.dev/analyze, plus the patch overlay applied to its file map.
4
+ *
5
+ * Tests build the effective (post-patch) Analysis exactly as the IDE does — project the
6
+ * patch onto the pre-patch file map and analyze the result — so every rule runs against
7
+ * AST facts instead of a tolerant text scan.
8
+ */
9
+ import { buildProjectContext, validatePatch, } from "../index.js";
10
+ import { normalizeProjectPath } from "../paths.js";
11
+ import { analyzeProjectSources } from "../node.js";
12
+ export const CONFIG = `import { defineConfig } from "@facilis.dev/core";
13
+
14
+ export default defineConfig({
15
+ release: { ref: "origin/main" },
16
+ db: { mode: "pglite", migrationsDir: "db/migrations" },
17
+ i18n: { defaultLocale: "en", locales: ["en"] },
18
+ auth: {
19
+ permissions: ["todos:read", "todos:write"],
20
+ roles: { admin: ["*"], user: ["todos:read", "todos:write"] },
21
+ defaultRole: "user",
22
+ },
23
+ });
24
+ `;
25
+ export const CONFIG_IT = CONFIG.replace('i18n: { defaultLocale: "en", locales: ["en"] },', 'i18n: { defaultLocale: "it", locales: ["en", "it"] },');
26
+ export const SCHEMA = `import { boolean, pgTable, text, uuid } from "drizzle-orm/pg-core";
27
+ import { ownerIsolationPolicy, tenantColumn, tenantIsolationPolicy } from "@facilis.dev/db";
28
+
29
+ export const todos = pgTable(
30
+ "todos",
31
+ {
32
+ id: uuid("id").primaryKey().defaultRandom(),
33
+ tenantId: tenantColumn(),
34
+ ownerId: uuid("owner_id").notNull(),
35
+ title: text("title").notNull(),
36
+ done: boolean("done").notNull().default(false),
37
+ },
38
+ () => [tenantIsolationPolicy(), ownerIsolationPolicy()],
39
+ ).enableRLS();
40
+ `;
41
+ const LOCALES = JSON.stringify({ "app.title": "Facilis todos", "todos.title": "Todos" }, null, 2) + "\n";
42
+ const PACKAGE_JSON = JSON.stringify({
43
+ dependencies: {
44
+ "@facilis.dev/blocks": "workspace:*",
45
+ "@facilis.dev/core": "workspace:*",
46
+ "@facilis.dev/db": "workspace:*",
47
+ "drizzle-orm": "0.45.4",
48
+ react: "19.3.1",
49
+ zod: "4.6.7",
50
+ },
51
+ devDependencies: { typescript: "7.0.2", vite: "8.3.5" },
52
+ }, null, 2) + "\n";
53
+ const TSCONFIG = JSON.stringify({ compilerOptions: { paths: { "~/*": ["./*"] } } }, null, 2) + "\n";
54
+ export const ROUTE_SOURCE = `import { z } from "zod";
55
+ import { defineAuthorizedHandler } from "@facilis.dev/core";
56
+ import { todos } from "~/db/schema";
57
+
58
+ export default defineAuthorizedHandler(
59
+ {
60
+ permission: ["todos:read"],
61
+ query: z.object({ includeDone: z.enum(["true", "false"]).optional() }),
62
+ dbTables: ["todos"],
63
+ dbColumns: ["todos.title", "todos.done"],
64
+ },
65
+ async (_event, { query, db }) => db.select().from(todos),
66
+ );
67
+ `;
68
+ /** Path aliases the Node analyzer gets, mirroring tsconfig's "~/*": ["./*"]. */
69
+ const ANALYZER_ALIASES = [{ prefix: "~/", target: "" }];
70
+ /** Registered blocks and a manifest-only catalog key (the fallback merge test reads it). */
71
+ export function manifest() {
72
+ return {
73
+ blocks: [{ name: "DataTable" }, { name: "ResourceForm" }],
74
+ i18n: { defaultLocale: "en", catalogs: { en: { "manifest.only": "Manifest fallback" } } },
75
+ };
76
+ }
77
+ export const BASE_SOURCES = {
78
+ "facilis.config.ts": CONFIG,
79
+ "db/schema.ts": SCHEMA,
80
+ "locales/en.json": LOCALES,
81
+ "locales/it.json": LOCALES,
82
+ "package.json": PACKAGE_JSON,
83
+ "tsconfig.json": TSCONFIG,
84
+ "server/routes/api/todos/index.get.ts": ROUTE_SOURCE,
85
+ "src/lib/i18n.ts": "export const t = (key: string) => key;\n",
86
+ "src/lib/api.ts": "export const api = true;\n",
87
+ };
88
+ /** Project the patch set onto a file map, like the IDE's apply-to-VFS projection. */
89
+ export function applyPatches(base, patches) {
90
+ const files = new Map(base);
91
+ for (const patch of patches) {
92
+ const path = normalizeProjectPath(patch.path);
93
+ if (patch.action === "delete") {
94
+ files.delete(path);
95
+ continue;
96
+ }
97
+ if (typeof patch.content === "string")
98
+ files.set(path, patch.content);
99
+ }
100
+ return files;
101
+ }
102
+ /** Build the effective PatchContext for one patch set. */
103
+ export function makeContext(patches = [], options = {}) {
104
+ const base = { ...BASE_SOURCES, ...options.sources };
105
+ for (const path of options.omit ?? [])
106
+ delete base[path];
107
+ const baseMap = new Map(Object.entries(base));
108
+ const effectiveMap = applyPatches(baseMap, patches);
109
+ const before = analyzeProjectSources(baseMap, ANALYZER_ALIASES);
110
+ const analysis = analyzeProjectSources(effectiveMap, ANALYZER_ALIASES);
111
+ const sources = {};
112
+ for (const path of ["package.json", "tsconfig.json"]) {
113
+ const content = effectiveMap.get(path);
114
+ if (content !== undefined)
115
+ sources[path] = content;
116
+ }
117
+ const project = buildProjectContext({
118
+ files: options.files ?? [...baseMap.keys()],
119
+ analysis,
120
+ before,
121
+ manifest: options.manifest === undefined ? manifest() : options.manifest,
122
+ sources,
123
+ ...(options.packages === undefined ? {} : { packages: options.packages }),
124
+ ...(options.aliases === undefined ? {} : { aliases: options.aliases }),
125
+ });
126
+ return { ...project, syntax: options.syntax ?? { status: "checked", findings: [] } };
127
+ }
128
+ /** Build the context for the patch set, then validate it (the IDE's whole round). */
129
+ export function validate(patches, options = {}) {
130
+ const context = makeContext(patches, options);
131
+ const overrides = {};
132
+ if (options.requiresConfirmation !== undefined) {
133
+ overrides.requiresConfirmation = options.requiresConfirmation;
134
+ }
135
+ if (options.allowedRootFiles !== undefined)
136
+ overrides.allowedRootFiles = options.allowedRootFiles;
137
+ if (options.writePolicy !== undefined)
138
+ overrides.writePolicy = options.writePolicy;
139
+ const result = validatePatch(patches, { context, ...overrides });
140
+ return { result, context };
141
+ }
142
+ export function create(path, content) {
143
+ return { path, action: "create", content };
144
+ }
145
+ export function update(path, content) {
146
+ return { path, action: "update", content };
147
+ }
148
+ export function remove(path) {
149
+ return { path, action: "delete" };
150
+ }
151
+ export function rulesOf(entries) {
152
+ return entries.map((entry) => entry.rule);
153
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Shared contracts of the SPEC §9.3 patch validator.
3
+ *
4
+ * Project facts now come from @facilis.dev/analyze's AST interpreter (D-092), not from local
5
+ * text scanners. This module only imports analyzer *types* (erased at build), so the main
6
+ * entry keeps its browser-safety guarantee: no analyzer runtime value is loaded.
7
+ *
8
+ * The caller supplies the effective post-patch {@link Analysis} plus, optionally, the
9
+ * pre-patch one. `null` means unknown and `[]` means explicitly empty, exactly as in the
10
+ * analyzer's fact model.
11
+ */
12
+ import type { Analysis, ConfigFacts as AnalyzerConfigFacts, RouteFact as AnalyzerRouteFact, TableFact as AnalyzerTableFact } from "@facilis.dev/analyze";
13
+ export type { Analysis, AnalyzerConfigFacts, AnalyzerRouteFact, AnalyzerTableFact };
14
+ export type { ColumnFact, Diagnostic, FileFacts, ImportFact, JobFact, JobRelationsFact, LocaleCatalogs, RouteElevationUsage, RouteRelationUse, RouteRelationsFact, SourceRef, TranslationCallFact, ViewFact, } from "@facilis.dev/analyze";
15
+ /** Readable aliases for the analyzer facts the rules consume. */
16
+ export type ConfigFacts = AnalyzerConfigFacts;
17
+ export type RouteFact = AnalyzerRouteFact;
18
+ export type TableFact = AnalyzerTableFact;
19
+ /**
20
+ * Rule 11's writable set, selected by the deployment profile (D-167). `managed` keeps the
21
+ * six source roots (D-084); `open` accepts the whole project minus the install, VCS/IDE and
22
+ * instruction surfaces. The default is `managed`, so an external caller changes nothing.
23
+ */
24
+ export type WritePolicy = "open" | "managed";
25
+ /** One proposed file write (SPEC §9.2 response contract). */
26
+ export interface FilePatch {
27
+ path: string;
28
+ action: "create" | "update" | "delete";
29
+ content?: string;
30
+ }
31
+ /** One validator finding (SPEC §7.6): machine-readable rule id plus file and detail. */
32
+ export interface ValidationError {
33
+ rule: string;
34
+ severity: "error" | "warning";
35
+ file: string;
36
+ /** The detail a caller can show or feed back to the model; never prose-free UI copy. */
37
+ message: string;
38
+ /** What to change to clear the finding. */
39
+ fix_hint: string;
40
+ /**
41
+ * The finding is about the validator's own plumbing (parser/analyzer unavailable), not the
42
+ * patch. The IDE must not ask the model to repair it; it retries or reports it to the user.
43
+ */
44
+ infrastructure?: boolean;
45
+ line?: number;
46
+ column?: number;
47
+ /** The offending source line, capped, when it is cheap to provide. */
48
+ excerpt?: string;
49
+ }
50
+ export interface ValidationResult {
51
+ ok: boolean;
52
+ errors: ValidationError[];
53
+ warnings: ValidationError[];
54
+ /**
55
+ * The patch set drops a table or a column (rule 15's predicate). The IDE's incremental
56
+ * write path records it on the turn journal; confirmation stays rule 15's business, so
57
+ * this can be true on a result that is also `ok`.
58
+ */
59
+ destructive?: boolean;
60
+ }
61
+ /** One parse diagnostic from the injected TypeScript service (rule 1). */
62
+ export interface SyntaxIssue {
63
+ line: number;
64
+ column: number;
65
+ message: string;
66
+ }
67
+ export interface SyntaxFinding extends SyntaxIssue {
68
+ file: string;
69
+ }
70
+ /**
71
+ * The rule-1 seam. The IDE injects the analyzer worker's oxc parser; Node tooling injects
72
+ * `@facilis.dev/ai/node` (the subpath is the only place that imports a parser).
73
+ */
74
+ export interface SyntaxChecker {
75
+ check(path: string, content: string): Promise<readonly SyntaxIssue[]>;
76
+ }
77
+ /** Rule 1's input: "unavailable" makes the rule fail closed for script patches. */
78
+ export type SyntaxCheck = {
79
+ status: "checked";
80
+ findings: readonly SyntaxFinding[];
81
+ } | {
82
+ status: "unavailable";
83
+ reason: string;
84
+ };
85
+ /** One registered block component (live manifest only; not a source fact; reserved, D-131). */
86
+ export interface BlockFacts {
87
+ name: string;
88
+ }
89
+ /** A tsconfig `paths` alias such as `"~/": ""` (project-relative target). */
90
+ export interface ImportAlias {
91
+ prefix: string;
92
+ target: string;
93
+ }
94
+ /** Raw material a caller assembles before the validator runs. */
95
+ export interface ProjectContextInput {
96
+ /** Project-relative paths that exist before the patch (the pre-patch file set). */
97
+ files: readonly string[];
98
+ /** The effective (post-patch) analysis: the facts the rules must validate. */
99
+ analysis: Analysis;
100
+ /** The pre-patch analysis, for inverse edits. Defaults to `analysis`. */
101
+ before?: Analysis;
102
+ /** The `/__facilis/manifest` envelope's manifest: blocks and catalog fallback only. */
103
+ manifest?: unknown;
104
+ /** Installed bare specifiers; derived from package.json when omitted. */
105
+ packages?: readonly string[];
106
+ aliases?: readonly ImportAlias[];
107
+ /** Only `package.json` and `tsconfig.json` are read from here; everything else is facts. */
108
+ sources?: Readonly<Record<string, string>>;
109
+ }
110
+ export interface SchemaSummary {
111
+ readonly parsed: boolean;
112
+ readonly tables: readonly TableFact[];
113
+ }
114
+ /** The typed project context every context-dependent rule reads. */
115
+ export interface ProjectContext {
116
+ files: readonly string[];
117
+ /** Effective (post-patch) facts. */
118
+ analysis: Analysis;
119
+ /** Pre-patch facts; equal to `analysis` when the caller has none. */
120
+ before: Analysis;
121
+ config: ConfigFacts;
122
+ schema: SchemaSummary;
123
+ /** Flattened default-locale catalogue (analyzer first, manifest fallback). */
124
+ locales: Readonly<Record<string, string>>;
125
+ blocks: readonly BlockFacts[];
126
+ routes: readonly RouteFact[];
127
+ packages: readonly string[];
128
+ aliases: readonly ImportAlias[];
129
+ }
130
+ /** The complete validator input: the project context plus the rule-1 result. */
131
+ export interface PatchContext extends ProjectContext {
132
+ syntax: SyntaxCheck;
133
+ }
134
+ export interface ValidatePatchOptions {
135
+ /** Without a context only the context-free structural rules run (parser gate). */
136
+ context?: PatchContext;
137
+ /** The AIResponse `requires_confirmation` flag, for rule 15. */
138
+ requiresConfirmation?: boolean;
139
+ /**
140
+ * Extra root files this write may touch. Only the IDE's own writers pass it (e.g. the
141
+ * AGENTS.md scaffold); AI patches never do, so the model cannot persist instructions.
142
+ */
143
+ allowedRootFiles?: readonly string[];
144
+ /** Rule 11's writable set (D-167). Defaults to `managed`, the strict roots. */
145
+ writePolicy?: WritePolicy;
146
+ }
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared contracts of the SPEC §9.3 patch validator.
3
+ *
4
+ * Project facts now come from @facilis.dev/analyze's AST interpreter (D-092), not from local
5
+ * text scanners. This module only imports analyzer *types* (erased at build), so the main
6
+ * entry keeps its browser-safety guarantee: no analyzer runtime value is loaded.
7
+ *
8
+ * The caller supplies the effective post-patch {@link Analysis} plus, optionally, the
9
+ * pre-patch one. `null` means unknown and `[]` means explicitly empty, exactly as in the
10
+ * analyzer's fact model.
11
+ */
12
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The SPEC §9.3 patch validator: 19 rules, grouped by the part of the project they read.
3
+ *
4
+ * Without a context only the two context-free structural checks run (the parser gate);
5
+ * with a context the full rule set runs. Rules 5, 13, 14 and 17 are warnings (they never
6
+ * block); the others are errors and make `ok` false, which is what stops the IDE's apply
7
+ * path before it writes anything (invariant 9).
8
+ *
9
+ * Every contextual rule reads @facilis.dev/analyze's fact model (D-092): the effective
10
+ * post-patch Analysis supplied by the caller, never a re-parse of the patch text.
11
+ */
12
+ import type { FilePatch, ValidatePatchOptions, ValidationResult } from "./types.js";
13
+ export declare function validatePatch(patches: readonly FilePatch[], options?: ValidatePatchOptions): ValidationResult;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The SPEC §9.3 patch validator: 19 rules, grouped by the part of the project they read.
3
+ *
4
+ * Without a context only the two context-free structural checks run (the parser gate);
5
+ * with a context the full rule set runs. Rules 5, 13, 14 and 17 are warnings (they never
6
+ * block); the others are errors and make `ok` false, which is what stops the IDE's apply
7
+ * path before it writes anything (invariant 9).
8
+ *
9
+ * Every contextual rule reads @facilis.dev/analyze's fact model (D-092): the effective
10
+ * post-patch Analysis supplied by the caller, never a re-parse of the patch text.
11
+ */
12
+ import { buildEffectiveState } from "./overlay.js";
13
+ import { blockImportErrors } from "./rules/blocks.js";
14
+ import { roleConfigSyncErrors } from "./rules/config-sync.js";
15
+ import { destructiveChanges, destructiveErrors, migrationWarnings, rlsErrors, } from "./rules/database.js";
16
+ import { i18nWarnings } from "./rules/i18n.js";
17
+ import { importResolveErrors, rawDbAccessErrors } from "./rules/imports.js";
18
+ import { jobTableDeclaredFindings } from "./rules/job-relations.js";
19
+ import { routeTableDeclaredFindings } from "./rules/route-relations.js";
20
+ import { routeDescriptionWarnings } from "./rules/routes-description.js";
21
+ import { permissionErrors, routeDbColumnsWarnings, routeDbTablesErrors, } from "./rules/routes-metadata.js";
22
+ import { routeDbTablesPresentErrors, routeInputErrors, routeSecurityErrors, } from "./rules/routes-security.js";
23
+ import { structuralErrors } from "./rules/structural.js";
24
+ import { tsParseErrors } from "./rules/ts-parse.js";
25
+ export function validatePatch(patches, options = {}) {
26
+ const errors = structuralErrors(patches, {
27
+ allowedRootFiles: options.allowedRootFiles ?? [],
28
+ writePolicy: options.writePolicy ?? "managed",
29
+ });
30
+ const warnings = [];
31
+ let destructive = false;
32
+ const context = options.context;
33
+ if (context !== undefined) {
34
+ const state = buildEffectiveState(context, patches);
35
+ destructive = destructiveChanges(state).length > 0;
36
+ errors.push(...tsParseErrors(patches, context.syntax));
37
+ errors.push(...routeSecurityErrors(state));
38
+ errors.push(...routeDbTablesPresentErrors(state));
39
+ errors.push(...routeDbTablesErrors(state));
40
+ warnings.push(...routeDbColumnsWarnings(state));
41
+ errors.push(...permissionErrors(state));
42
+ errors.push(...blockImportErrors(state, context));
43
+ errors.push(...routeInputErrors(state));
44
+ errors.push(...rawDbAccessErrors(state));
45
+ errors.push(...rlsErrors(state));
46
+ errors.push(...importResolveErrors(state, context));
47
+ warnings.push(...i18nWarnings(state));
48
+ warnings.push(...migrationWarnings(patches, state));
49
+ errors.push(...destructiveErrors(state, options.requiresConfirmation === true));
50
+ errors.push(...roleConfigSyncErrors(patches, state));
51
+ warnings.push(...routeDescriptionWarnings(state));
52
+ // D-157 rule 18: dbTables is applied over every effective route, not only patched ones.
53
+ const routeTables = routeTableDeclaredFindings(state);
54
+ errors.push(...routeTables.errors);
55
+ warnings.push(...routeTables.warnings);
56
+ // D-157 rule 19: the same declaration, applied to every effective job's system client.
57
+ const jobTables = jobTableDeclaredFindings(state);
58
+ errors.push(...jobTables.errors);
59
+ warnings.push(...jobTables.warnings);
60
+ }
61
+ return { ok: errors.length === 0, errors, warnings, destructive };
62
+ }
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@facilis.dev/ai",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "Facilis AI orchestration: prompt assembly, response parsing and patch validation.",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./node": "./dist/node.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=22.15.0"
20
+ },
21
+ "dependencies": {
22
+ "@facilis.dev/analyze": "0.1.1"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/facilis-dev/facilis.git",
30
+ "directory": "packages/ai"
31
+ },
32
+ "homepage": "https://github.com/facilis-dev/facilis/tree/main/packages/ai#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/facilis-dev/facilis/issues"
35
+ },
36
+ "keywords": [
37
+ "facilis",
38
+ "framework",
39
+ "vite",
40
+ "nitro",
41
+ "low-code"
42
+ ],
43
+ "main": "./dist/index.js",
44
+ "types": "./dist/index.d.ts"
45
+ }
package/src/context.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Assembles the typed project context the validator rules read.
3
+ *
4
+ * After D-092 this is a pure mapping, not a parser: the caller hands in the effective
5
+ * (post-patch) Analysis and optionally the pre-patch one, and the facts are projected onto
6
+ * the validator's contract. The only bytes read here are package.json and tsconfig.json
7
+ * (installed packages and path aliases), which the analyzer does not model as facts.
8
+ * Everything is browser-safe.
9
+ */
10
+
11
+ import { factsFromManifest } from "./manifest-facts.ts";
12
+ import { normalizeProjectPath, SCHEMA_PATH } from "./paths.ts";
13
+ import type {
14
+ Analysis,
15
+ BlockFacts,
16
+ ConfigFacts,
17
+ ImportAlias,
18
+ ProjectContext,
19
+ ProjectContextInput,
20
+ RouteFact,
21
+ TableFact,
22
+ } from "./types.ts";
23
+
24
+ function isRecord(value: unknown): value is Record<string, unknown> {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+
28
+ function normalizeSources(
29
+ sources: Readonly<Record<string, string>> | undefined,
30
+ ): Record<string, string> {
31
+ const normalized: Record<string, string> = {};
32
+ for (const [path, content] of Object.entries(sources ?? {})) {
33
+ normalized[normalizeProjectPath(path)] = content;
34
+ }
35
+ return normalized;
36
+ }
37
+
38
+ function parseJsonRecord(text: string): Record<string, unknown> | null {
39
+ try {
40
+ const parsed: unknown = JSON.parse(text);
41
+ return isRecord(parsed) ? parsed : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** Installed bare specifiers from the project's package.json. */
48
+ function packagesFromSource(source: string | undefined): string[] {
49
+ if (source === undefined) return [];
50
+ const parsed = parseJsonRecord(source);
51
+ if (parsed === null) return [];
52
+ const names = new Set<string>();
53
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
54
+ const section = parsed[field];
55
+ if (!isRecord(section)) continue;
56
+ for (const name of Object.keys(section)) names.add(name);
57
+ }
58
+ return [...names];
59
+ }
60
+
61
+ /** `tsconfig.json` `paths` aliases, normalized to project-relative targets. */
62
+ function aliasesFromSource(source: string | undefined): ImportAlias[] {
63
+ const aliases: ImportAlias[] = [{ prefix: "~/", target: "" }];
64
+ if (source === undefined) return aliases;
65
+ const parsed = parseJsonRecord(source);
66
+ const options =
67
+ parsed !== null && isRecord(parsed.compilerOptions) ? parsed.compilerOptions : null;
68
+ const paths = options !== null && isRecord(options.paths) ? options.paths : null;
69
+ if (paths === null) return aliases;
70
+ for (const [pattern, targets] of Object.entries(paths)) {
71
+ if (!Array.isArray(targets) || targets.length === 0) continue;
72
+ const target = targets[0];
73
+ if (typeof target !== "string") continue;
74
+ const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
75
+ const resolved = target.replace(/^\.\//, "").replace(/\*$/, "").replace(/\/$/, "");
76
+ aliases.push({ prefix, target: resolved });
77
+ }
78
+ return aliases;
79
+ }
80
+
81
+ /**
82
+ * The default-locale catalogue: the analyzer's facts first, the manifest only underneath
83
+ * as a fallback. `analysis.locales.en` is the generated project's source of truth; when
84
+ * the project's default locale is not English, its catalogue is the next best fact.
85
+ */
86
+ function flattenCatalog(analysis: Analysis, config: ConfigFacts): Record<string, string> {
87
+ const defaultLocale = config.defaultLocale ?? "en";
88
+ const analyzerCatalog =
89
+ analysis.locales["en"] ?? analysis.locales[defaultLocale] ?? ({} as Record<string, string>);
90
+ return { ...analyzerCatalog };
91
+ }
92
+
93
+ /** Build the typed context from analyzer facts; the manifest only contributes fallbacks. */
94
+ export function buildProjectContext(input: ProjectContextInput): ProjectContext {
95
+ const sources = normalizeSources(input.sources);
96
+ const manifest = factsFromManifest(input.manifest);
97
+ const analysis = input.analysis;
98
+ const before = input.before ?? analysis;
99
+ const config = analysis.config;
100
+ const analyzerCatalog = flattenCatalog(analysis, config);
101
+ const locales: Record<string, string> = { ...manifest.locales, ...analyzerCatalog };
102
+ const blocks: readonly BlockFacts[] = manifest.blocks;
103
+ const routes: readonly RouteFact[] = analysis.routes;
104
+ const tables: readonly TableFact[] = analysis.tables;
105
+ return {
106
+ files: input.files.map(normalizeProjectPath).filter((path) => path.length > 0),
107
+ analysis,
108
+ before,
109
+ config,
110
+ schema: { parsed: analysis.files.includes(SCHEMA_PATH), tables },
111
+ locales,
112
+ blocks,
113
+ routes,
114
+ packages: input.packages ?? packagesFromSource(sources["package.json"]),
115
+ aliases: input.aliases ?? aliasesFromSource(sources["tsconfig.json"]),
116
+ };
117
+ }
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @facilis.dev/ai — the SPEC §9.3 patch validator and the contracts it shares with the IDE
3
+ * chat (patches, findings, project context).
4
+ *
5
+ * This entry must stay browser-safe: no TypeScript/oxc parser, no Node builtin, no
6
+ * filesystem. Rule 1's parser is injected as a {@link SyntaxChecker} (the IDE passes the
7
+ * analyzer worker's oxc parser; Node tooling imports the `@facilis.dev/ai/node` subpath, the
8
+ * only module that imports a parser). Project facts come from @facilis.dev/analyze's Analysis,
9
+ * which the caller builds from the VFS file map; analyzer imports here are type-only except
10
+ * the route-description rule's shared character limit (D-129), a plain constant with no
11
+ * parser or Node dependency, so nothing heavy is bundled into the IDE.
12
+ */
13
+
14
+ export { buildProjectContext } from "./context.ts";
15
+ export { checkPatchSyntax } from "./syntax-runner.ts";
16
+ export type {
17
+ Analysis,
18
+ BlockFacts,
19
+ ColumnFact,
20
+ ConfigFacts,
21
+ Diagnostic,
22
+ FileFacts,
23
+ FilePatch,
24
+ ImportAlias,
25
+ ImportFact,
26
+ LocaleCatalogs,
27
+ PatchContext,
28
+ ProjectContext,
29
+ ProjectContextInput,
30
+ RouteFact,
31
+ SchemaSummary,
32
+ SourceRef,
33
+ SyntaxCheck,
34
+ SyntaxChecker,
35
+ SyntaxFinding,
36
+ SyntaxIssue,
37
+ TableFact,
38
+ TranslationCallFact,
39
+ ValidatePatchOptions,
40
+ ValidationError,
41
+ ValidationResult,
42
+ WritePolicy,
43
+ } from "./types.ts";
44
+ export { validatePatch } from "./validator.ts";