@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.
- 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 +41 -0
- package/dist/paths.js +70 -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 +24 -0
- package/dist/rules/database.js +105 -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 +16 -0
- package/dist/rules/structural.js +41 -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 +48 -0
- package/dist/testing/fixtures.js +153 -0
- package/dist/types.d.ts +146 -0
- package/dist/types.js +12 -0
- package/dist/validator.d.ts +13 -0
- package/dist/validator.js +62 -0
- package/package.json +43 -4
- package/src/context.ts +117 -0
- package/src/index.ts +44 -0
- package/src/manifest-facts.ts +54 -0
- package/src/node.ts +75 -0
- package/src/overlay.ts +131 -0
- package/src/paths.ts +80 -0
- package/src/rules/blocks.ts +67 -0
- package/src/rules/config-sync.ts +72 -0
- package/src/rules/database.ts +139 -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 +83 -0
- package/src/rules/ts-parse.ts +71 -0
- package/src/syntax-runner.ts +33 -0
- package/src/testing/fixtures.ts +215 -0
- package/src/types.ts +179 -0
- package/src/validator.ts +81 -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
|
-
#
|
|
1
|
+
# @facilis.dev/ai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Facilis AI orchestration: prompt assembly, response parsing and patch validation.
|
|
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/ai
|
|
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,12 @@
|
|
|
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
|
+
import type { ProjectContext, ProjectContextInput } from "./types.js";
|
|
11
|
+
/** Build the typed context from analyzer facts; the manifest only contributes fallbacks. */
|
|
12
|
+
export declare function buildProjectContext(input: ProjectContextInput): ProjectContext;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
import { factsFromManifest } from "./manifest-facts.js";
|
|
11
|
+
import { normalizeProjectPath, SCHEMA_PATH } from "./paths.js";
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function normalizeSources(sources) {
|
|
16
|
+
const normalized = {};
|
|
17
|
+
for (const [path, content] of Object.entries(sources ?? {})) {
|
|
18
|
+
normalized[normalizeProjectPath(path)] = content;
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
function parseJsonRecord(text) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(text);
|
|
25
|
+
return isRecord(parsed) ? parsed : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Installed bare specifiers from the project's package.json. */
|
|
32
|
+
function packagesFromSource(source) {
|
|
33
|
+
if (source === undefined)
|
|
34
|
+
return [];
|
|
35
|
+
const parsed = parseJsonRecord(source);
|
|
36
|
+
if (parsed === null)
|
|
37
|
+
return [];
|
|
38
|
+
const names = new Set();
|
|
39
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
40
|
+
const section = parsed[field];
|
|
41
|
+
if (!isRecord(section))
|
|
42
|
+
continue;
|
|
43
|
+
for (const name of Object.keys(section))
|
|
44
|
+
names.add(name);
|
|
45
|
+
}
|
|
46
|
+
return [...names];
|
|
47
|
+
}
|
|
48
|
+
/** `tsconfig.json` `paths` aliases, normalized to project-relative targets. */
|
|
49
|
+
function aliasesFromSource(source) {
|
|
50
|
+
const aliases = [{ prefix: "~/", target: "" }];
|
|
51
|
+
if (source === undefined)
|
|
52
|
+
return aliases;
|
|
53
|
+
const parsed = parseJsonRecord(source);
|
|
54
|
+
const options = parsed !== null && isRecord(parsed.compilerOptions) ? parsed.compilerOptions : null;
|
|
55
|
+
const paths = options !== null && isRecord(options.paths) ? options.paths : null;
|
|
56
|
+
if (paths === null)
|
|
57
|
+
return aliases;
|
|
58
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
59
|
+
if (!Array.isArray(targets) || targets.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
const target = targets[0];
|
|
62
|
+
if (typeof target !== "string")
|
|
63
|
+
continue;
|
|
64
|
+
const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
|
|
65
|
+
const resolved = target.replace(/^\.\//, "").replace(/\*$/, "").replace(/\/$/, "");
|
|
66
|
+
aliases.push({ prefix, target: resolved });
|
|
67
|
+
}
|
|
68
|
+
return aliases;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The default-locale catalogue: the analyzer's facts first, the manifest only underneath
|
|
72
|
+
* as a fallback. `analysis.locales.en` is the generated project's source of truth; when
|
|
73
|
+
* the project's default locale is not English, its catalogue is the next best fact.
|
|
74
|
+
*/
|
|
75
|
+
function flattenCatalog(analysis, config) {
|
|
76
|
+
const defaultLocale = config.defaultLocale ?? "en";
|
|
77
|
+
const analyzerCatalog = analysis.locales["en"] ?? analysis.locales[defaultLocale] ?? {};
|
|
78
|
+
return { ...analyzerCatalog };
|
|
79
|
+
}
|
|
80
|
+
/** Build the typed context from analyzer facts; the manifest only contributes fallbacks. */
|
|
81
|
+
export function buildProjectContext(input) {
|
|
82
|
+
const sources = normalizeSources(input.sources);
|
|
83
|
+
const manifest = factsFromManifest(input.manifest);
|
|
84
|
+
const analysis = input.analysis;
|
|
85
|
+
const before = input.before ?? analysis;
|
|
86
|
+
const config = analysis.config;
|
|
87
|
+
const analyzerCatalog = flattenCatalog(analysis, config);
|
|
88
|
+
const locales = { ...manifest.locales, ...analyzerCatalog };
|
|
89
|
+
const blocks = manifest.blocks;
|
|
90
|
+
const routes = analysis.routes;
|
|
91
|
+
const tables = analysis.tables;
|
|
92
|
+
return {
|
|
93
|
+
files: input.files.map(normalizeProjectPath).filter((path) => path.length > 0),
|
|
94
|
+
analysis,
|
|
95
|
+
before,
|
|
96
|
+
config,
|
|
97
|
+
schema: { parsed: analysis.files.includes(SCHEMA_PATH), tables },
|
|
98
|
+
locales,
|
|
99
|
+
blocks,
|
|
100
|
+
routes,
|
|
101
|
+
packages: input.packages ?? packagesFromSource(sources["package.json"]),
|
|
102
|
+
aliases: input.aliases ?? aliasesFromSource(sources["tsconfig.json"]),
|
|
103
|
+
};
|
|
104
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export { buildProjectContext } from "./context.js";
|
|
14
|
+
export { checkPatchSyntax } from "./syntax-runner.js";
|
|
15
|
+
export type { Analysis, BlockFacts, ColumnFact, ConfigFacts, Diagnostic, FileFacts, FilePatch, ImportAlias, ImportFact, LocaleCatalogs, PatchContext, ProjectContext, ProjectContextInput, RouteFact, SchemaSummary, SourceRef, SyntaxCheck, SyntaxChecker, SyntaxFinding, SyntaxIssue, TableFact, TranslationCallFact, ValidatePatchOptions, ValidationError, ValidationResult, WritePolicy, } from "./types.js";
|
|
16
|
+
export { validatePatch } from "./validator.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export { buildProjectContext } from "./context.js";
|
|
14
|
+
export { checkPatchSyntax } from "./syntax-runner.js";
|
|
15
|
+
export { validatePatch } from "./validator.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive reader for the live `/__facilis/manifest` payload (SPEC §6.2).
|
|
3
|
+
*
|
|
4
|
+
* Only two things still come from the manifest: the registered blocks (reviewed supply
|
|
5
|
+
* chain, not a source fact) and a locale-catalogue fallback. Config, schema and routes are
|
|
6
|
+
* read from @facilis.dev/analyze's source facts (D-092); the manifest is data from a running
|
|
7
|
+
* app, not a trusted compiler output, so an unexpected shape yields fewer facts and never
|
|
8
|
+
* an exception.
|
|
9
|
+
*/
|
|
10
|
+
import type { BlockFacts } from "./types.js";
|
|
11
|
+
export interface ManifestFacts {
|
|
12
|
+
readonly blocks: readonly BlockFacts[];
|
|
13
|
+
readonly locales: Readonly<Record<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
/** Read whatever the manifest is allowed to contribute; a broken payload yields zero facts. */
|
|
16
|
+
export declare function factsFromManifest(manifest: unknown): ManifestFacts;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive reader for the live `/__facilis/manifest` payload (SPEC §6.2).
|
|
3
|
+
*
|
|
4
|
+
* Only two things still come from the manifest: the registered blocks (reviewed supply
|
|
5
|
+
* chain, not a source fact) and a locale-catalogue fallback. Config, schema and routes are
|
|
6
|
+
* read from @facilis.dev/analyze's source facts (D-092); the manifest is data from a running
|
|
7
|
+
* app, not a trusted compiler output, so an unexpected shape yields fewer facts and never
|
|
8
|
+
* an exception.
|
|
9
|
+
*/
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function factsFromBlocks(value) {
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return [];
|
|
16
|
+
return value.flatMap((entry) => isRecord(entry) && typeof entry.name === "string" ? [{ name: entry.name }] : []);
|
|
17
|
+
}
|
|
18
|
+
/** Prefer the default locale, fall back to English, then to the first object catalogue. */
|
|
19
|
+
function factsFromCatalog(value, defaultLocale) {
|
|
20
|
+
if (!isRecord(value))
|
|
21
|
+
return {};
|
|
22
|
+
const preferred = defaultLocale !== null && isRecord(value[defaultLocale]) ? defaultLocale : "en";
|
|
23
|
+
const locale = isRecord(value[preferred])
|
|
24
|
+
? preferred
|
|
25
|
+
: Object.keys(value).find((candidate) => isRecord(value[candidate]));
|
|
26
|
+
const catalog = locale === undefined ? undefined : value[locale];
|
|
27
|
+
if (!isRecord(catalog))
|
|
28
|
+
return {};
|
|
29
|
+
const strings = {};
|
|
30
|
+
for (const [key, text] of Object.entries(catalog)) {
|
|
31
|
+
if (typeof text === "string")
|
|
32
|
+
strings[key] = text;
|
|
33
|
+
}
|
|
34
|
+
return strings;
|
|
35
|
+
}
|
|
36
|
+
/** Read whatever the manifest is allowed to contribute; a broken payload yields zero facts. */
|
|
37
|
+
export function factsFromManifest(manifest) {
|
|
38
|
+
const root = isRecord(manifest) ? manifest : {};
|
|
39
|
+
const i18n = isRecord(root.i18n) ? root.i18n : {};
|
|
40
|
+
const defaultLocale = typeof i18n.defaultLocale === "string" ? i18n.defaultLocale : null;
|
|
41
|
+
return {
|
|
42
|
+
blocks: factsFromBlocks(root.blocks),
|
|
43
|
+
locales: factsFromCatalog(i18n.catalogs, defaultLocale),
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only helpers, exported as `@facilis.dev/ai/node` (D-084, D-092).
|
|
3
|
+
*
|
|
4
|
+
* The main entry must stay browser-safe, so this subpath is the only place that imports
|
|
5
|
+
* runtime code from a parser: rule 1's rolldown `parseSync` (oxc) and, for tests/tooling,
|
|
6
|
+
* @facilis.dev/analyze's oxc adapter so a project file map can be analyzed without a browser.
|
|
7
|
+
* The IDE never imports this module: it injects the analyzer worker's oxc parser instead.
|
|
8
|
+
*/
|
|
9
|
+
import { type Analysis, type PathAlias } from "@facilis.dev/analyze";
|
|
10
|
+
import type { SyntaxChecker } from "./types.js";
|
|
11
|
+
export type ScriptLanguage = "ts" | "tsx" | "js" | "jsx";
|
|
12
|
+
/** A {@link SyntaxChecker} backed by rolldown's oxc parser (Node only). */
|
|
13
|
+
export declare function createTypeScriptSyntaxChecker(): SyntaxChecker;
|
|
14
|
+
/**
|
|
15
|
+
* Analyze a project file map with the Node oxc parser. Tests/tooling only: the browser
|
|
16
|
+
* passes its own parser adapter to `analyzeProject`, and the main entry never imports
|
|
17
|
+
* this module (it would pull a parser into the IDE bundle).
|
|
18
|
+
*/
|
|
19
|
+
export declare function analyzeProjectSources(files: ReadonlyMap<string, string>, aliases?: readonly PathAlias[]): Analysis;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only helpers, exported as `@facilis.dev/ai/node` (D-084, D-092).
|
|
3
|
+
*
|
|
4
|
+
* The main entry must stay browser-safe, so this subpath is the only place that imports
|
|
5
|
+
* runtime code from a parser: rule 1's rolldown `parseSync` (oxc) and, for tests/tooling,
|
|
6
|
+
* @facilis.dev/analyze's oxc adapter so a project file map can be analyzed without a browser.
|
|
7
|
+
* The IDE never imports this module: it injects the analyzer worker's oxc parser instead.
|
|
8
|
+
*/
|
|
9
|
+
import { analyzeProject } from "@facilis.dev/analyze";
|
|
10
|
+
import { createOxcParser as createAnalyzerParser } from "@facilis.dev/analyze/node";
|
|
11
|
+
import { parseSync } from "rolldown/utils";
|
|
12
|
+
function languageOf(path) {
|
|
13
|
+
if (path.endsWith(".tsx"))
|
|
14
|
+
return "tsx";
|
|
15
|
+
if (path.endsWith(".jsx"))
|
|
16
|
+
return "jsx";
|
|
17
|
+
if (path.endsWith(".js") || path.endsWith(".mjs") || path.endsWith(".cjs"))
|
|
18
|
+
return "js";
|
|
19
|
+
return "ts";
|
|
20
|
+
}
|
|
21
|
+
function positionOf(content, offset) {
|
|
22
|
+
const bounded = Math.max(0, Math.min(offset, content.length));
|
|
23
|
+
const lines = content.slice(0, bounded).split("\n");
|
|
24
|
+
return { line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
|
|
25
|
+
}
|
|
26
|
+
function toIssue(content, error) {
|
|
27
|
+
const label = error.labels?.[0];
|
|
28
|
+
const position = positionOf(content, label?.start ?? 0);
|
|
29
|
+
return { line: position.line, column: position.column, message: error.message };
|
|
30
|
+
}
|
|
31
|
+
/** A {@link SyntaxChecker} backed by rolldown's oxc parser (Node only). */
|
|
32
|
+
export function createTypeScriptSyntaxChecker() {
|
|
33
|
+
return {
|
|
34
|
+
async check(path, content) {
|
|
35
|
+
const language = languageOf(path);
|
|
36
|
+
try {
|
|
37
|
+
const result = parseSync(path, content, { lang: language });
|
|
38
|
+
return result.errors
|
|
39
|
+
.filter((error) => error.severity === "Error")
|
|
40
|
+
.map((error) => toIssue(content, error));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
return [{ line: 1, column: 1, message }];
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Analyze a project file map with the Node oxc parser. Tests/tooling only: the browser
|
|
51
|
+
* passes its own parser adapter to `analyzeProject`, and the main entry never imports
|
|
52
|
+
* this module (it would pull a parser into the IDE bundle).
|
|
53
|
+
*/
|
|
54
|
+
export function analyzeProjectSources(files, aliases) {
|
|
55
|
+
return analyzeProject({
|
|
56
|
+
files,
|
|
57
|
+
parser: createAnalyzerParser(),
|
|
58
|
+
...(aliases === undefined ? {} : { aliases }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-patch project context overlaid with one patch set.
|
|
3
|
+
*
|
|
4
|
+
* After D-092 this is fact-based: the caller supplies the effective (post-patch) Analysis
|
|
5
|
+
* and the pre-patch one, so the overlay only projects them plus the patch-shaped
|
|
6
|
+
* bookkeeping the rules need (file set, patched paths, removed facts). It never parses
|
|
7
|
+
* project sources; the one JSON read is the patched locale catalogue, because rule 13 must
|
|
8
|
+
* tell "no keys" from "broken catalogue". Pure and browser-safe.
|
|
9
|
+
*/
|
|
10
|
+
import type { Analysis, ConfigFacts, FileFacts, TableFact } from "@facilis.dev/analyze";
|
|
11
|
+
import type { FilePatch, PatchContext } from "./types.js";
|
|
12
|
+
export { CONFIG_PATH, LOCALES_PATH, SCHEMA_PATH } from "./paths.js";
|
|
13
|
+
export interface EffectiveState {
|
|
14
|
+
/** Project-relative files that exist after the patch. */
|
|
15
|
+
files: ReadonlySet<string>;
|
|
16
|
+
/** Every path the patch touches, and those it deletes. */
|
|
17
|
+
patchedPaths: ReadonlySet<string>;
|
|
18
|
+
deletedPaths: ReadonlySet<string>;
|
|
19
|
+
/** Effective analysis + the pre-patch one, for inverse edits. */
|
|
20
|
+
analysis: Analysis;
|
|
21
|
+
before: Analysis;
|
|
22
|
+
/** Module facts of the created/updated files (from the effective analysis). */
|
|
23
|
+
patchedFileFacts: readonly FileFacts[];
|
|
24
|
+
/** Effective config after the patch (analyzer facts). */
|
|
25
|
+
config: ConfigFacts;
|
|
26
|
+
/** Every effective table, including framework tables re-exported into the schema. */
|
|
27
|
+
schemaTables: readonly TableFact[];
|
|
28
|
+
/** Tables declared in the code schema before and after the patch (no re-exports). */
|
|
29
|
+
beforeSchemaTables: readonly TableFact[];
|
|
30
|
+
afterSchemaTables: readonly TableFact[];
|
|
31
|
+
locales: Readonly<Record<string, string>>;
|
|
32
|
+
/** False when a patched locales/en.json is not a valid JSON catalogue. */
|
|
33
|
+
localesValid: boolean;
|
|
34
|
+
migrationsDir: string;
|
|
35
|
+
removedTables: readonly string[];
|
|
36
|
+
removedPermissions: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
/** True when the patch creates a SQL migration file in the configured directory. */
|
|
39
|
+
export declare function hasMigrationPatch(patches: readonly FilePatch[], migrationsDir: string): boolean;
|
|
40
|
+
/** Overlay the patch set onto the project context. */
|
|
41
|
+
export declare function buildEffectiveState(context: PatchContext, patches: readonly FilePatch[]): EffectiveState;
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-patch project context overlaid with one patch set.
|
|
3
|
+
*
|
|
4
|
+
* After D-092 this is fact-based: the caller supplies the effective (post-patch) Analysis
|
|
5
|
+
* and the pre-patch one, so the overlay only projects them plus the patch-shaped
|
|
6
|
+
* bookkeeping the rules need (file set, patched paths, removed facts). It never parses
|
|
7
|
+
* project sources; the one JSON read is the patched locale catalogue, because rule 13 must
|
|
8
|
+
* tell "no keys" from "broken catalogue". Pure and browser-safe.
|
|
9
|
+
*/
|
|
10
|
+
import { LOCALES_PATH, normalizeProjectPath, SCHEMA_PATH } from "./paths.js";
|
|
11
|
+
export { CONFIG_PATH, LOCALES_PATH, SCHEMA_PATH } from "./paths.js";
|
|
12
|
+
function parseCatalog(content) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(content);
|
|
15
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
16
|
+
return null;
|
|
17
|
+
const catalog = {};
|
|
18
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
19
|
+
if (typeof value === "string")
|
|
20
|
+
catalog[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return catalog;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** A table the project declares in its own schema file (not a framework re-export). */
|
|
29
|
+
function isSchemaTable(table) {
|
|
30
|
+
return normalizeProjectPath(table.source.file) === SCHEMA_PATH;
|
|
31
|
+
}
|
|
32
|
+
/** True when the patch creates a SQL migration file in the configured directory. */
|
|
33
|
+
export function hasMigrationPatch(patches, migrationsDir) {
|
|
34
|
+
const directory = normalizeProjectPath(migrationsDir).replace(/\/+$/, "");
|
|
35
|
+
return patches.some((patch) => patch.action === "create" &&
|
|
36
|
+
normalizeProjectPath(patch.path).startsWith(directory + "/") &&
|
|
37
|
+
normalizeProjectPath(patch.path).endsWith(".sql"));
|
|
38
|
+
}
|
|
39
|
+
/** Overlay the patch set onto the project context. */
|
|
40
|
+
export function buildEffectiveState(context, patches) {
|
|
41
|
+
const patched = new Map();
|
|
42
|
+
const deleted = new Set();
|
|
43
|
+
const patchedPaths = new Set();
|
|
44
|
+
for (const patch of patches) {
|
|
45
|
+
const path = normalizeProjectPath(patch.path);
|
|
46
|
+
patchedPaths.add(path);
|
|
47
|
+
if (patch.action === "delete")
|
|
48
|
+
deleted.add(path);
|
|
49
|
+
else if (typeof patch.content === "string")
|
|
50
|
+
patched.set(path, patch.content);
|
|
51
|
+
}
|
|
52
|
+
const analysis = context.analysis;
|
|
53
|
+
const before = context.before;
|
|
54
|
+
const config = analysis.config;
|
|
55
|
+
const beforeSchemaTables = before.tables.filter(isSchemaTable);
|
|
56
|
+
const afterSchemaTables = analysis.tables.filter(isSchemaTable);
|
|
57
|
+
const afterNames = new Set(afterSchemaTables.map((table) => table.name));
|
|
58
|
+
const beforeNames = new Set(beforeSchemaTables.map((table) => table.name));
|
|
59
|
+
let locales = context.locales;
|
|
60
|
+
let localesValid = true;
|
|
61
|
+
const catalogContent = patched.get(LOCALES_PATH);
|
|
62
|
+
if (catalogContent !== undefined) {
|
|
63
|
+
const catalog = parseCatalog(catalogContent);
|
|
64
|
+
if (catalog === null)
|
|
65
|
+
localesValid = false;
|
|
66
|
+
else
|
|
67
|
+
locales = catalog;
|
|
68
|
+
}
|
|
69
|
+
const files = new Set(context.files.map(normalizeProjectPath));
|
|
70
|
+
for (const path of deleted)
|
|
71
|
+
files.delete(path);
|
|
72
|
+
for (const path of patched.keys())
|
|
73
|
+
files.add(path);
|
|
74
|
+
return {
|
|
75
|
+
files,
|
|
76
|
+
patchedPaths,
|
|
77
|
+
deletedPaths: deleted,
|
|
78
|
+
analysis,
|
|
79
|
+
before,
|
|
80
|
+
patchedFileFacts: analysis.fileFacts.filter((facts) => patchedPaths.has(normalizeProjectPath(facts.path))),
|
|
81
|
+
config,
|
|
82
|
+
schemaTables: analysis.tables,
|
|
83
|
+
beforeSchemaTables,
|
|
84
|
+
afterSchemaTables,
|
|
85
|
+
locales,
|
|
86
|
+
localesValid,
|
|
87
|
+
migrationsDir: normalizeProjectPath(config.migrationsDir),
|
|
88
|
+
removedTables: [...beforeNames].filter((name) => !afterNames.has(name)),
|
|
89
|
+
removedPermissions: before.config.permissions.filter((permission) => !config.permissions.includes(permission)),
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-relative path predicates shared by the validator rules (SPEC §9.3 rule 11).
|
|
3
|
+
*
|
|
4
|
+
* Paths are always compared in their normalized, root-relative form ("src/App.tsx"), so
|
|
5
|
+
* Windows separators or a leading "./" never change a rule's verdict.
|
|
6
|
+
*
|
|
7
|
+
* Rule 11's writable set follows the deployment profile (D-167). A `managed` build keeps
|
|
8
|
+
* the six source roots (D-084); an `open` build accepts the whole project minus the
|
|
9
|
+
* machine surfaces a patch must never touch: dependencies, install state, VCS/IDE state,
|
|
10
|
+
* build output and the project's own instruction file.
|
|
11
|
+
*/
|
|
12
|
+
import type { WritePolicy } from "./types.js";
|
|
13
|
+
/** The project files whose facts the validator treats specially. */
|
|
14
|
+
export declare const CONFIG_PATH = "facilis.config.ts";
|
|
15
|
+
export declare const SCHEMA_PATH = "db/schema.ts";
|
|
16
|
+
export declare const LOCALES_PATH = "locales/en.json";
|
|
17
|
+
/** Roots a `managed` patch may write to (SPEC §9.3 rule 11, D-084). */
|
|
18
|
+
export declare const ALLOWED_ROOTS: string[];
|
|
19
|
+
/**
|
|
20
|
+
* The one root file a `managed` patch may touch: rule 16 exists because a new
|
|
21
|
+
* permission/role is declared in facilis.config.ts in the same patch, so a blanket
|
|
22
|
+
* root-file rejection would make the 17-rule contract self-contradictory (D-084).
|
|
23
|
+
*/
|
|
24
|
+
export declare const ALLOWED_ROOT_FILES: Set<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Directory segments no `open` patch may write into (D-167). They are the same entries
|
|
27
|
+
* the PTC read policy hides (docs/PTC.md §6): dependencies, VCS and IDE state, and build
|
|
28
|
+
* output the Explorer does not review. A patch writing them could hide its own changes,
|
|
29
|
+
* poison a cache, or add code without an install.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DENIED_SEGMENTS: Set<string>;
|
|
32
|
+
/**
|
|
33
|
+
* File names no `open` patch may write (D-167). The install surface is the dependency
|
|
34
|
+
* manifest, the lockfiles and the npm config; AGENTS.md stays IDE-writer-only because it
|
|
35
|
+
* persists instructions into every later session (D-084).
|
|
36
|
+
*/
|
|
37
|
+
export declare const DENIED_FILES: Set<string>;
|
|
38
|
+
export declare function normalizeProjectPath(path: string): string;
|
|
39
|
+
export declare function isScriptPath(path: string): boolean;
|
|
40
|
+
/** True when the normalized path is writable under `policy` (rule 11). */
|
|
41
|
+
export declare function isAllowedWritePath(path: string, policy?: WritePolicy): boolean;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-relative path predicates shared by the validator rules (SPEC §9.3 rule 11).
|
|
3
|
+
*
|
|
4
|
+
* Paths are always compared in their normalized, root-relative form ("src/App.tsx"), so
|
|
5
|
+
* Windows separators or a leading "./" never change a rule's verdict.
|
|
6
|
+
*
|
|
7
|
+
* Rule 11's writable set follows the deployment profile (D-167). A `managed` build keeps
|
|
8
|
+
* the six source roots (D-084); an `open` build accepts the whole project minus the
|
|
9
|
+
* machine surfaces a patch must never touch: dependencies, install state, VCS/IDE state,
|
|
10
|
+
* build output and the project's own instruction file.
|
|
11
|
+
*/
|
|
12
|
+
const SCRIPT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
13
|
+
/** The project files whose facts the validator treats specially. */
|
|
14
|
+
export const CONFIG_PATH = "facilis.config.ts";
|
|
15
|
+
export const SCHEMA_PATH = "db/schema.ts";
|
|
16
|
+
export const LOCALES_PATH = "locales/en.json";
|
|
17
|
+
/** Roots a `managed` patch may write to (SPEC §9.3 rule 11, D-084). */
|
|
18
|
+
export const ALLOWED_ROOTS = ["server/", "src/", "db/", "lib/", "components/", "locales/"];
|
|
19
|
+
/**
|
|
20
|
+
* The one root file a `managed` patch may touch: rule 16 exists because a new
|
|
21
|
+
* permission/role is declared in facilis.config.ts in the same patch, so a blanket
|
|
22
|
+
* root-file rejection would make the 17-rule contract self-contradictory (D-084).
|
|
23
|
+
*/
|
|
24
|
+
export const ALLOWED_ROOT_FILES = new Set(["facilis.config.ts"]);
|
|
25
|
+
/**
|
|
26
|
+
* Directory segments no `open` patch may write into (D-167). They are the same entries
|
|
27
|
+
* the PTC read policy hides (docs/PTC.md §6): dependencies, VCS and IDE state, and build
|
|
28
|
+
* output the Explorer does not review. A patch writing them could hide its own changes,
|
|
29
|
+
* poison a cache, or add code without an install.
|
|
30
|
+
*/
|
|
31
|
+
export const DENIED_SEGMENTS = new Set([
|
|
32
|
+
"node_modules",
|
|
33
|
+
".git",
|
|
34
|
+
".facilis",
|
|
35
|
+
"dist",
|
|
36
|
+
".turbo",
|
|
37
|
+
"coverage",
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* File names no `open` patch may write (D-167). The install surface is the dependency
|
|
41
|
+
* manifest, the lockfiles and the npm config; AGENTS.md stays IDE-writer-only because it
|
|
42
|
+
* persists instructions into every later session (D-084).
|
|
43
|
+
*/
|
|
44
|
+
export const DENIED_FILES = new Set([
|
|
45
|
+
"package.json",
|
|
46
|
+
"package-lock.json",
|
|
47
|
+
"npm-shrinkwrap.json",
|
|
48
|
+
"pnpm-lock.yaml",
|
|
49
|
+
"yarn.lock",
|
|
50
|
+
".npmrc",
|
|
51
|
+
"AGENTS.md",
|
|
52
|
+
]);
|
|
53
|
+
export function normalizeProjectPath(path) {
|
|
54
|
+
let normalized = path.trim();
|
|
55
|
+
while (normalized.startsWith("./"))
|
|
56
|
+
normalized = normalized.slice(2);
|
|
57
|
+
return normalized.replace(/^\/+/, "");
|
|
58
|
+
}
|
|
59
|
+
export function isScriptPath(path) {
|
|
60
|
+
return SCRIPT_EXTENSIONS.some((extension) => path.endsWith(extension));
|
|
61
|
+
}
|
|
62
|
+
/** True when the normalized path is writable under `policy` (rule 11). */
|
|
63
|
+
export function isAllowedWritePath(path, policy = "managed") {
|
|
64
|
+
if (policy === "managed") {
|
|
65
|
+
return ALLOWED_ROOT_FILES.has(path) || ALLOWED_ROOTS.some((root) => path.startsWith(root));
|
|
66
|
+
}
|
|
67
|
+
const segments = path.split("/");
|
|
68
|
+
const name = segments[segments.length - 1] ?? "";
|
|
69
|
+
return !DENIED_FILES.has(name) && !segments.some((segment) => DENIED_SEGMENTS.has(segment));
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule 7 `block-import-valid` (SPEC §9.3): every block the patch imports must match a
|
|
3
|
+
* registered `BlockManifest`. The block supply chain is retired (D-131, no `@facilis.dev/blocks`
|
|
4
|
+
* package ships), so in practice the manifest registers none and the guard rejects every block
|
|
5
|
+
* import; it stays in the rule set as the contract's reserved slot (§7.2/§7.3).
|
|
6
|
+
*
|
|
7
|
+
* The imports are the analyzer's FileFacts, so re-exports and dynamic imports are covered
|
|
8
|
+
* by the same AST truth the rest of validation uses.
|
|
9
|
+
*/
|
|
10
|
+
import type { EffectiveState } from "../overlay.js";
|
|
11
|
+
import type { PatchContext, ValidationError } from "../types.js";
|
|
12
|
+
export declare function blockImportErrors(state: EffectiveState, context: PatchContext): ValidationError[];
|