@facilis.dev/ai 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/context.d.ts +12 -0
- package/dist/context.js +104 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/manifest-facts.d.ts +16 -0
- package/dist/manifest-facts.js +45 -0
- package/dist/node.d.ts +19 -0
- package/dist/node.js +60 -0
- package/dist/overlay.d.ts +41 -0
- package/dist/overlay.js +91 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +32 -0
- package/dist/rules/blocks.d.ts +12 -0
- package/dist/rules/blocks.js +64 -0
- package/dist/rules/config-sync.d.ts +9 -0
- package/dist/rules/config-sync.js +64 -0
- package/dist/rules/database.d.ts +21 -0
- package/dist/rules/database.js +100 -0
- package/dist/rules/i18n.d.ts +9 -0
- package/dist/rules/i18n.js +40 -0
- package/dist/rules/imports.d.ts +15 -0
- package/dist/rules/imports.js +194 -0
- package/dist/rules/job-relations.d.ts +23 -0
- package/dist/rules/job-relations.js +84 -0
- package/dist/rules/route-relations.d.ts +20 -0
- package/dist/rules/route-relations.js +113 -0
- package/dist/rules/routes-description.d.ts +14 -0
- package/dist/rules/routes-description.js +86 -0
- package/dist/rules/routes-metadata.d.ts +16 -0
- package/dist/rules/routes-metadata.js +169 -0
- package/dist/rules/routes-security.d.ts +22 -0
- package/dist/rules/routes-security.js +52 -0
- package/dist/rules/structural.d.ts +7 -0
- package/dist/rules/structural.js +33 -0
- package/dist/rules/ts-parse.d.ts +16 -0
- package/dist/rules/ts-parse.js +62 -0
- package/dist/syntax-runner.d.ts +8 -0
- package/dist/syntax-runner.js +25 -0
- package/dist/testing/fixtures.d.ts +47 -0
- package/dist/testing/fixtures.js +151 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +12 -0
- package/dist/validator.d.ts +13 -0
- package/dist/validator.js +57 -0
- package/package.json +43 -4
- package/src/context.ts +117 -0
- package/src/index.ts +43 -0
- package/src/manifest-facts.ts +54 -0
- package/src/node.ts +75 -0
- package/src/overlay.ts +131 -0
- package/src/paths.ts +38 -0
- package/src/rules/blocks.ts +67 -0
- package/src/rules/config-sync.ts +72 -0
- package/src/rules/database.ts +133 -0
- package/src/rules/i18n.ts +42 -0
- package/src/rules/imports.ts +227 -0
- package/src/rules/job-relations.ts +126 -0
- package/src/rules/route-relations.ts +162 -0
- package/src/rules/routes-description.ts +113 -0
- package/src/rules/routes-metadata.ts +241 -0
- package/src/rules/routes-security.ts +90 -0
- package/src/rules/structural.ts +63 -0
- package/src/rules/ts-parse.ts +71 -0
- package/src/syntax-runner.ts +33 -0
- package/src/testing/fixtures.ts +210 -0
- package/src/types.ts +164 -0
- package/src/validator.ts +71 -0
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
|
|
10
|
+
import {
|
|
11
|
+
buildProjectContext,
|
|
12
|
+
validatePatch,
|
|
13
|
+
type FilePatch,
|
|
14
|
+
type ImportAlias,
|
|
15
|
+
type PatchContext,
|
|
16
|
+
type SyntaxCheck,
|
|
17
|
+
type ValidatePatchOptions,
|
|
18
|
+
type ValidationResult,
|
|
19
|
+
} from "../index.ts";
|
|
20
|
+
import { normalizeProjectPath } from "../paths.ts";
|
|
21
|
+
import { analyzeProjectSources } from "../node.ts";
|
|
22
|
+
|
|
23
|
+
export const CONFIG = `import { defineConfig } from "@facilis.dev/core";
|
|
24
|
+
|
|
25
|
+
export default defineConfig({
|
|
26
|
+
release: { ref: "origin/main" },
|
|
27
|
+
db: { mode: "pglite", migrationsDir: "db/migrations" },
|
|
28
|
+
i18n: { defaultLocale: "en", locales: ["en"] },
|
|
29
|
+
auth: {
|
|
30
|
+
permissions: ["todos:read", "todos:write"],
|
|
31
|
+
roles: { admin: ["*"], user: ["todos:read", "todos:write"] },
|
|
32
|
+
defaultRole: "user",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export const CONFIG_IT = CONFIG.replace(
|
|
38
|
+
'i18n: { defaultLocale: "en", locales: ["en"] },',
|
|
39
|
+
'i18n: { defaultLocale: "it", locales: ["en", "it"] },',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const SCHEMA = `import { boolean, pgTable, text, uuid } from "drizzle-orm/pg-core";
|
|
43
|
+
import { ownerIsolationPolicy, tenantColumn, tenantIsolationPolicy } from "@facilis.dev/db";
|
|
44
|
+
|
|
45
|
+
export const todos = pgTable(
|
|
46
|
+
"todos",
|
|
47
|
+
{
|
|
48
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
49
|
+
tenantId: tenantColumn(),
|
|
50
|
+
ownerId: uuid("owner_id").notNull(),
|
|
51
|
+
title: text("title").notNull(),
|
|
52
|
+
done: boolean("done").notNull().default(false),
|
|
53
|
+
},
|
|
54
|
+
() => [tenantIsolationPolicy(), ownerIsolationPolicy()],
|
|
55
|
+
).enableRLS();
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const LOCALES =
|
|
59
|
+
JSON.stringify({ "app.title": "Facilis todos", "todos.title": "Todos" }, null, 2) + "\n";
|
|
60
|
+
|
|
61
|
+
const PACKAGE_JSON =
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{
|
|
64
|
+
dependencies: {
|
|
65
|
+
"@facilis.dev/blocks": "workspace:*",
|
|
66
|
+
"@facilis.dev/core": "workspace:*",
|
|
67
|
+
"@facilis.dev/db": "workspace:*",
|
|
68
|
+
"drizzle-orm": "0.45.4",
|
|
69
|
+
react: "19.3.1",
|
|
70
|
+
zod: "4.6.7",
|
|
71
|
+
},
|
|
72
|
+
devDependencies: { typescript: "7.0.2", vite: "8.3.5" },
|
|
73
|
+
},
|
|
74
|
+
null,
|
|
75
|
+
2,
|
|
76
|
+
) + "\n";
|
|
77
|
+
|
|
78
|
+
const TSCONFIG = JSON.stringify({ compilerOptions: { paths: { "~/*": ["./*"] } } }, null, 2) + "\n";
|
|
79
|
+
|
|
80
|
+
export const ROUTE_SOURCE = `import { z } from "zod";
|
|
81
|
+
import { defineAuthorizedHandler } from "@facilis.dev/core";
|
|
82
|
+
import { todos } from "~/db/schema";
|
|
83
|
+
|
|
84
|
+
export default defineAuthorizedHandler(
|
|
85
|
+
{
|
|
86
|
+
permission: ["todos:read"],
|
|
87
|
+
query: z.object({ includeDone: z.enum(["true", "false"]).optional() }),
|
|
88
|
+
dbTables: ["todos"],
|
|
89
|
+
dbColumns: ["todos.title", "todos.done"],
|
|
90
|
+
},
|
|
91
|
+
async (_event, { query, db }) => db.select().from(todos),
|
|
92
|
+
);
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
/** Path aliases the Node analyzer gets, mirroring tsconfig's "~/*": ["./*"]. */
|
|
96
|
+
const ANALYZER_ALIASES: readonly ImportAlias[] = [{ prefix: "~/", target: "" }];
|
|
97
|
+
|
|
98
|
+
/** Registered blocks and a manifest-only catalog key (the fallback merge test reads it). */
|
|
99
|
+
export function manifest(): unknown {
|
|
100
|
+
return {
|
|
101
|
+
blocks: [{ name: "DataTable" }, { name: "ResourceForm" }],
|
|
102
|
+
i18n: { defaultLocale: "en", catalogs: { en: { "manifest.only": "Manifest fallback" } } },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const BASE_SOURCES: Readonly<Record<string, string>> = {
|
|
107
|
+
"facilis.config.ts": CONFIG,
|
|
108
|
+
"db/schema.ts": SCHEMA,
|
|
109
|
+
"locales/en.json": LOCALES,
|
|
110
|
+
"locales/it.json": LOCALES,
|
|
111
|
+
"package.json": PACKAGE_JSON,
|
|
112
|
+
"tsconfig.json": TSCONFIG,
|
|
113
|
+
"server/routes/api/todos/index.get.ts": ROUTE_SOURCE,
|
|
114
|
+
"src/lib/i18n.ts": "export const t = (key: string) => key;\n",
|
|
115
|
+
"src/lib/api.ts": "export const api = true;\n",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Project the patch set onto a file map, like the IDE's apply-to-VFS projection. */
|
|
119
|
+
export function applyPatches(
|
|
120
|
+
base: ReadonlyMap<string, string>,
|
|
121
|
+
patches: readonly FilePatch[],
|
|
122
|
+
): Map<string, string> {
|
|
123
|
+
const files = new Map(base);
|
|
124
|
+
for (const patch of patches) {
|
|
125
|
+
const path = normalizeProjectPath(patch.path);
|
|
126
|
+
if (patch.action === "delete") {
|
|
127
|
+
files.delete(path);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (typeof patch.content === "string") files.set(path, patch.content);
|
|
131
|
+
}
|
|
132
|
+
return files;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ContextOptions {
|
|
136
|
+
/** Base-source overrides (the pre-patch project). */
|
|
137
|
+
sources?: Readonly<Record<string, string>>;
|
|
138
|
+
/** Base files to drop (e.g. locales/en.json, to exercise the locale fallback). */
|
|
139
|
+
omit?: readonly string[];
|
|
140
|
+
/** Pre-patch file list; defaults to the base source keys. */
|
|
141
|
+
files?: readonly string[];
|
|
142
|
+
manifest?: unknown;
|
|
143
|
+
syntax?: SyntaxCheck;
|
|
144
|
+
packages?: readonly string[];
|
|
145
|
+
aliases?: readonly ImportAlias[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Build the effective PatchContext for one patch set. */
|
|
149
|
+
export function makeContext(
|
|
150
|
+
patches: readonly FilePatch[] = [],
|
|
151
|
+
options: ContextOptions = {},
|
|
152
|
+
): PatchContext {
|
|
153
|
+
const base: Record<string, string> = { ...BASE_SOURCES, ...options.sources };
|
|
154
|
+
for (const path of options.omit ?? []) delete base[path];
|
|
155
|
+
const baseMap = new Map(Object.entries(base));
|
|
156
|
+
const effectiveMap = applyPatches(baseMap, patches);
|
|
157
|
+
const before = analyzeProjectSources(baseMap, ANALYZER_ALIASES);
|
|
158
|
+
const analysis = analyzeProjectSources(effectiveMap, ANALYZER_ALIASES);
|
|
159
|
+
const sources: Record<string, string> = {};
|
|
160
|
+
for (const path of ["package.json", "tsconfig.json"]) {
|
|
161
|
+
const content = effectiveMap.get(path);
|
|
162
|
+
if (content !== undefined) sources[path] = content;
|
|
163
|
+
}
|
|
164
|
+
const project = buildProjectContext({
|
|
165
|
+
files: options.files ?? [...baseMap.keys()],
|
|
166
|
+
analysis,
|
|
167
|
+
before,
|
|
168
|
+
manifest: options.manifest === undefined ? manifest() : options.manifest,
|
|
169
|
+
sources,
|
|
170
|
+
...(options.packages === undefined ? {} : { packages: options.packages }),
|
|
171
|
+
...(options.aliases === undefined ? {} : { aliases: options.aliases }),
|
|
172
|
+
});
|
|
173
|
+
return { ...project, syntax: options.syntax ?? { status: "checked", findings: [] } };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ValidateOptions extends ContextOptions {
|
|
177
|
+
requiresConfirmation?: boolean;
|
|
178
|
+
allowedRootFiles?: readonly string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Build the context for the patch set, then validate it (the IDE's whole round). */
|
|
182
|
+
export function validate(
|
|
183
|
+
patches: readonly FilePatch[],
|
|
184
|
+
options: ValidateOptions = {},
|
|
185
|
+
): { result: ValidationResult; context: PatchContext } {
|
|
186
|
+
const context = makeContext(patches, options);
|
|
187
|
+
const overrides: Pick<ValidatePatchOptions, "requiresConfirmation" | "allowedRootFiles"> = {};
|
|
188
|
+
if (options.requiresConfirmation !== undefined) {
|
|
189
|
+
overrides.requiresConfirmation = options.requiresConfirmation;
|
|
190
|
+
}
|
|
191
|
+
if (options.allowedRootFiles !== undefined) overrides.allowedRootFiles = options.allowedRootFiles;
|
|
192
|
+
const result = validatePatch(patches, { context, ...overrides });
|
|
193
|
+
return { result, context };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function create(path: string, content: string): FilePatch {
|
|
197
|
+
return { path, action: "create", content };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function update(path: string, content: string): FilePatch {
|
|
201
|
+
return { path, action: "update", content };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function remove(path: string): FilePatch {
|
|
205
|
+
return { path, action: "delete" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function rulesOf(entries: readonly { rule: string }[]): string[] {
|
|
209
|
+
return entries.map((entry) => entry.rule);
|
|
210
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
|
|
13
|
+
import type {
|
|
14
|
+
Analysis,
|
|
15
|
+
ConfigFacts as AnalyzerConfigFacts,
|
|
16
|
+
RouteFact as AnalyzerRouteFact,
|
|
17
|
+
TableFact as AnalyzerTableFact,
|
|
18
|
+
} from "@facilis.dev/analyze";
|
|
19
|
+
|
|
20
|
+
export type { Analysis, AnalyzerConfigFacts, AnalyzerRouteFact, AnalyzerTableFact };
|
|
21
|
+
export type {
|
|
22
|
+
ColumnFact,
|
|
23
|
+
Diagnostic,
|
|
24
|
+
FileFacts,
|
|
25
|
+
ImportFact,
|
|
26
|
+
JobFact,
|
|
27
|
+
JobRelationsFact,
|
|
28
|
+
LocaleCatalogs,
|
|
29
|
+
RouteElevationUsage,
|
|
30
|
+
RouteRelationUse,
|
|
31
|
+
RouteRelationsFact,
|
|
32
|
+
SourceRef,
|
|
33
|
+
TranslationCallFact,
|
|
34
|
+
ViewFact,
|
|
35
|
+
} from "@facilis.dev/analyze";
|
|
36
|
+
|
|
37
|
+
/** Readable aliases for the analyzer facts the rules consume. */
|
|
38
|
+
export type ConfigFacts = AnalyzerConfigFacts;
|
|
39
|
+
export type RouteFact = AnalyzerRouteFact;
|
|
40
|
+
export type TableFact = AnalyzerTableFact;
|
|
41
|
+
|
|
42
|
+
/** One proposed file write (SPEC §9.2 response contract). */
|
|
43
|
+
export interface FilePatch {
|
|
44
|
+
path: string;
|
|
45
|
+
action: "create" | "update" | "delete";
|
|
46
|
+
content?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One validator finding (SPEC §7.6): machine-readable rule id plus file and detail. */
|
|
50
|
+
export interface ValidationError {
|
|
51
|
+
rule: string;
|
|
52
|
+
severity: "error" | "warning";
|
|
53
|
+
file: string;
|
|
54
|
+
/** The detail a caller can show or feed back to the model; never prose-free UI copy. */
|
|
55
|
+
message: string;
|
|
56
|
+
/** What to change to clear the finding. */
|
|
57
|
+
fix_hint: string;
|
|
58
|
+
/**
|
|
59
|
+
* The finding is about the validator's own plumbing (parser/analyzer unavailable), not the
|
|
60
|
+
* patch. The IDE must not ask the model to repair it; it retries or reports it to the user.
|
|
61
|
+
*/
|
|
62
|
+
infrastructure?: boolean;
|
|
63
|
+
line?: number;
|
|
64
|
+
column?: number;
|
|
65
|
+
/** The offending source line, capped, when it is cheap to provide. */
|
|
66
|
+
excerpt?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ValidationResult {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
errors: ValidationError[];
|
|
72
|
+
warnings: ValidationError[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One parse diagnostic from the injected TypeScript service (rule 1). */
|
|
76
|
+
export interface SyntaxIssue {
|
|
77
|
+
line: number;
|
|
78
|
+
column: number;
|
|
79
|
+
message: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SyntaxFinding extends SyntaxIssue {
|
|
83
|
+
file: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The rule-1 seam. The IDE injects the analyzer worker's oxc parser; Node tooling injects
|
|
88
|
+
* `@facilis.dev/ai/node` (the subpath is the only place that imports a parser).
|
|
89
|
+
*/
|
|
90
|
+
export interface SyntaxChecker {
|
|
91
|
+
check(path: string, content: string): Promise<readonly SyntaxIssue[]>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Rule 1's input: "unavailable" makes the rule fail closed for script patches. */
|
|
95
|
+
export type SyntaxCheck =
|
|
96
|
+
| { status: "checked"; findings: readonly SyntaxFinding[] }
|
|
97
|
+
| { status: "unavailable"; reason: string };
|
|
98
|
+
|
|
99
|
+
/** One registered block component (live manifest only; not a source fact; reserved, D-131). */
|
|
100
|
+
export interface BlockFacts {
|
|
101
|
+
name: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A tsconfig `paths` alias such as `"~/": ""` (project-relative target). */
|
|
105
|
+
export interface ImportAlias {
|
|
106
|
+
prefix: string;
|
|
107
|
+
target: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Raw material a caller assembles before the validator runs. */
|
|
111
|
+
export interface ProjectContextInput {
|
|
112
|
+
/** Project-relative paths that exist before the patch (the pre-patch file set). */
|
|
113
|
+
files: readonly string[];
|
|
114
|
+
/** The effective (post-patch) analysis: the facts the rules must validate. */
|
|
115
|
+
analysis: Analysis;
|
|
116
|
+
/** The pre-patch analysis, for inverse edits. Defaults to `analysis`. */
|
|
117
|
+
before?: Analysis;
|
|
118
|
+
/** The `/__facilis/manifest` envelope's manifest: blocks and catalog fallback only. */
|
|
119
|
+
manifest?: unknown;
|
|
120
|
+
/** Installed bare specifiers; derived from package.json when omitted. */
|
|
121
|
+
packages?: readonly string[];
|
|
122
|
+
aliases?: readonly ImportAlias[];
|
|
123
|
+
/** Only `package.json` and `tsconfig.json` are read from here; everything else is facts. */
|
|
124
|
+
sources?: Readonly<Record<string, string>>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SchemaSummary {
|
|
128
|
+
readonly parsed: boolean;
|
|
129
|
+
readonly tables: readonly TableFact[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The typed project context every context-dependent rule reads. */
|
|
133
|
+
export interface ProjectContext {
|
|
134
|
+
files: readonly string[];
|
|
135
|
+
/** Effective (post-patch) facts. */
|
|
136
|
+
analysis: Analysis;
|
|
137
|
+
/** Pre-patch facts; equal to `analysis` when the caller has none. */
|
|
138
|
+
before: Analysis;
|
|
139
|
+
config: ConfigFacts;
|
|
140
|
+
schema: SchemaSummary;
|
|
141
|
+
/** Flattened default-locale catalogue (analyzer first, manifest fallback). */
|
|
142
|
+
locales: Readonly<Record<string, string>>;
|
|
143
|
+
blocks: readonly BlockFacts[];
|
|
144
|
+
routes: readonly RouteFact[];
|
|
145
|
+
packages: readonly string[];
|
|
146
|
+
aliases: readonly ImportAlias[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The complete validator input: the project context plus the rule-1 result. */
|
|
150
|
+
export interface PatchContext extends ProjectContext {
|
|
151
|
+
syntax: SyntaxCheck;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ValidatePatchOptions {
|
|
155
|
+
/** Without a context only the context-free structural rules run (parser gate). */
|
|
156
|
+
context?: PatchContext;
|
|
157
|
+
/** The AIResponse `requires_confirmation` flag, for rule 15. */
|
|
158
|
+
requiresConfirmation?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Extra root files this write may touch. Only the IDE's own writers pass it (e.g. the
|
|
161
|
+
* AGENTS.md scaffold); AI patches never do, so the model cannot persist instructions.
|
|
162
|
+
*/
|
|
163
|
+
allowedRootFiles?: readonly string[];
|
|
164
|
+
}
|
package/src/validator.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
|
|
13
|
+
import { buildEffectiveState } from "./overlay.ts";
|
|
14
|
+
import { blockImportErrors } from "./rules/blocks.ts";
|
|
15
|
+
import { roleConfigSyncErrors } from "./rules/config-sync.ts";
|
|
16
|
+
import { destructiveErrors, migrationWarnings, rlsErrors } from "./rules/database.ts";
|
|
17
|
+
import { i18nWarnings } from "./rules/i18n.ts";
|
|
18
|
+
import { importResolveErrors, rawDbAccessErrors } from "./rules/imports.ts";
|
|
19
|
+
import { jobTableDeclaredFindings } from "./rules/job-relations.ts";
|
|
20
|
+
import { routeTableDeclaredFindings } from "./rules/route-relations.ts";
|
|
21
|
+
import { routeDescriptionWarnings } from "./rules/routes-description.ts";
|
|
22
|
+
import {
|
|
23
|
+
permissionErrors,
|
|
24
|
+
routeDbColumnsWarnings,
|
|
25
|
+
routeDbTablesErrors,
|
|
26
|
+
} from "./rules/routes-metadata.ts";
|
|
27
|
+
import {
|
|
28
|
+
routeDbTablesPresentErrors,
|
|
29
|
+
routeInputErrors,
|
|
30
|
+
routeSecurityErrors,
|
|
31
|
+
} from "./rules/routes-security.ts";
|
|
32
|
+
import { structuralErrors } from "./rules/structural.ts";
|
|
33
|
+
import { tsParseErrors } from "./rules/ts-parse.ts";
|
|
34
|
+
import type { FilePatch, ValidatePatchOptions, ValidationResult } from "./types.ts";
|
|
35
|
+
|
|
36
|
+
export function validatePatch(
|
|
37
|
+
patches: readonly FilePatch[],
|
|
38
|
+
options: ValidatePatchOptions = {},
|
|
39
|
+
): ValidationResult {
|
|
40
|
+
const errors = structuralErrors(patches, options.allowedRootFiles);
|
|
41
|
+
const warnings: ValidationResult["warnings"] = [];
|
|
42
|
+
const context = options.context;
|
|
43
|
+
if (context !== undefined) {
|
|
44
|
+
const state = buildEffectiveState(context, patches);
|
|
45
|
+
errors.push(...tsParseErrors(patches, context.syntax));
|
|
46
|
+
errors.push(...routeSecurityErrors(state));
|
|
47
|
+
errors.push(...routeDbTablesPresentErrors(state));
|
|
48
|
+
errors.push(...routeDbTablesErrors(state));
|
|
49
|
+
warnings.push(...routeDbColumnsWarnings(state));
|
|
50
|
+
errors.push(...permissionErrors(state));
|
|
51
|
+
errors.push(...blockImportErrors(state, context));
|
|
52
|
+
errors.push(...routeInputErrors(state));
|
|
53
|
+
errors.push(...rawDbAccessErrors(state));
|
|
54
|
+
errors.push(...rlsErrors(state));
|
|
55
|
+
errors.push(...importResolveErrors(state, context));
|
|
56
|
+
warnings.push(...i18nWarnings(state));
|
|
57
|
+
warnings.push(...migrationWarnings(patches, state));
|
|
58
|
+
errors.push(...destructiveErrors(state, options.requiresConfirmation === true));
|
|
59
|
+
errors.push(...roleConfigSyncErrors(patches, state));
|
|
60
|
+
warnings.push(...routeDescriptionWarnings(state));
|
|
61
|
+
// D-157 rule 18: dbTables is applied over every effective route, not only patched ones.
|
|
62
|
+
const routeTables = routeTableDeclaredFindings(state);
|
|
63
|
+
errors.push(...routeTables.errors);
|
|
64
|
+
warnings.push(...routeTables.warnings);
|
|
65
|
+
// D-157 rule 19: the same declaration, applied to every effective job's system client.
|
|
66
|
+
const jobTables = jobTableDeclaredFindings(state);
|
|
67
|
+
errors.push(...jobTables.errors);
|
|
68
|
+
warnings.push(...jobTables.warnings);
|
|
69
|
+
}
|
|
70
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
71
|
+
}
|