@presolve/typescript-authority 0.2.0-beta.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 ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2026 Presolve contributors
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.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { analyzeV2Authoring } from "../src/index.js";
3
+
4
+ try {
5
+ let input = "";
6
+ for await (const chunk of process.stdin) input += chunk;
7
+ const request = JSON.parse(input);
8
+ const response = await analyzeV2Authoring(request);
9
+ process.stdout.write(`${JSON.stringify(response)}\n`);
10
+ } catch (error) {
11
+ process.stderr.write(`presolve-typescript-authority: ${error instanceof Error ? error.message : String(error)}\n`);
12
+ process.exitCode = 1;
13
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@presolve/typescript-authority",
3
+ "version": "0.2.0-beta.1",
4
+ "description": "The sole TypeScript semantic-authority boundary for Presolve.",
5
+ "license": "MIT OR Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/fierstdev/presolve.git",
10
+ "directory": "packages/typescript-authority"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "tag": "beta"
15
+ },
16
+ "exports": {
17
+ ".": "./src/index.js"
18
+ },
19
+ "bin": {
20
+ "presolve-typescript-authority": "./bin/presolve-typescript-authority.mjs"
21
+ },
22
+ "dependencies": {
23
+ "@typescript/native": "npm:typescript@^7.0.2"
24
+ },
25
+ "scripts": {
26
+ "test": "node --test test/*.test.mjs"
27
+ }
28
+ }
package/src/index.js ADDED
@@ -0,0 +1,330 @@
1
+ import { API, SymbolFlags } from "@typescript/native/unstable/async";
2
+ import { getTokenAtPosition, SyntaxKind } from "@typescript/native/unstable/ast";
3
+ import { dirname, relative, resolve, sep } from "node:path";
4
+
5
+ export {
6
+ CANONICAL_INTRINSIC_KINDS,
7
+ classifyResolvedComponentHeritage,
8
+ classifyResolvedIntrinsic,
9
+ createCanonicalIntrinsicRegistry,
10
+ } from "./intrinsics.js";
11
+ export { analyzeV2Authoring, V2_AUTHORED_AUTHORITY_SCHEMA_VERSION } from "./v2-authoring.js";
12
+
13
+ export const TYPESCRIPT_SEMANTIC_AUTHORITY_SCHEMA_VERSION = 1;
14
+ export const PRIMARY_TYPESCRIPT_VERSION = "7.0.2";
15
+
16
+ /**
17
+ * Queries TypeScript's semantic engine and returns only Presolve-owned,
18
+ * serializable products. Compiler domain modules must consume this boundary
19
+ * rather than importing concrete TypeScript APIs.
20
+ */
21
+ export async function analyzeTypeScriptProject(request) {
22
+ validateRequest(request);
23
+ const configFile = resolve(request.cwd ?? process.cwd(), request.configFile);
24
+ const api = new API({ cwd: request.cwd ?? process.cwd() });
25
+
26
+ try {
27
+ const config = await api.parseConfigFile(configFile);
28
+ const snapshot = await api.updateSnapshot({ openProjects: [configFile] });
29
+ const project = snapshot.getProject(configFile);
30
+ if (!project) throw new Error(`TypeScript did not open project ${configFile}`);
31
+
32
+ const queries = request.queries ?? {};
33
+ const diagnostics = await collectDiagnostics(project.program, config);
34
+ const response = {
35
+ schemaVersion: TYPESCRIPT_SEMANTIC_AUTHORITY_SCHEMA_VERSION,
36
+ typeScriptVersion: PRIMARY_TYPESCRIPT_VERSION,
37
+ project: {
38
+ configFile: project.configFileName,
39
+ rootFiles: [...project.rootFiles].sort(),
40
+ },
41
+ diagnostics,
42
+ symbols: await querySymbols(project, queries.symbols ?? []),
43
+ componentHeritage: await queryComponentHeritage(project, queries.componentHeritage ?? []),
44
+ types: await queryTypes(project, queries.types ?? []),
45
+ contextualTypes: await queryContextualTypes(project, queries.contextualTypes ?? []),
46
+ signatures: await querySignatures(project, queries.signatures ?? []),
47
+ assignability: await queryAssignability(project, queries.assignability ?? []),
48
+ modules: await queryModules(project, queries.modules ?? []),
49
+ };
50
+ await snapshot.dispose();
51
+ return response;
52
+ } finally {
53
+ await api.close();
54
+ }
55
+ }
56
+
57
+ async function collectDiagnostics(program, config) {
58
+ const sources = [
59
+ ["config", config.diagnostics ?? []],
60
+ ["program", await program.getProgramDiagnostics()],
61
+ ["syntactic", await program.getSyntacticDiagnostics()],
62
+ ["bind", await program.getBindDiagnostics()],
63
+ ["semantic", await program.getSemanticDiagnostics()],
64
+ ];
65
+ return sources
66
+ .flatMap(([source, diagnostics]) => diagnostics.map(diagnostic => serializeDiagnostic(source, diagnostic)))
67
+ .sort(compareDiagnostics);
68
+ }
69
+
70
+ async function querySymbols(project, queries) {
71
+ return Promise.all(queries.map(async query => {
72
+ const symbol = await symbolAt(project, query);
73
+ return { id: query.id, symbol: await serializeSymbol(project, symbol) };
74
+ }));
75
+ }
76
+
77
+ /**
78
+ * Serializes the resolved direct-and-indirect base chain for a class symbol.
79
+ * It assigns no framework meaning: callers must classify these symbols through
80
+ * the canonical intrinsic registry.
81
+ */
82
+ async function queryComponentHeritage(project, queries) {
83
+ return Promise.all(queries.map(async query => {
84
+ const symbol = await symbolAt(project, query);
85
+ return {
86
+ id: query.id,
87
+ symbol: await serializeSymbol(project, symbol),
88
+ bases: await resolvedBaseSymbols(project, symbol),
89
+ };
90
+ }));
91
+ }
92
+
93
+ async function queryTypes(project, queries) {
94
+ return Promise.all(queries.map(async query => {
95
+ const type = await typeAt(project, query);
96
+ return { id: query.id, type: await serializeType(project.checker, type) };
97
+ }));
98
+ }
99
+
100
+ async function queryContextualTypes(project, queries) {
101
+ return Promise.all(queries.map(async query => {
102
+ const { file, token } = await tokenAt(project, query);
103
+ const type = await nearestContextualType(project.checker, token, file);
104
+ return { id: query.id, type: await serializeType(project.checker, type) };
105
+ }));
106
+ }
107
+
108
+ async function querySignatures(project, queries) {
109
+ return Promise.all(queries.map(async query => {
110
+ const { file, token } = await tokenAt(project, query);
111
+ const signature = await nearestSignature(project.checker, token);
112
+ return { id: query.id, signature: await serializeSignature(project.checker, signature) };
113
+ }));
114
+ }
115
+
116
+ async function queryAssignability(project, queries) {
117
+ return Promise.all(queries.map(async query => {
118
+ const source = await typeAt(project, query.source);
119
+ const target = await typeAt(project, query.target);
120
+ return {
121
+ id: query.id,
122
+ assignable: Boolean(source && target && await project.checker.isTypeAssignableTo(source, target)),
123
+ source: await serializeType(project.checker, source),
124
+ target: await serializeType(project.checker, target),
125
+ };
126
+ }));
127
+ }
128
+
129
+ async function queryModules(project, queries) {
130
+ return Promise.all(queries.map(async query => {
131
+ const { file, token } = await tokenAt(project, query);
132
+ const symbol = await project.checker.getSymbolAtPosition(file.fileName, query.position);
133
+ const module = await serializeSymbol(project, symbol);
134
+ return {
135
+ id: query.id,
136
+ module: module && {
137
+ ...module,
138
+ specifier: token.getText(file).replace(/^(?:"|')|(?:"|')$/g, ""),
139
+ resolvedModulePaths: module.identity.declarationModules,
140
+ },
141
+ };
142
+ }));
143
+ }
144
+
145
+ async function symbolAt(project, query) {
146
+ const { file } = await tokenAt(project, query);
147
+ return project.checker.getSymbolAtPosition(file.fileName, query.position);
148
+ }
149
+
150
+ async function resolvedBaseSymbols(project, symbol) {
151
+ const bases = [];
152
+ const seen = new Set();
153
+ let current = await resolvedSymbol(project.checker, symbol);
154
+ while (current && !seen.has(current.id)) {
155
+ seen.add(current.id);
156
+ const base = await directBaseSymbol(project, current);
157
+ if (!base) {
158
+ // The compiler's structural selection points at a heritage expression,
159
+ // so a direct `extends Component` query resolves to Component itself.
160
+ // Preserve that terminal resolved base without treating source spelling
161
+ // as framework meaning.
162
+ if (bases.length === 0) bases.push(await serializeSymbol(project, current));
163
+ break;
164
+ }
165
+ bases.push(await serializeSymbol(project, base));
166
+ current = await resolvedSymbol(project.checker, base);
167
+ }
168
+ return bases;
169
+ }
170
+
171
+ async function resolvedSymbol(checker, symbol) {
172
+ if (!symbol) return undefined;
173
+ return (symbol.flags & SymbolFlags.Alias) === 0
174
+ ? symbol
175
+ : checker.getAliasedSymbol(symbol);
176
+ }
177
+
178
+ async function directBaseSymbol(project, symbol) {
179
+ for (const declarationHandle of symbol.declarations ?? []) {
180
+ const declaration = await declarationHandle.resolve();
181
+ for (const clause of declaration.heritageClauses ?? []) {
182
+ if (clause.token !== SyntaxKind.ExtendsKeyword) continue;
183
+ const base = clause.types?.[0]?.expression;
184
+ if (!base) continue;
185
+ const source = declaration.getSourceFile();
186
+ return project.checker.getSymbolAtPosition(source.fileName, base.getStart(source));
187
+ }
188
+ }
189
+ return undefined;
190
+ }
191
+
192
+ async function typeAt(project, query) {
193
+ const { file } = await tokenAt(project, query);
194
+ return project.checker.getTypeAtPosition(file.fileName, query.position);
195
+ }
196
+
197
+ async function tokenAt(project, query) {
198
+ validatePositionQuery(query);
199
+ const fileName = resolve(query.file);
200
+ const file = await project.program.getSourceFile(fileName);
201
+ if (!file) throw new Error(`TypeScript project does not contain ${fileName}`);
202
+ if (query.position < 0 || query.position > file.text.length) {
203
+ throw new RangeError(`position ${query.position} is outside ${fileName}`);
204
+ }
205
+ return { file, token: getTokenAtPosition(file, query.position) };
206
+ }
207
+
208
+ async function nearestContextualType(checker, token, file) {
209
+ for (let node = token; node; node = node.parent) {
210
+ const type = await checker.getContextualType(node);
211
+ if (type) return type;
212
+ if (node === file) break;
213
+ }
214
+ return undefined;
215
+ }
216
+
217
+ async function nearestSignature(checker, token) {
218
+ for (let node = token; node; node = node.parent) {
219
+ try {
220
+ const signature = await checker.getResolvedSignature(node);
221
+ if (signature) return signature;
222
+ } catch {
223
+ // TypeScript only resolves signatures for invocation-like nodes.
224
+ }
225
+ }
226
+ return undefined;
227
+ }
228
+
229
+ async function serializeSymbol(project, symbol) {
230
+ if (!symbol) return undefined;
231
+ const { checker } = project;
232
+ const declarationPaths = symbol.declarations
233
+ .map(declaration => String(declaration.path))
234
+ .sort();
235
+ const serialized = {
236
+ name: symbol.name,
237
+ flags: symbol.flags,
238
+ declarationPaths,
239
+ identity: identityForSymbol(project, symbol, declarationPaths),
240
+ };
241
+ if ((symbol.flags & SymbolFlags.Alias) === 0) return serialized;
242
+
243
+ const target = await checker.getAliasedSymbol(symbol);
244
+ return {
245
+ ...serialized,
246
+ aliasTarget: {
247
+ name: target.name,
248
+ flags: target.flags,
249
+ declarationPaths: target.declarations.map(declaration => String(declaration.path)).sort(),
250
+ identity: identityForSymbol(
251
+ project,
252
+ target,
253
+ target.declarations.map(declaration => String(declaration.path)).sort(),
254
+ ),
255
+ unknown: await checker.isUnknownSymbol(target),
256
+ },
257
+ };
258
+ }
259
+
260
+ function identityForSymbol(project, symbol, declarationPaths) {
261
+ const projectRoot = dirname(String(project.id));
262
+ return {
263
+ name: symbol.name,
264
+ flags: symbol.flags,
265
+ declarationModules: [...new Set(declarationPaths.map(path => normalizeProjectPath(projectRoot, path)))],
266
+ };
267
+ }
268
+
269
+ function normalizeProjectPath(projectRoot, path) {
270
+ const relativePath = relative(projectRoot, path);
271
+ return relativePath.split(sep).join("/") || ".";
272
+ }
273
+
274
+ async function serializeType(checker, type) {
275
+ if (!type) return undefined;
276
+ return {
277
+ text: await checker.typeToString(type),
278
+ flags: type.flags,
279
+ error: type.isErrorType(),
280
+ };
281
+ }
282
+
283
+ async function serializeSignature(checker, signature) {
284
+ if (!signature) return undefined;
285
+ const parameters = await signature.getParameters();
286
+ return {
287
+ parameterTypes: await Promise.all(parameters.map(async parameter => ({
288
+ name: parameter.name,
289
+ type: await serializeType(checker, await checker.getTypeOfSymbol(parameter)),
290
+ }))),
291
+ returnType: await serializeType(checker, await checker.getReturnTypeOfSignature(signature)),
292
+ };
293
+ }
294
+
295
+ function serializeDiagnostic(source, diagnostic) {
296
+ return {
297
+ source,
298
+ code: diagnostic.code,
299
+ category: diagnostic.category,
300
+ file: diagnostic.fileName,
301
+ start: diagnostic.pos,
302
+ end: diagnostic.end,
303
+ message: diagnostic.text,
304
+ };
305
+ }
306
+
307
+ function compareDiagnostics(left, right) {
308
+ return (left.file ?? "").localeCompare(right.file ?? "")
309
+ || left.start - right.start
310
+ || left.code - right.code
311
+ || left.source.localeCompare(right.source);
312
+ }
313
+
314
+ function validateRequest(request) {
315
+ if (!request || typeof request !== "object" || typeof request.configFile !== "string") {
316
+ throw new TypeError("TypeScript authority requests require a configFile");
317
+ }
318
+ if (request.cwd !== undefined && typeof request.cwd !== "string") {
319
+ throw new TypeError("TypeScript authority cwd must be a string");
320
+ }
321
+ if (request.queries !== undefined && (typeof request.queries !== "object" || Array.isArray(request.queries))) {
322
+ throw new TypeError("TypeScript authority queries must be an object");
323
+ }
324
+ }
325
+
326
+ function validatePositionQuery(query) {
327
+ if (!query || typeof query.file !== "string" || !Number.isInteger(query.position)) {
328
+ throw new TypeError("TypeScript authority position queries require file and integer position");
329
+ }
330
+ }
@@ -0,0 +1,49 @@
1
+ export const CANONICAL_INTRINSIC_KINDS = Object.freeze([
2
+ "component", "state", "action", "computed", "effect", "slot", "context",
3
+ "provide", "consume", "form", "serialize", "field", "validate", "submit",
4
+ "resource", "loader", "server_action", "opaque", "environment_public",
5
+ ]);
6
+
7
+ /** Builds a registry exclusively from TypeScript-resolved canonical targets. */
8
+ export function createCanonicalIntrinsicRegistry(entries) {
9
+ const registry = new Map();
10
+ for (const entry of entries) {
11
+ if (!CANONICAL_INTRINSIC_KINDS.includes(entry.kind)) {
12
+ throw new TypeError(`unknown Presolve intrinsic kind ${entry.kind}`);
13
+ }
14
+ const identity = targetIdentity(entry.symbol);
15
+ if (!identity) throw new TypeError(`intrinsic ${entry.kind} has no resolved target identity`);
16
+ const key = identityKey(identity);
17
+ if (registry.has(key)) throw new TypeError(`duplicate canonical intrinsic identity for ${entry.kind}`);
18
+ registry.set(key, { kind: entry.kind, identity });
19
+ }
20
+ return registry;
21
+ }
22
+
23
+ /** Classifies a use-site result from `analyzeTypeScriptProject`, never its spelling. */
24
+ export function classifyResolvedIntrinsic(registry, symbol) {
25
+ const identity = targetIdentity(symbol);
26
+ return identity ? registry.get(identityKey(identity)) : undefined;
27
+ }
28
+
29
+ /**
30
+ * Classifies a resolved base-class chain without relying on the local spelling
31
+ * of a component base. The TypeScript adapter supplies the chain, including
32
+ * aliases and indirect bases; the registry supplies framework meaning.
33
+ */
34
+ export function classifyResolvedComponentHeritage(registry, bases) {
35
+ for (const base of bases) {
36
+ const intrinsic = classifyResolvedIntrinsic(registry, base);
37
+ if (intrinsic?.kind === "component") return intrinsic;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function targetIdentity(symbol) {
43
+ return symbol?.aliasTarget?.identity ?? symbol?.identity;
44
+ }
45
+
46
+ function identityKey(identity) {
47
+ if (!identity || !Array.isArray(identity.declarationModules)) return "";
48
+ return JSON.stringify([identity.name, identity.flags, [...identity.declarationModules].sort()]);
49
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ analyzeTypeScriptProject,
3
+ classifyResolvedComponentHeritage,
4
+ classifyResolvedIntrinsic,
5
+ createCanonicalIntrinsicRegistry,
6
+ } from "./index.js";
7
+
8
+ export const V2_AUTHORED_AUTHORITY_SCHEMA_VERSION = 4;
9
+
10
+ /**
11
+ * Resolves explicit source positions for the implemented decorator-free V2
12
+ * authoring forms. Syntax selection belongs to the caller; this bridge owns
13
+ * only TypeScript resolution and canonical-registry classification.
14
+ */
15
+ export async function analyzeV2Authoring(request) {
16
+ validateV2AuthoringRequest(request);
17
+ const queries = {
18
+ symbols: [
19
+ ...(request.canonical.component ? [{ id: "canonical:component", ...request.canonical.component }] : []),
20
+ ...(request.canonical.state ? [{ id: "canonical:state", ...request.canonical.state }] : []),
21
+ ...(request.canonical.action ? [{ id: "canonical:action", ...request.canonical.action }] : []),
22
+ ...(request.canonical.effect ? [{ id: "canonical:effect", ...request.canonical.effect }] : []),
23
+ ...(request.canonical.environment ? [{ id: "canonical:environment", ...request.canonical.environment }] : []),
24
+ ...request.states.map(site => ({ id: `state:${site.id}`, file: site.file, position: site.position })),
25
+ ...request.actions.map(site => ({ id: `action:${site.id}`, file: site.file, position: site.position })),
26
+ ...request.effects.map(site => ({ id: `effect:${site.id}`, file: site.file, position: site.position })),
27
+ ...request.environmentPublic.flatMap(site => [
28
+ { id: `environment-object:${site.id}`, file: site.file, position: site.objectPosition },
29
+ { id: `environment-property:${site.id}`, file: site.file, position: site.propertyPosition },
30
+ ]),
31
+ ],
32
+ componentHeritage: request.components.map(site => ({
33
+ id: `component:${site.id}`,
34
+ file: site.file,
35
+ position: site.position,
36
+ })),
37
+ };
38
+ const authority = await analyzeTypeScriptProject({
39
+ configFile: request.configFile,
40
+ ...(request.cwd === undefined ? {} : { cwd: request.cwd }),
41
+ queries,
42
+ });
43
+ const symbols = new Map(authority.symbols.map(entry => [entry.id, entry.symbol]));
44
+ const registry = createCanonicalIntrinsicRegistry([
45
+ ...(request.canonical.component ? [{ kind: "component", symbol: symbols.get("canonical:component") }] : []),
46
+ ...(request.canonical.state ? [{ kind: "state", symbol: symbols.get("canonical:state") }] : []),
47
+ ...(request.canonical.action ? [{ kind: "action", symbol: symbols.get("canonical:action") }] : []),
48
+ ...(request.canonical.effect ? [{ kind: "effect", symbol: symbols.get("canonical:effect") }] : []),
49
+ ...(request.canonical.environment ? [{ kind: "environment_public", symbol: symbols.get("canonical:environment") }] : []),
50
+ ]);
51
+ return {
52
+ schemaVersion: V2_AUTHORED_AUTHORITY_SCHEMA_VERSION,
53
+ diagnostics: authority.diagnostics,
54
+ components: authority.componentHeritage.flatMap(site => {
55
+ const intrinsic = classifyResolvedComponentHeritage(registry, site.bases);
56
+ return intrinsic ? [{ id: stripPrefix(site.id, "component:"), identity: intrinsic.identity }] : [];
57
+ }),
58
+ states: request.states.flatMap(site => {
59
+ const intrinsic = classifyResolvedIntrinsic(registry, symbols.get(`state:${site.id}`));
60
+ return intrinsic?.kind === "state" ? [{ id: site.id, identity: intrinsic.identity }] : [];
61
+ }),
62
+ actions: request.actions.flatMap(site => {
63
+ const intrinsic = classifyResolvedIntrinsic(registry, symbols.get(`action:${site.id}`));
64
+ return intrinsic?.kind === "action" ? [{ id: site.id, identity: intrinsic.identity }] : [];
65
+ }),
66
+ effects: request.effects.flatMap(site => {
67
+ const intrinsic = classifyResolvedIntrinsic(registry, symbols.get(`effect:${site.id}`));
68
+ return intrinsic?.kind === "effect" ? [{ id: site.id, identity: intrinsic.identity }] : [];
69
+ }),
70
+ environmentPublic: request.environmentPublic.flatMap(site => {
71
+ const receiver = classifyResolvedIntrinsic(registry, symbols.get(`environment-object:${site.id}`));
72
+ const member = resolvedIdentity(symbols.get(`environment-property:${site.id}`));
73
+ return receiver?.kind === "environment_public" && member?.name === "public"
74
+ && sameDeclarationModules(member, receiver.identity)
75
+ ? [{ id: site.id, identity: member }]
76
+ : [];
77
+ }),
78
+ };
79
+ }
80
+
81
+ function validateV2AuthoringRequest(request) {
82
+ if (!request || typeof request !== "object" || typeof request.configFile !== "string") {
83
+ throw new TypeError("V2 authoring authority requests require a configFile");
84
+ }
85
+ if (!request.canonical || typeof request.canonical !== "object") {
86
+ throw new TypeError("V2 authoring authority requests require canonical framework positions");
87
+ }
88
+ if (request.schemaVersion !== V2_AUTHORED_AUTHORITY_SCHEMA_VERSION) {
89
+ throw new TypeError(`unsupported V2 authoring authority schema version ${request.schemaVersion}`);
90
+ }
91
+ for (const kind of ["component", "state", "action", "effect", "environment"]) {
92
+ if (request.canonical[kind] !== undefined) {
93
+ validatePosition(request.canonical[kind], `canonical ${kind}`);
94
+ }
95
+ }
96
+ for (const [kind, sites] of [["component", request.components], ["state", request.states], ["action", request.actions], ["effect", request.effects]]) {
97
+ if (!Array.isArray(sites)) throw new TypeError(`V2 authoring ${kind} sites must be an array`);
98
+ if (sites.length > 0 && request.canonical[kind] === undefined) {
99
+ throw new TypeError(`V2 authoring ${kind} sites require a canonical ${kind} position`);
100
+ }
101
+ const ids = new Set();
102
+ for (const site of sites) {
103
+ if (!site || typeof site.id !== "string" || !site.id || ids.has(site.id)) {
104
+ throw new TypeError(`V2 authoring ${kind} sites require unique non-empty ids`);
105
+ }
106
+ ids.add(site.id);
107
+ validatePosition(site, `${kind} site`);
108
+ }
109
+ }
110
+ if (!Array.isArray(request.environmentPublic)) {
111
+ throw new TypeError("V2 authoring environment public sites must be an array");
112
+ }
113
+ if (request.environmentPublic.length > 0 && request.canonical.environment === undefined) {
114
+ throw new TypeError("V2 authoring environment public sites require a canonical environment position");
115
+ }
116
+ const ids = new Set();
117
+ for (const site of request.environmentPublic) {
118
+ if (!site || typeof site.id !== "string" || !site.id || ids.has(site.id)) {
119
+ throw new TypeError("V2 authoring environment public sites require unique non-empty ids");
120
+ }
121
+ ids.add(site.id);
122
+ validatePosition({ file: site.file, position: site.objectPosition }, "environment public object site");
123
+ validatePosition({ file: site.file, position: site.propertyPosition }, "environment public property site");
124
+ }
125
+ }
126
+
127
+ function validatePosition(value, label) {
128
+ if (!value || typeof value.file !== "string" || !Number.isInteger(value.position)) {
129
+ throw new TypeError(`V2 authoring ${label} requires file and integer position`);
130
+ }
131
+ }
132
+
133
+ function stripPrefix(value, prefix) {
134
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
135
+ }
136
+
137
+ function resolvedIdentity(symbol) {
138
+ return symbol?.aliasTarget?.identity ?? symbol?.identity;
139
+ }
140
+
141
+ function sameDeclarationModules(left, right) {
142
+ if (!left || !right) return false;
143
+ return JSON.stringify([...left.declarationModules].sort())
144
+ === JSON.stringify([...right.declarationModules].sort());
145
+ }
@@ -0,0 +1,265 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import test from "node:test";
4
+ import { resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ import {
8
+ analyzeTypeScriptProject,
9
+ analyzeV2Authoring,
10
+ classifyResolvedComponentHeritage,
11
+ classifyResolvedIntrinsic,
12
+ createCanonicalIntrinsicRegistry,
13
+ PRIMARY_TYPESCRIPT_VERSION,
14
+ TYPESCRIPT_SEMANTIC_AUTHORITY_SCHEMA_VERSION,
15
+ V2_AUTHORED_AUTHORITY_SCHEMA_VERSION,
16
+ } from "../src/index.js";
17
+
18
+ const root = resolve(import.meta.dirname, "../../..");
19
+ const configFile = resolve(root, "tests/typescript-compatibility/tsconfig.json");
20
+ const file = resolve(root, "tests/typescript-compatibility/src/compatibility.tsx");
21
+ const source = readFileSync(file, "utf8");
22
+
23
+ function position(text, occurrence = 0) {
24
+ let start = -1;
25
+ for (let index = 0; index <= occurrence; index += 1) {
26
+ start = source.indexOf(text, start + 1);
27
+ }
28
+ assert.notEqual(start, -1, `missing ${text}`);
29
+ return start;
30
+ }
31
+
32
+ test("the authority adapter owns TypeScript semantic queries", async () => {
33
+ const result = await analyzeTypeScriptProject({
34
+ configFile,
35
+ queries: {
36
+ symbols: [{ id: "overload", file, position: position('overload("typed")') }],
37
+ types: [{ id: "generic", file, position: position("box: Box<LocalAlias>") }],
38
+ contextualTypes: [{ id: "callback", file, position: position("value => value.length") }],
39
+ signatures: [{ id: "call", file, position: position('overload("typed")') }],
40
+ assignability: [
41
+ {
42
+ id: "number-to-number",
43
+ source: { file, position: position("selected =") },
44
+ target: { file, position: position("selected =") },
45
+ },
46
+ {
47
+ id: "string-to-number",
48
+ source: { file, position: position("packageValue;") },
49
+ target: { file, position: position("selected =") },
50
+ },
51
+ ],
52
+ modules: [
53
+ { id: "package-export", file, position: position("@compat/library") },
54
+ { id: "re-export", file, position: position("@compat/library", 1) },
55
+ ],
56
+ },
57
+ });
58
+
59
+ assert.equal(result.schemaVersion, TYPESCRIPT_SEMANTIC_AUTHORITY_SCHEMA_VERSION);
60
+ assert.equal(result.typeScriptVersion, PRIMARY_TYPESCRIPT_VERSION);
61
+ assert.equal(result.diagnostics.length, 0);
62
+ assert.equal(result.symbols[0].symbol.name, "overload");
63
+ assert.equal(result.symbols[0].symbol.aliasTarget.name, "overload");
64
+ assert.match(result.symbols[0].symbol.aliasTarget.declarationPaths[0], /@compat\/library\/types\/index\.d\.ts$/);
65
+ assert.deepEqual(result.symbols[0].symbol.aliasTarget.identity.declarationModules, ["node_modules/@compat/library/types/index.d.ts"]);
66
+ assert.equal(result.types[0].type.text, "Box<LocalAlias>");
67
+ assert.equal(result.contextualTypes[0].type.text, "number");
68
+ assert.deepEqual(result.signatures[0].signature.parameterTypes.map(parameter => parameter.type.text), ["string"]);
69
+ assert.equal(result.signatures[0].signature.returnType.text, "number");
70
+ assert.equal(result.assignability[0].assignable, true);
71
+ assert.equal(result.assignability[1].assignable, false);
72
+ assert.match(result.modules[0].module.declarationPaths[0], /@compat\/library\/types\/index\.d\.ts$/);
73
+ assert.equal(result.modules[0].module.specifier, "@compat/library");
74
+ assert.deepEqual(result.modules[0].module.resolvedModulePaths, ["node_modules/@compat/library/types/index.d.ts"]);
75
+ assert.deepEqual(result.modules[1].module.resolvedModulePaths, result.modules[0].module.resolvedModulePaths);
76
+ });
77
+
78
+ test("the authority adapter preserves native TypeScript diagnostics", async () => {
79
+ const diagnostics = await analyzeTypeScriptProject({
80
+ configFile: resolve(root, "tests/typescript-compatibility/diagnostics/tsconfig.json"),
81
+ });
82
+ assert.deepEqual(diagnostics.diagnostics.map(diagnostic => diagnostic.code), [2322, 2345]);
83
+ assert(diagnostics.diagnostics.every(diagnostic => diagnostic.source === "semantic"));
84
+ });
85
+
86
+ test("canonical intrinsics classify resolved exports rather than local spellings", async () => {
87
+ const frameworkFile = resolve(root, "tests/framework-public-api/src/PublicCounter.tsx");
88
+ const frameworkSource = readFileSync(frameworkFile, "utf8");
89
+ const frameworkResult = await analyzeTypeScriptProject({
90
+ configFile: resolve(root, "tests/framework-public-api/tsconfig.json"),
91
+ queries: {
92
+ symbols: [
93
+ { id: "component-import", file: frameworkFile, position: frameworkSource.indexOf("component,") },
94
+ { id: "component-use", file: frameworkFile, position: frameworkSource.indexOf("@component()") + 1 },
95
+ ],
96
+ },
97
+ });
98
+ const registry = createCanonicalIntrinsicRegistry([
99
+ { kind: "component", symbol: frameworkResult.symbols[0].symbol },
100
+ ]);
101
+ assert.equal(classifyResolvedIntrinsic(registry, frameworkResult.symbols[1].symbol)?.kind, "component");
102
+ assert.equal(classifyResolvedIntrinsic(registry, { identity: { name: "component", flags: 0, declarationModules: [] } }), undefined);
103
+ });
104
+
105
+ test("component heritage preserves aliases and indirect bases for registry classification", async () => {
106
+ const frameworkFile = resolve(root, "tests/framework-public-api/src/V2Counter.tsx");
107
+ const frameworkSource = readFileSync(frameworkFile, "utf8");
108
+ const result = await analyzeTypeScriptProject({
109
+ configFile: resolve(root, "tests/framework-public-api/tsconfig.json"),
110
+ queries: {
111
+ symbols: [{ id: "component-import", file: frameworkFile, position: frameworkSource.indexOf("Component") }],
112
+ componentHeritage: [{ id: "counter", file: frameworkFile, position: frameworkSource.indexOf("V2Counter extends") }],
113
+ },
114
+ });
115
+ const registry = createCanonicalIntrinsicRegistry([
116
+ { kind: "component", symbol: result.symbols[0].symbol },
117
+ ]);
118
+ const heritage = result.componentHeritage[0];
119
+ assert.deepEqual(heritage.bases.map(base => base.name), ["V2CounterBase", "Component"]);
120
+ assert.equal(classifyResolvedComponentHeritage(registry, heritage.bases)?.kind, "component");
121
+ });
122
+
123
+ test("the V2 authoring bridge resolves canonical component, State, Action, Effect, and Environment evidence", async () => {
124
+ const frameworkFile = resolve(root, "tests/framework-public-api/src/V2Counter.tsx");
125
+ const frameworkSource = readFileSync(frameworkFile, "utf8");
126
+ const result = await analyzeV2Authoring({
127
+ schemaVersion: V2_AUTHORED_AUTHORITY_SCHEMA_VERSION,
128
+ configFile: resolve(root, "tests/framework-public-api/tsconfig.json"),
129
+ canonical: {
130
+ component: { file: frameworkFile, position: frameworkSource.indexOf("Component") },
131
+ state: { file: frameworkFile, position: frameworkSource.indexOf("state") },
132
+ action: { file: frameworkFile, position: frameworkSource.indexOf("action") },
133
+ effect: { file: frameworkFile, position: frameworkSource.indexOf("effect") },
134
+ environment: { file: frameworkFile, position: frameworkSource.indexOf("runtimeEnvironment") },
135
+ },
136
+ components: [{ id: "counter", file: frameworkFile, position: frameworkSource.indexOf("V2Counter extends") }],
137
+ states: [{ id: "count", file: frameworkFile, position: frameworkSource.indexOf("state(0)") }],
138
+ actions: [{ id: "increment", file: frameworkFile, position: frameworkSource.indexOf("action(()") }],
139
+ effects: [{ id: "syncTitle", file: frameworkFile, position: frameworkSource.indexOf("effect(()") }],
140
+ environmentPublic: [
141
+ {
142
+ id: "application-name",
143
+ file: frameworkFile,
144
+ objectPosition: frameworkSource.indexOf("runtimeEnvironment.public"),
145
+ propertyPosition: frameworkSource.indexOf("runtimeEnvironment.public") + "runtimeEnvironment.".length,
146
+ },
147
+ {
148
+ id: "lookalike",
149
+ file: frameworkFile,
150
+ objectPosition: frameworkSource.indexOf("lookalikeEnvironment.public"),
151
+ propertyPosition: frameworkSource.indexOf("lookalikeEnvironment.public") + "lookalikeEnvironment.".length,
152
+ },
153
+ ],
154
+ });
155
+ assert.equal(result.schemaVersion, V2_AUTHORED_AUTHORITY_SCHEMA_VERSION);
156
+ assert.equal(result.diagnostics.length, 0);
157
+ assert.deepEqual(result.components.map(entry => entry.id), ["counter"]);
158
+ assert.deepEqual(result.states.map(entry => entry.id), ["count"]);
159
+ assert.deepEqual(result.actions.map(entry => entry.id), ["increment"]);
160
+ assert.deepEqual(result.effects.map(entry => entry.id), ["syncTitle"]);
161
+ assert.deepEqual(result.environmentPublic.map(entry => entry.id), ["application-name"]);
162
+ assert.equal(result.components[0].identity.name, "Component");
163
+ assert.equal(result.states[0].identity.name, "state");
164
+ assert.equal(result.actions[0].identity.name, "action");
165
+ assert.equal(result.effects[0].identity.name, "effect");
166
+ assert.equal(result.environmentPublic[0].identity.name, "public");
167
+ });
168
+
169
+ test("the V2 authoring bridge supports a component-only discovery phase", async () => {
170
+ const frameworkFile = resolve(root, "tests/framework-public-api/src/V2Counter.tsx");
171
+ const frameworkSource = readFileSync(frameworkFile, "utf8");
172
+ const result = await analyzeV2Authoring({
173
+ schemaVersion: V2_AUTHORED_AUTHORITY_SCHEMA_VERSION,
174
+ configFile: resolve(root, "tests/framework-public-api/tsconfig.json"),
175
+ canonical: {
176
+ component: { file: frameworkFile, position: frameworkSource.indexOf("Component") },
177
+ },
178
+ components: [{ id: "counter", file: frameworkFile, position: frameworkSource.indexOf("V2Counter extends") }],
179
+ states: [],
180
+ actions: [],
181
+ effects: [],
182
+ environmentPublic: [],
183
+ });
184
+ assert.deepEqual(result.components.map(entry => entry.id), ["counter"]);
185
+ assert.deepEqual(result.states, []);
186
+ assert.deepEqual(result.actions, []);
187
+ assert.deepEqual(result.effects, []);
188
+ });
189
+
190
+ test("the V2 authoring bridge recognizes a direct Component heritage-expression query", async () => {
191
+ const frameworkFile = resolve(root, "tests/framework-public-api/src/V2Counter.tsx");
192
+ const frameworkSource = readFileSync(frameworkFile, "utf8");
193
+ const directFile = resolve(root, "tests/framework-public-api/src/DirectV2.tsx");
194
+ const directSource = readFileSync(directFile, "utf8");
195
+ const result = await analyzeV2Authoring({
196
+ schemaVersion: V2_AUTHORED_AUTHORITY_SCHEMA_VERSION,
197
+ configFile: resolve(root, "tests/framework-public-api/tsconfig.json"),
198
+ canonical: {
199
+ component: { file: frameworkFile, position: frameworkSource.indexOf("Component") },
200
+ },
201
+ components: [{ id: "direct", file: directFile, position: directSource.lastIndexOf("Component") }],
202
+ states: [],
203
+ actions: [],
204
+ effects: [],
205
+ environmentPublic: [],
206
+ });
207
+ assert.deepEqual(result.components.map(entry => entry.id), ["direct"]);
208
+ assert.equal(result.components[0].identity.name, "Component");
209
+ });
210
+
211
+ test("the V2 authoring bridge resolves environment evidence without a component query", async () => {
212
+ const frameworkFile = resolve(root, "tests/framework-public-api/src/V2Counter.tsx");
213
+ const frameworkSource = readFileSync(frameworkFile, "utf8");
214
+ const result = await analyzeV2Authoring({
215
+ schemaVersion: V2_AUTHORED_AUTHORITY_SCHEMA_VERSION,
216
+ configFile: resolve(root, "tests/framework-public-api/tsconfig.json"),
217
+ canonical: {
218
+ environment: { file: frameworkFile, position: frameworkSource.indexOf("runtimeEnvironment") },
219
+ },
220
+ components: [],
221
+ states: [],
222
+ actions: [],
223
+ effects: [],
224
+ environmentPublic: [{
225
+ id: "application-name",
226
+ file: frameworkFile,
227
+ objectPosition: frameworkSource.indexOf("runtimeEnvironment.public"),
228
+ propertyPosition: frameworkSource.indexOf("runtimeEnvironment.public") + "runtimeEnvironment.".length,
229
+ }],
230
+ });
231
+ assert.deepEqual(result.components, []);
232
+ assert.deepEqual(result.environmentPublic.map(entry => entry.id), ["application-name"]);
233
+ });
234
+
235
+ test("the V2 authoring executable speaks the versioned stdin/stdout bridge protocol", () => {
236
+ const frameworkFile = resolve(root, "tests/framework-public-api/src/V2Counter.tsx");
237
+ const frameworkSource = readFileSync(frameworkFile, "utf8");
238
+ const result = spawnSync(
239
+ process.execPath,
240
+ [resolve(import.meta.dirname, "../bin/presolve-typescript-authority.mjs")],
241
+ {
242
+ input: JSON.stringify({
243
+ schemaVersion: V2_AUTHORED_AUTHORITY_SCHEMA_VERSION,
244
+ configFile: resolve(root, "tests/framework-public-api/tsconfig.json"),
245
+ canonical: {
246
+ component: { file: frameworkFile, position: frameworkSource.indexOf("Component") },
247
+ state: { file: frameworkFile, position: frameworkSource.indexOf("state") },
248
+ action: { file: frameworkFile, position: frameworkSource.indexOf("action") },
249
+ effect: { file: frameworkFile, position: frameworkSource.indexOf("effect") },
250
+ environment: { file: frameworkFile, position: frameworkSource.indexOf("runtimeEnvironment") },
251
+ },
252
+ components: [{ id: "counter", file: frameworkFile, position: frameworkSource.indexOf("V2Counter extends") }],
253
+ states: [{ id: "count", file: frameworkFile, position: frameworkSource.indexOf("state(0)") }],
254
+ actions: [{ id: "increment", file: frameworkFile, position: frameworkSource.indexOf("action(()") }],
255
+ effects: [{ id: "syncTitle", file: frameworkFile, position: frameworkSource.indexOf("effect(()") }],
256
+ environmentPublic: [],
257
+ }),
258
+ encoding: "utf8",
259
+ },
260
+ );
261
+ assert.equal(result.status, 0, result.stderr);
262
+ const response = JSON.parse(result.stdout);
263
+ assert.equal(response.schemaVersion, V2_AUTHORED_AUTHORITY_SCHEMA_VERSION);
264
+ assert.deepEqual(response.components.map(entry => entry.id), ["counter"]);
265
+ });