@suss/extractor 0.0.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/dist/index.js ADDED
@@ -0,0 +1,282 @@
1
+ // src/index.ts
2
+ import { createHash } from "crypto";
3
+
4
+ // src/packHelpers.ts
5
+ function httpRouteDiscovery(opts) {
6
+ const kind = opts.kind ?? "handler";
7
+ return opts.importNames.map((importName) => ({
8
+ kind,
9
+ match: {
10
+ type: "registrationCall",
11
+ importModule: opts.importModule,
12
+ importName,
13
+ registrationChain: [...opts.methods]
14
+ },
15
+ bindingExtraction: {
16
+ method: { type: "fromRegistration", position: "methodName" },
17
+ path: { type: "fromRegistration", position: 0 }
18
+ },
19
+ requiresImport: [opts.importModule]
20
+ }));
21
+ }
22
+
23
+ // src/index.ts
24
+ var DEFAULT_OPTIONS = { gapHandling: "permissive" };
25
+ function makeTransitionId(functionName, branch) {
26
+ const { terminal } = branch;
27
+ const statusKey = terminal.statusCode === null ? "none" : terminal.statusCode.type === "literal" ? String(terminal.statusCode.value) : `dyn:${terminal.statusCode.sourceText}`;
28
+ const conditionSig = branch.conditions.map((c) => `${c.polarity}:${c.sourceText}`).join(";");
29
+ const conditionHash = createHash("sha1").update(conditionSig).digest("hex").slice(0, 7);
30
+ return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
31
+ }
32
+ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
33
+ const transitions = raw.branches.map((branch) => {
34
+ const conditions = branch.conditions.map(
35
+ rawConditionToPredicate
36
+ );
37
+ const transition = {
38
+ id: makeTransitionId(raw.identity.name, branch),
39
+ conditions,
40
+ output: terminalToOutput(branch.terminal),
41
+ effects: [
42
+ ...branch.effects.map(effectToIR),
43
+ ...branch.extraEffects ?? []
44
+ ],
45
+ location: branch.location,
46
+ isDefault: branch.isDefault
47
+ };
48
+ if (branch.expectedInput != null) {
49
+ transition.expectedInput = branch.expectedInput;
50
+ }
51
+ return transition;
52
+ });
53
+ const gaps = detectGaps(raw, transitions, options);
54
+ const confidence = assessConfidence(raw);
55
+ const inputs = raw.parameters.map(paramToInput);
56
+ const metadata = buildMetadata(raw);
57
+ return {
58
+ kind: raw.identity.kind,
59
+ location: {
60
+ file: raw.identity.file,
61
+ range: raw.identity.range,
62
+ exportName: raw.identity.exportName
63
+ },
64
+ identity: {
65
+ name: raw.identity.name,
66
+ exportPath: raw.identity.exportPath,
67
+ boundaryBinding: raw.boundaryBinding ?? null
68
+ },
69
+ inputs,
70
+ transitions,
71
+ gaps,
72
+ confidence,
73
+ ...metadata !== null ? { metadata } : {}
74
+ };
75
+ }
76
+ function buildMetadata(raw) {
77
+ const metadata = {};
78
+ const http = buildHttpMetadata(raw);
79
+ if (http !== null) {
80
+ metadata.http = http;
81
+ }
82
+ const graphql = buildGraphqlMetadata(raw);
83
+ if (graphql !== null) {
84
+ metadata.graphql = graphql;
85
+ }
86
+ return Object.keys(metadata).length > 0 ? metadata : null;
87
+ }
88
+ function buildHttpMetadata(raw) {
89
+ const http = {};
90
+ if (raw.declaredContract !== null) {
91
+ http.declaredContract = raw.declaredContract;
92
+ }
93
+ if (raw.bodyAccessors !== void 0 && raw.bodyAccessors.length > 0) {
94
+ http.bodyAccessors = raw.bodyAccessors;
95
+ }
96
+ if (raw.statusAccessors !== void 0 && raw.statusAccessors.length > 0) {
97
+ http.statusAccessors = raw.statusAccessors;
98
+ }
99
+ return Object.keys(http).length > 0 ? http : null;
100
+ }
101
+ function buildGraphqlMetadata(raw) {
102
+ const graphql = {};
103
+ if (raw.graphqlDocument !== void 0) {
104
+ graphql.document = raw.graphqlDocument;
105
+ }
106
+ if (raw.graphqlSchemaSdl !== void 0) {
107
+ graphql.schemaSdl = raw.graphqlSchemaSdl;
108
+ }
109
+ if (raw.graphqlDeclaredContract !== void 0) {
110
+ graphql.declaredContract = raw.graphqlDeclaredContract;
111
+ }
112
+ if (raw.graphqlUnresolvedDocument !== void 0) {
113
+ graphql.unresolvedDocument = raw.graphqlUnresolvedDocument;
114
+ }
115
+ return Object.keys(graphql).length > 0 ? graphql : null;
116
+ }
117
+ function detectGaps(raw, transitions, options) {
118
+ if (options.gapHandling === "silent") {
119
+ return [];
120
+ }
121
+ const gaps = [];
122
+ if (raw.declaredContract) {
123
+ const producedStatuses = new Set(
124
+ transitions.flatMap((t) => {
125
+ if (t.output.type !== "response") {
126
+ return [];
127
+ }
128
+ const sc = t.output.statusCode;
129
+ if (sc?.type === "literal") {
130
+ return [sc.value];
131
+ }
132
+ return [];
133
+ })
134
+ );
135
+ const declaredStatuses = new Set(
136
+ raw.declaredContract.responses.map((r) => r.statusCode)
137
+ );
138
+ for (const declared of declaredStatuses) {
139
+ if (!producedStatuses.has(declared)) {
140
+ gaps.push({
141
+ type: "unhandledCase",
142
+ conditions: [],
143
+ consequence: "frameworkDefault",
144
+ description: `Declared response ${declared} is never produced by the handler`
145
+ });
146
+ }
147
+ }
148
+ for (const produced of producedStatuses) {
149
+ if (!declaredStatuses.has(produced)) {
150
+ gaps.push({
151
+ type: "unhandledCase",
152
+ conditions: [],
153
+ consequence: "unknown",
154
+ description: `Handler produces status ${produced} which is not declared in the ${raw.declaredContract.framework} contract`
155
+ });
156
+ }
157
+ }
158
+ }
159
+ return gaps;
160
+ }
161
+ function assessConfidence(raw) {
162
+ let total = 0;
163
+ let opaque = 0;
164
+ for (const branch of raw.branches) {
165
+ for (const condition of branch.conditions) {
166
+ total++;
167
+ if (!condition.structured || condition.structured.type === "opaque") {
168
+ opaque++;
169
+ }
170
+ }
171
+ }
172
+ const ratio = total === 0 ? 0 : opaque / total;
173
+ const level = ratio === 0 ? "high" : ratio < 0.5 ? "medium" : "low";
174
+ return { source: "inferred_static", level };
175
+ }
176
+ function bodyToShape(body) {
177
+ if (!body) {
178
+ return null;
179
+ }
180
+ if (body.shape !== null) {
181
+ return body.shape;
182
+ }
183
+ if (body.typeText) {
184
+ return { type: "ref", name: body.typeText };
185
+ }
186
+ return null;
187
+ }
188
+ var terminalConverters = {
189
+ response: (t) => {
190
+ const statusCode = t.statusCode ? t.statusCode.type === "literal" ? { type: "literal", value: t.statusCode.value } : { type: "unresolved", sourceText: t.statusCode.sourceText } : null;
191
+ const body = bodyToShape(t.body);
192
+ return { type: "response", statusCode, body, headers: {} };
193
+ },
194
+ throw: (t) => {
195
+ if (t.statusCode) {
196
+ const statusCode = t.statusCode.type === "literal" ? { type: "literal", value: t.statusCode.value } : { type: "unresolved", sourceText: t.statusCode.sourceText };
197
+ return {
198
+ type: "response",
199
+ statusCode,
200
+ body: bodyToShape(t.body),
201
+ headers: {}
202
+ };
203
+ }
204
+ return {
205
+ type: "throw",
206
+ exceptionType: t.exceptionType,
207
+ message: t.message
208
+ };
209
+ },
210
+ render: (t) => ({
211
+ type: "render",
212
+ component: t.component ?? "unknown",
213
+ ...t.renderTree !== null ? { root: t.renderTree } : {}
214
+ }),
215
+ delegate: (t) => ({
216
+ type: "delegate",
217
+ to: t.delegateTarget ?? "unknown"
218
+ }),
219
+ emit: (t) => ({
220
+ type: "emit",
221
+ event: t.emitEvent ?? "unknown"
222
+ }),
223
+ return: (t) => ({
224
+ type: "return",
225
+ value: bodyToShape(t.body)
226
+ }),
227
+ void: (_t) => ({ type: "void" })
228
+ };
229
+ function terminalToOutput(terminal) {
230
+ return terminalConverters[terminal.kind](terminal);
231
+ }
232
+ function rawConditionToPredicate(c) {
233
+ const pred = c.structured !== null ? c.structured : {
234
+ type: "opaque",
235
+ sourceText: c.sourceText,
236
+ reason: "complexExpression"
237
+ };
238
+ return c.polarity === "negative" ? { type: "negation", operand: pred } : pred;
239
+ }
240
+ var effectConverters = {
241
+ mutation: (e) => ({
242
+ type: "mutation",
243
+ target: e.target,
244
+ operation: e.operation
245
+ }),
246
+ invocation: (e) => {
247
+ const invocation = {
248
+ type: "invocation",
249
+ callee: e.callee,
250
+ args: e.args,
251
+ async: e.async
252
+ };
253
+ if (e.preconditions !== void 0 && e.preconditions.length > 0) {
254
+ invocation.preconditions = e.preconditions.map(rawConditionToPredicate);
255
+ }
256
+ return invocation;
257
+ },
258
+ emission: (e) => ({ type: "emission", event: e.event }),
259
+ stateChange: (e) => ({ type: "stateChange", variable: e.variable })
260
+ };
261
+ function effectToIR(effect) {
262
+ return effectConverters[effect.type](effect);
263
+ }
264
+ function paramToInput(param) {
265
+ return {
266
+ type: "parameter",
267
+ name: param.name,
268
+ position: param.position,
269
+ role: param.role,
270
+ shape: param.typeText ? { type: "ref", name: param.typeText } : null
271
+ };
272
+ }
273
+ export {
274
+ assembleSummary,
275
+ assessConfidence,
276
+ detectGaps,
277
+ effectToIR,
278
+ httpRouteDiscovery,
279
+ makeTransitionId,
280
+ paramToInput,
281
+ terminalToOutput
282
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@suss/extractor",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Framework-agnostic extraction pipeline that turns raw extractions into suss behavioral summaries.",
6
+ "keywords": [
7
+ "suss",
8
+ "behavioral-summary",
9
+ "static-analysis",
10
+ "extraction",
11
+ "typescript"
12
+ ],
13
+ "license": "Apache-2.0",
14
+ "author": "Nimbus Cloud AI LLC",
15
+ "homepage": "https://github.com/nimbuscloud-ai/suss/tree/main/packages/extractor#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/nimbuscloud-ai/suss.git",
19
+ "directory": "packages/extractor"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/nimbuscloud-ai/suss/issues"
23
+ },
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "test": "vitest run",
42
+ "test:coverage": "vitest run --coverage",
43
+ "lint": "biome check .",
44
+ "lint:fix": "biome check --write .",
45
+ "typecheck": "tsc --noEmit"
46
+ },
47
+ "dependencies": {
48
+ "@suss/behavioral-ir": "0.0.1"
49
+ },
50
+ "devDependencies": {
51
+ "tsup": "*",
52
+ "typescript": "*",
53
+ "vitest": "*"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }