@descryy/adapter-python 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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Python module paths and import resolution — the whole of R1.
3
+ *
4
+ * ## Where the last adapter lost three quarters of its graph
5
+ *
6
+ * DEC-059: the TypeScript adapter ignored the repository's own path
7
+ * configuration, so every aliased import failed and every cross-directory edge
8
+ * with it. Python's equivalents are `src/` layouts, `PYTHONPATH`, namespace
9
+ * packages and `pyproject.toml`'s package table — **the same defect in a
10
+ * different spelling** — so the layout is read from the repository here rather
11
+ * than assumed, and what could not be read is disclosed.
12
+ *
13
+ * ## The two hard cases, both in the corpus
14
+ *
15
+ * `from . import validate_order` — level 1, **no module name**. The dots are the
16
+ * whole address, and they are not recoverable from the specifier text: this is
17
+ * why the Python side reports `ImportFrom.level` rather than a string. It
18
+ * resolves to the containing package's `__init__.py`, which is the re-export hop
19
+ * golden pattern 03 turns on.
20
+ *
21
+ * `from .money import format_money` — level 1 with a module. Resolves to a
22
+ * sibling file, and the imported name is a *symbol* in it rather than a module.
23
+ * Deciding which of the two a name is cannot be done from the import alone —
24
+ * `from .pkg import mod` and `from .mod import fn` are identical in shape — so
25
+ * both candidates are returned and the caller picks by what exists.
26
+ */
27
+ /** `src/order_service.py` -> `["src", "order_service"]`; `src/__init__.py` -> `["src"]`. */
28
+ export declare function modulePathOf(repoRelativeFile: string): readonly string[];
29
+ /**
30
+ * The display name of a module node.
31
+ *
32
+ * A package takes its directory's name — the corpus binds the `barrel` role to
33
+ * the symbol `src` at `src/__init__.py`, so `__init__` would fail to bind and
34
+ * the failure would read as a missing node rather than as a naming choice.
35
+ */
36
+ export declare function moduleNameOf(repoRelativeFile: string): string;
37
+ /**
38
+ * The project's distribution name, read rather than guessed.
39
+ *
40
+ * `pyproject.toml` first, then `setup.cfg`. Parsed with a narrow regex on
41
+ * purpose: a TOML dependency would buy correctness on a field nothing else in
42
+ * this adapter reads, and the fallback is disclosed through
43
+ * `capabilities().nodeIdentityFallback` either way.
44
+ */
45
+ export declare function projectName(absoluteRoot: string): {
46
+ name: string;
47
+ evidence: string;
48
+ };
49
+ export interface ImportRecord {
50
+ readonly kind: string;
51
+ readonly level: number;
52
+ readonly module: string | null;
53
+ readonly name: string | null;
54
+ readonly local: string;
55
+ readonly line: number;
56
+ }
57
+ /** What an import might refer to: a module file, or a symbol inside one. */
58
+ export interface ImportTarget {
59
+ /** Repo-relative file the specifier reaches, or undefined if nothing does. */
60
+ readonly file: string | undefined;
61
+ /** The symbol name inside that file, when the import names one. */
62
+ readonly symbol: string | undefined;
63
+ /** Set when the specifier names a package whose `__init__.py` re-exports. */
64
+ readonly viaPackageInit: boolean;
65
+ }
66
+ export interface ResolveOptions {
67
+ /** Repo-relative files this run analysed. Nothing outside it can be a target. */
68
+ readonly known: ReadonlySet<string>;
69
+ /**
70
+ * Directories that are import roots, repo-relative, longest first.
71
+ *
72
+ * A `src/` layout means `from myapp.models import X` resolves under `src/`,
73
+ * not under the repository root. Discovered rather than assumed — see
74
+ * `importRoots`.
75
+ */
76
+ readonly roots: readonly string[];
77
+ }
78
+ /**
79
+ * Resolve one import to a file, and to a symbol inside it when it names one.
80
+ *
81
+ * Returns `file: undefined` for anything outside the analysed set — the standard
82
+ * library, an installed package, a namespace package this run did not read.
83
+ * That is a scope boundary rather than a failure, and DEC-050 keeps the two
84
+ * apart in the ledger.
85
+ */
86
+ export declare function resolveImport(record: ImportRecord, fromFile: string, options: ResolveOptions): ImportTarget;
87
+ /**
88
+ * Directories an absolute import can be resolved against.
89
+ *
90
+ * This is the DEC-059 surface for Python, and it is *derived from the files that
91
+ * exist* rather than from a configuration file that may not. A directory is an
92
+ * import root when it directly contains a package — a subdirectory with an
93
+ * `__init__.py` — or a top-level module. `src/` layouts fall out of that without
94
+ * being special-cased, and so does a repository with no layout at all.
95
+ *
96
+ * Longest first, so a `src/` layout wins over the root when both could match.
97
+ */
98
+ export declare function importRoots(files: readonly string[]): readonly string[];
99
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAQxE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAwBpF;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC;AAeD,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,cAAc,GACtB,YAAY,CA0Dd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAwCvE"}
package/dist/module.js ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Python module paths and import resolution — the whole of R1.
3
+ *
4
+ * ## Where the last adapter lost three quarters of its graph
5
+ *
6
+ * DEC-059: the TypeScript adapter ignored the repository's own path
7
+ * configuration, so every aliased import failed and every cross-directory edge
8
+ * with it. Python's equivalents are `src/` layouts, `PYTHONPATH`, namespace
9
+ * packages and `pyproject.toml`'s package table — **the same defect in a
10
+ * different spelling** — so the layout is read from the repository here rather
11
+ * than assumed, and what could not be read is disclosed.
12
+ *
13
+ * ## The two hard cases, both in the corpus
14
+ *
15
+ * `from . import validate_order` — level 1, **no module name**. The dots are the
16
+ * whole address, and they are not recoverable from the specifier text: this is
17
+ * why the Python side reports `ImportFrom.level` rather than a string. It
18
+ * resolves to the containing package's `__init__.py`, which is the re-export hop
19
+ * golden pattern 03 turns on.
20
+ *
21
+ * `from .money import format_money` — level 1 with a module. Resolves to a
22
+ * sibling file, and the imported name is a *symbol* in it rather than a module.
23
+ * Deciding which of the two a name is cannot be done from the import alone —
24
+ * `from .pkg import mod` and `from .mod import fn` are identical in shape — so
25
+ * both candidates are returned and the caller picks by what exists.
26
+ */
27
+ import { existsSync, readFileSync } from "node:fs";
28
+ import { join } from "node:path";
29
+ /** `src/order_service.py` -> `["src", "order_service"]`; `src/__init__.py` -> `["src"]`. */
30
+ export function modulePathOf(repoRelativeFile) {
31
+ const withoutExtension = repoRelativeFile.replace(/\.pyi?$/, "");
32
+ const parts = withoutExtension.split("/").filter((p) => p !== "" && p !== ".");
33
+ // A package *is* its directory. `src/__init__.py` and the directory `src` are
34
+ // one module, and emitting two nodes for them would split every import into
35
+ // the package across two targets.
36
+ if (parts[parts.length - 1] === "__init__")
37
+ parts.pop();
38
+ return parts;
39
+ }
40
+ /**
41
+ * The display name of a module node.
42
+ *
43
+ * A package takes its directory's name — the corpus binds the `barrel` role to
44
+ * the symbol `src` at `src/__init__.py`, so `__init__` would fail to bind and
45
+ * the failure would read as a missing node rather than as a naming choice.
46
+ */
47
+ export function moduleNameOf(repoRelativeFile) {
48
+ const path = modulePathOf(repoRelativeFile);
49
+ return path[path.length - 1] ?? "";
50
+ }
51
+ /**
52
+ * The project's distribution name, read rather than guessed.
53
+ *
54
+ * `pyproject.toml` first, then `setup.cfg`. Parsed with a narrow regex on
55
+ * purpose: a TOML dependency would buy correctness on a field nothing else in
56
+ * this adapter reads, and the fallback is disclosed through
57
+ * `capabilities().nodeIdentityFallback` either way.
58
+ */
59
+ export function projectName(absoluteRoot) {
60
+ const pyproject = join(absoluteRoot, "pyproject.toml");
61
+ if (existsSync(pyproject)) {
62
+ const text = readFileSync(pyproject, "utf8");
63
+ const inProject = /\[project\][^[]*?^\s*name\s*=\s*["']([^"']+)["']/ms.exec(text);
64
+ const inPoetry = /\[tool\.poetry\][^[]*?^\s*name\s*=\s*["']([^"']+)["']/ms.exec(text);
65
+ const found = inProject?.[1] ?? inPoetry?.[1];
66
+ if (found !== undefined)
67
+ return { name: found, evidence: `pyproject.toml declares ${found}` };
68
+ }
69
+ const setupCfg = join(absoluteRoot, "setup.cfg");
70
+ if (existsSync(setupCfg)) {
71
+ const found = /^\s*name\s*=\s*(.+)$/m.exec(readFileSync(setupCfg, "utf8"));
72
+ if (found?.[1] !== undefined) {
73
+ return { name: found[1].trim(), evidence: `setup.cfg declares ${found[1].trim()}` };
74
+ }
75
+ }
76
+ return {
77
+ name: ".",
78
+ // The same shape of admission DEC-024 required of the TypeScript adapter:
79
+ // without a declared name, node identity rests on the module path alone.
80
+ evidence: "no pyproject.toml or setup.cfg declares a project name, so node identity falls back to " +
81
+ "the module path alone (DEC-024, DEC-054)",
82
+ };
83
+ }
84
+ /**
85
+ * Candidate files for a dotted module path, in Python's own order.
86
+ *
87
+ * A package's `__init__.py` is checked before a same-named module, because that
88
+ * is what the interpreter does and a graph that disagrees with the interpreter
89
+ * is wrong regardless of which is nicer.
90
+ */
91
+ function candidates(parts) {
92
+ if (parts.length === 0)
93
+ return [];
94
+ const base = parts.join("/");
95
+ return [`${base}/__init__.py`, `${base}.py`, `${base}.pyi`];
96
+ }
97
+ /**
98
+ * Resolve one import to a file, and to a symbol inside it when it names one.
99
+ *
100
+ * Returns `file: undefined` for anything outside the analysed set — the standard
101
+ * library, an installed package, a namespace package this run did not read.
102
+ * That is a scope boundary rather than a failure, and DEC-050 keeps the two
103
+ * apart in the ledger.
104
+ */
105
+ export function resolveImport(record, fromFile, options) {
106
+ const miss = { file: undefined, symbol: undefined, viaPackageInit: false };
107
+ if (record.level > 0) {
108
+ // Relative. `level` counts dots: 1 is the containing package, 2 its parent.
109
+ const here = modulePathOf(fromFile);
110
+ // A module inside a package is addressed relative to the package, so the
111
+ // module's own last segment is dropped first unless it *is* the package.
112
+ const isPackage = fromFile.endsWith("__init__.py");
113
+ const packagePath = isPackage ? [...here] : here.slice(0, -1);
114
+ const base = packagePath.slice(0, packagePath.length - (record.level - 1));
115
+ if (base.length === 0 && record.level > 1)
116
+ return miss;
117
+ const parts = record.module === null ? base : [...base, ...record.module.split(".")];
118
+ // `from . import x` and `from .mod import x`: x may be a submodule or a
119
+ // symbol. The submodule is tried first because it is unambiguous when it
120
+ // exists; a symbol needs the package's `__init__` to be the file.
121
+ if (record.name !== null) {
122
+ const asModule = candidates([...parts, record.name]).find((c) => options.known.has(c));
123
+ if (asModule !== undefined) {
124
+ return { file: asModule, symbol: undefined, viaPackageInit: false };
125
+ }
126
+ }
127
+ const asFile = candidates(parts).find((c) => options.known.has(c));
128
+ if (asFile === undefined)
129
+ return miss;
130
+ return {
131
+ file: asFile,
132
+ symbol: record.name ?? undefined,
133
+ viaPackageInit: asFile.endsWith("__init__.py"),
134
+ };
135
+ }
136
+ // Absolute: try each import root. `import a.b.c` binds `a`, but the file the
137
+ // specifier names is still `a/b/c`, so the whole dotted path is resolved.
138
+ const dotted = (record.module ?? "").split(".").filter((p) => p !== "");
139
+ if (dotted.length === 0)
140
+ return miss;
141
+ for (const root of options.roots) {
142
+ const prefix = root === "" ? [] : root.split("/");
143
+ if (record.name !== null) {
144
+ const asModule = candidates([...prefix, ...dotted, record.name]).find((c) => options.known.has(c));
145
+ if (asModule !== undefined) {
146
+ return { file: asModule, symbol: undefined, viaPackageInit: false };
147
+ }
148
+ }
149
+ const asFile = candidates([...prefix, ...dotted]).find((c) => options.known.has(c));
150
+ if (asFile !== undefined) {
151
+ return {
152
+ file: asFile,
153
+ symbol: record.name ?? undefined,
154
+ viaPackageInit: asFile.endsWith("__init__.py"),
155
+ };
156
+ }
157
+ }
158
+ return miss;
159
+ }
160
+ /**
161
+ * Directories an absolute import can be resolved against.
162
+ *
163
+ * This is the DEC-059 surface for Python, and it is *derived from the files that
164
+ * exist* rather than from a configuration file that may not. A directory is an
165
+ * import root when it directly contains a package — a subdirectory with an
166
+ * `__init__.py` — or a top-level module. `src/` layouts fall out of that without
167
+ * being special-cased, and so does a repository with no layout at all.
168
+ *
169
+ * Longest first, so a `src/` layout wins over the root when both could match.
170
+ */
171
+ export function importRoots(files) {
172
+ // **A Set, because `files.includes` here was quadratic in the repository.**
173
+ //
174
+ // This ran a linear scan of every file in the run, inside a loop over every
175
+ // file, inside a walk up each path — hundreds of millions of string
176
+ // comparisons on a large repository, and worse the deeper its packages nest.
177
+ // Measured: posthog took 1,655 s against 112 s for home-assistant-core at the
178
+ // same file count (18,479 against 18,189). It reads as a repository-specific
179
+ // slowdown and is nothing of the kind — every repository paid it, in
180
+ // proportion to its size squared.
181
+ const present = new Set(files);
182
+ const roots = new Set();
183
+ // Directories walk out to the same root repeatedly; caching by directory turns
184
+ // the remaining per-file walk into one walk per distinct directory.
185
+ const rootOfDirectory = new Map();
186
+ for (const file of files) {
187
+ const parts = file.split("/");
188
+ if (parts.length === 1) {
189
+ roots.add("");
190
+ continue;
191
+ }
192
+ const directory = parts.slice(0, -1).join("/");
193
+ const cached = rootOfDirectory.get(directory);
194
+ if (cached !== undefined) {
195
+ roots.add(cached);
196
+ continue;
197
+ }
198
+ // The outermost directory that is NOT a package is the import root: walking
199
+ // out of `src/myapp/models.py` stops at `src`, because `src` has no
200
+ // `__init__.py` while `src/myapp` does.
201
+ let depth = parts.length - 1;
202
+ while (depth > 0 && present.has(`${parts.slice(0, depth).join("/")}/__init__.py`)) {
203
+ depth -= 1;
204
+ }
205
+ const root = parts.slice(0, depth).join("/");
206
+ rootOfDirectory.set(directory, root);
207
+ roots.add(root);
208
+ }
209
+ return [...roots].sort((a, b) => b.length - a.length || a.localeCompare(b));
210
+ }
211
+ //# sourceMappingURL=module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
package/dist/orm.d.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The ORM framework extractor for Python — SQLAlchemy and Django.
3
+ *
4
+ * **A `MODEL` is a framework claim, and this file is where the claim is earned.**
5
+ * Golden pattern 07 states the constraint that produced it: in the conformance
6
+ * corpus `Order` and `CreateOrderRequest` are structurally identical, so any
7
+ * classification a *language* adapter made between them would be a name
8
+ * heuristic — the one thing pattern 12 exists to punish. The evidence that
9
+ * separates them is a persistence mapping: a declarative base, a table binding,
10
+ * a column declaration. All three are written in the source, and this file reads
11
+ * them rather than inferring them.
12
+ *
13
+ * Nothing here is a name rule. `isTestSuiteClass` in `extract.ts` deliberately
14
+ * keeps one — a base whose name ends in `TestCase` — because the cost of being
15
+ * wrong is a test attributed to the wrong suite. The cost of being wrong here is
16
+ * a `MODEL` node that no database backs, which corrupts every data-propagation
17
+ * traversal above it, so provenance or a chain to provenance is the only
18
+ * admission. A class named `Model` proves nothing.
19
+ *
20
+ * The split with `extract.py` is the same one the rest of this adapter uses: the
21
+ * Python side records syntactic form and names no framework, and every decision
22
+ * about what a form *means* is taken here.
23
+ */
24
+ /** One assignment in a class body, as `extract.py` recorded it. */
25
+ export interface PyAttribute {
26
+ readonly name: string;
27
+ readonly annotation: string | null;
28
+ /** The callee as written — `Column`, `mapped_column`, `models.CharField`. */
29
+ readonly callee: string | null;
30
+ readonly args: readonly string[];
31
+ /** Keyword arguments whose value is a written constant. */
32
+ readonly keywords: Readonly<Record<string, unknown>>;
33
+ /** Keyword arguments that were written with a value this reader cannot state. */
34
+ readonly opaqueKeywords: readonly string[];
35
+ readonly literal: unknown;
36
+ readonly hasLiteral: boolean;
37
+ readonly line: number;
38
+ }
39
+ export type OrmFramework = "sqlalchemy" | "django";
40
+ export interface OrmField {
41
+ readonly name: string;
42
+ readonly nullable: boolean;
43
+ /** Where the answer came from, so a default can be told from a declaration. */
44
+ readonly nullableFrom: "declared" | "annotation" | "framework-default";
45
+ }
46
+ export interface OrmShape {
47
+ readonly framework: OrmFramework;
48
+ /** The mapped table, where the source names one. */
49
+ readonly table: string | null;
50
+ readonly fields: readonly OrmField[];
51
+ /**
52
+ * Fields whose nullability is written in a form this reader cannot evaluate.
53
+ *
54
+ * Listed rather than guessed, and listed rather than dropped. `nullable=FLAG`
55
+ * is a stated answer this reader failed to read, and letting the framework
56
+ * default stand in for it would report the opposite of the source as fact.
57
+ */
58
+ readonly undecided: readonly string[];
59
+ }
60
+ /**
61
+ * Modules whose names, if a base class came from one, make the framework certain.
62
+ *
63
+ * Provenance is written in the import statement and needs no resolution, which
64
+ * is why a model is recognisable at R0. Only the cross-file *chain* — a project's
65
+ * own `class BaseModel(Base)` — needs imports resolved.
66
+ */
67
+ export declare const ORM_BASE_MODULES: Readonly<Record<OrmFramework, ReadonlySet<string>>>;
68
+ /**
69
+ * Calls that *produce* a declarative base rather than being one.
70
+ *
71
+ * SQLAlchemy 1.x writes `Base = declarative_base()` at module level, so the base
72
+ * class of a model is a name bound to a call result and not a class declaration
73
+ * at all. Without this rule every 1.x model in existence is invisible.
74
+ */
75
+ export declare const DECLARATIVE_BASE_FACTORIES: ReadonlySet<string>;
76
+ /**
77
+ * Is this attribute a mapped column, and by which framework's rule?
78
+ *
79
+ * Django's rule is shape-based rather than a name list: every field class in
80
+ * `django.db.models` ends in `Field`, and new ones are added by Django and by
81
+ * every third-party app. A closed list would go stale on each release and would
82
+ * miss `JSONField`, `ArrayField` and every custom field a project writes. The
83
+ * receiver still has to be the imported `models` module, so this is not a name
84
+ * heuristic — the provenance is checked by the caller before this is consulted.
85
+ */
86
+ export declare function isColumnCall(attribute: PyAttribute, framework: OrmFramework): boolean;
87
+ /** A base class name that is the framework's own, rather than a project's. */
88
+ export declare function isFrameworkBaseName(name: string, framework: OrmFramework): boolean;
89
+ /**
90
+ * The mapped shape of a class already established to belong to `framework`.
91
+ *
92
+ * Returns `null` where the class declares no column at all. That guard is what
93
+ * keeps a declarative base out of the graph as a model of its own: `class
94
+ * Base(DeclarativeBase): pass` inherits the provenance and maps nothing, and a
95
+ * `MODEL` with no fields is a claim about a table that does not exist.
96
+ */
97
+ export declare function ormShapeOf(input: {
98
+ readonly framework: OrmFramework;
99
+ readonly attributes: readonly PyAttribute[];
100
+ readonly meta: readonly PyAttribute[];
101
+ }): OrmShape | null;
102
+ //# sourceMappingURL=orm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orm.d.ts","sourceRoot":"","sources":["../src/orm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,iFAAiF;IACjF,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEnD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,EAAE,UAAU,GAAG,YAAY,GAAG,mBAAmB,CAAC;CACxE;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAQhF,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAIzD,CAAC;AA2CH;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,GAAG,OAAO,CAMrF;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,OAAO,CAElF;AA8DD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;CACvC,GAAG,QAAQ,GAAG,IAAI,CA0ClB"}
package/dist/orm.js ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The ORM framework extractor for Python — SQLAlchemy and Django.
3
+ *
4
+ * **A `MODEL` is a framework claim, and this file is where the claim is earned.**
5
+ * Golden pattern 07 states the constraint that produced it: in the conformance
6
+ * corpus `Order` and `CreateOrderRequest` are structurally identical, so any
7
+ * classification a *language* adapter made between them would be a name
8
+ * heuristic — the one thing pattern 12 exists to punish. The evidence that
9
+ * separates them is a persistence mapping: a declarative base, a table binding,
10
+ * a column declaration. All three are written in the source, and this file reads
11
+ * them rather than inferring them.
12
+ *
13
+ * Nothing here is a name rule. `isTestSuiteClass` in `extract.ts` deliberately
14
+ * keeps one — a base whose name ends in `TestCase` — because the cost of being
15
+ * wrong is a test attributed to the wrong suite. The cost of being wrong here is
16
+ * a `MODEL` node that no database backs, which corrupts every data-propagation
17
+ * traversal above it, so provenance or a chain to provenance is the only
18
+ * admission. A class named `Model` proves nothing.
19
+ *
20
+ * The split with `extract.py` is the same one the rest of this adapter uses: the
21
+ * Python side records syntactic form and names no framework, and every decision
22
+ * about what a form *means* is taken here.
23
+ */
24
+ /**
25
+ * Modules whose names, if a base class came from one, make the framework certain.
26
+ *
27
+ * Provenance is written in the import statement and needs no resolution, which
28
+ * is why a model is recognisable at R0. Only the cross-file *chain* — a project's
29
+ * own `class BaseModel(Base)` — needs imports resolved.
30
+ */
31
+ export const ORM_BASE_MODULES = {
32
+ sqlalchemy: new Set([
33
+ "sqlalchemy.orm",
34
+ "sqlalchemy.orm.decl_api",
35
+ "sqlalchemy.ext.declarative",
36
+ "sqlalchemy.ext.declarative.api",
37
+ ]),
38
+ django: new Set(["django.db", "django.db.models", "django.db.models.base"]),
39
+ };
40
+ /** The framework's own base class names. A project's own base reaches these by chain. */
41
+ const ORM_BASE_NAMES = {
42
+ // `Base` is deliberately absent. It is what almost every SQLAlchemy project
43
+ // calls its declarative base, and admitting it would be a name rule wearing a
44
+ // framework's clothes — a project is free to call its base anything, and
45
+ // something else is free to be called `Base`. The 1.x shape is reached through
46
+ // `DECLARATIVE_BASE_FACTORIES` instead, which is provenance.
47
+ sqlalchemy: new Set(["DeclarativeBase"]),
48
+ django: new Set(["Model"]),
49
+ };
50
+ /**
51
+ * Calls that *produce* a declarative base rather than being one.
52
+ *
53
+ * SQLAlchemy 1.x writes `Base = declarative_base()` at module level, so the base
54
+ * class of a model is a name bound to a call result and not a class declaration
55
+ * at all. Without this rule every 1.x model in existence is invisible.
56
+ */
57
+ export const DECLARATIVE_BASE_FACTORIES = new Set([
58
+ "declarative_base",
59
+ "as_declarative",
60
+ "registry",
61
+ ]);
62
+ /** Column constructors, per framework. */
63
+ const COLUMN_CALLS = {
64
+ sqlalchemy: new Set(["Column", "mapped_column", "deferred", "synonym_for"]),
65
+ /**
66
+ * Django's relational fields, which are the exception to the `…Field` shape
67
+ * below and were **not** guessed at — they were counted.
68
+ *
69
+ * Across django's own tree `ForeignKey` is the second most common field
70
+ * constructor in existence, 914 uses against `CharField`'s 1,226, and the
71
+ * shape rule alone missed every one of them because it is the one field class
72
+ * Django did not suffix. `GenericForeignKey` and `GenericRelation` came out of
73
+ * the same count at 59 and 46.
74
+ */
75
+ django: new Set([
76
+ "ForeignKey",
77
+ "OneToOneField",
78
+ "ManyToManyField",
79
+ "GenericForeignKey",
80
+ "GenericRelation",
81
+ ]),
82
+ };
83
+ /**
84
+ * Attribute names that are configuration rather than columns.
85
+ *
86
+ * `__tablename__` is the table binding and `__table_args__` its options; neither
87
+ * is a field, and reporting them as fields would put two members in every model
88
+ * that no query can select.
89
+ */
90
+ const NOT_A_FIELD = new Set([
91
+ "__tablename__",
92
+ "__table_args__",
93
+ "__mapper_args__",
94
+ "__abstract__",
95
+ "objects",
96
+ "Meta",
97
+ ]);
98
+ const leafOf = (dotted) => dotted === null ? "" : (dotted.split(".").pop() ?? "");
99
+ /**
100
+ * Is this attribute a mapped column, and by which framework's rule?
101
+ *
102
+ * Django's rule is shape-based rather than a name list: every field class in
103
+ * `django.db.models` ends in `Field`, and new ones are added by Django and by
104
+ * every third-party app. A closed list would go stale on each release and would
105
+ * miss `JSONField`, `ArrayField` and every custom field a project writes. The
106
+ * receiver still has to be the imported `models` module, so this is not a name
107
+ * heuristic — the provenance is checked by the caller before this is consulted.
108
+ */
109
+ export function isColumnCall(attribute, framework) {
110
+ if (attribute.callee === null)
111
+ return false;
112
+ const leaf = leafOf(attribute.callee);
113
+ if (COLUMN_CALLS[framework].has(leaf))
114
+ return true;
115
+ if (framework === "sqlalchemy")
116
+ return false;
117
+ return leaf.endsWith("Field") && leaf.length > "Field".length;
118
+ }
119
+ /** A base class name that is the framework's own, rather than a project's. */
120
+ export function isFrameworkBaseName(name, framework) {
121
+ return ORM_BASE_NAMES[framework].has(leafOf(name));
122
+ }
123
+ /**
124
+ * Nullability for one mapped column.
125
+ *
126
+ * Order is the framework's own, not this reader's preference. An explicit
127
+ * keyword is the source's answer and wins outright. A primary key is not
128
+ * nullable — that is SQL, not a convention. SQLAlchemy 2.0 then derives
129
+ * nullability from the `Mapped[...]` annotation, and only when none of those
130
+ * spoke does the framework default apply.
131
+ */
132
+ function nullabilityOf(attribute, framework) {
133
+ const keyword = framework === "django" ? "null" : "nullable";
134
+ if (attribute.opaqueKeywords.includes(keyword))
135
+ return { undecided: true };
136
+ const declared = attribute.keywords[keyword];
137
+ if (typeof declared === "boolean") {
138
+ return { name: attribute.name, nullable: declared, nullableFrom: "declared" };
139
+ }
140
+ if (attribute.keywords["primary_key"] === true) {
141
+ return { name: attribute.name, nullable: false, nullableFrom: "declared" };
142
+ }
143
+ if (framework === "django") {
144
+ // Django's documented default, and the only one it has: a field is NOT NULL
145
+ // unless the model says otherwise.
146
+ return { name: attribute.name, nullable: false, nullableFrom: "framework-default" };
147
+ }
148
+ // SQLAlchemy 2.0: `Mapped[str]` is NOT NULL, `Mapped[str | None]` is nullable.
149
+ // The annotation is the declaration in this style, so it ranks above the
150
+ // 1.x-era default rather than below it.
151
+ const annotation = attribute.annotation;
152
+ if (annotation !== null && /\bMapped\s*\[/.test(annotation)) {
153
+ const inner = annotation.slice(annotation.indexOf("[") + 1, annotation.lastIndexOf("]"));
154
+ const nullable = /\bOptional\s*\[/.test(inner) || /\bNone\b/.test(inner);
155
+ return { name: attribute.name, nullable, nullableFrom: "annotation" };
156
+ }
157
+ // SQLAlchemy 1.x: a column is nullable unless it says otherwise. The opposite
158
+ // of Django's default, which is why neither is written once for both.
159
+ return { name: attribute.name, nullable: true, nullableFrom: "framework-default" };
160
+ }
161
+ /** The table a model binds to, where the source names one. */
162
+ function tableOf(attributes, meta, framework) {
163
+ const named = framework === "django" ? "db_table" : "__tablename__";
164
+ const source = framework === "django" ? meta : attributes;
165
+ for (const attribute of source) {
166
+ if (attribute.name === named && typeof attribute.literal === "string")
167
+ return attribute.literal;
168
+ }
169
+ return null;
170
+ }
171
+ /**
172
+ * The mapped shape of a class already established to belong to `framework`.
173
+ *
174
+ * Returns `null` where the class declares no column at all. That guard is what
175
+ * keeps a declarative base out of the graph as a model of its own: `class
176
+ * Base(DeclarativeBase): pass` inherits the provenance and maps nothing, and a
177
+ * `MODEL` with no fields is a claim about a table that does not exist.
178
+ */
179
+ export function ormShapeOf(input) {
180
+ const fields = [];
181
+ const undecided = [];
182
+ for (const attribute of input.attributes) {
183
+ if (NOT_A_FIELD.has(attribute.name) || attribute.name.startsWith("__"))
184
+ continue;
185
+ if (!isColumnCall(attribute, input.framework))
186
+ continue;
187
+ const read = nullabilityOf(attribute, input.framework);
188
+ if ("undecided" in read)
189
+ undecided.push(attribute.name);
190
+ else
191
+ fields.push(read);
192
+ }
193
+ // **The column requirement is SQLAlchemy's alone, and the asymmetry is the
194
+ // frameworks', not this reader's.**
195
+ //
196
+ // SQLAlchemy's declarative base is a class the *project* writes — `class
197
+ // Base(DeclarativeBase): pass` — so it inherits the provenance that admits a
198
+ // model and maps nothing. Without this guard the base itself becomes a `MODEL`
199
+ // describing a table that does not exist.
200
+ //
201
+ // Django has no such class in the analysed set: `models.Model` is Django's own
202
+ // and is never a subclass of itself. Inheriting it IS the declaration, and
203
+ // `class Foo(models.Model): pass` is a complete model — Django gives it an
204
+ // implicit primary key. Requiring a column here cost 317 real models on
205
+ // django's own tree, which is how the asymmetry was found rather than reasoned
206
+ // about.
207
+ const table = tableOf(input.attributes, input.meta, input.framework);
208
+ if (input.framework === "sqlalchemy" &&
209
+ fields.length === 0 &&
210
+ undecided.length === 0 &&
211
+ table === null) {
212
+ return null;
213
+ }
214
+ return {
215
+ framework: input.framework,
216
+ table,
217
+ fields: [...fields].sort((a, b) => a.name.localeCompare(b.name)),
218
+ undecided: [...undecided].sort(),
219
+ };
220
+ }
221
+ //# sourceMappingURL=orm.js.map