@principal-ai/principal-view-react 0.16.49 → 0.16.50

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.
Files changed (44) hide show
  1. package/dist/graphify/anchor.d.ts +28 -0
  2. package/dist/graphify/anchor.d.ts.map +1 -0
  3. package/dist/graphify/anchor.js +132 -0
  4. package/dist/graphify/anchor.js.map +1 -0
  5. package/dist/graphify/ids.d.ts +19 -0
  6. package/dist/graphify/ids.d.ts.map +1 -0
  7. package/dist/graphify/ids.js +46 -0
  8. package/dist/graphify/ids.js.map +1 -0
  9. package/dist/graphify/index.d.ts +7 -0
  10. package/dist/graphify/index.d.ts.map +1 -1
  11. package/dist/graphify/index.js +4 -0
  12. package/dist/graphify/index.js.map +1 -1
  13. package/dist/graphify/kind.d.ts +23 -0
  14. package/dist/graphify/kind.d.ts.map +1 -0
  15. package/dist/graphify/kind.js +71 -0
  16. package/dist/graphify/kind.js.map +1 -0
  17. package/dist/graphify/signature.d.ts +87 -0
  18. package/dist/graphify/signature.d.ts.map +1 -0
  19. package/dist/graphify/signature.js +276 -0
  20. package/dist/graphify/signature.js.map +1 -0
  21. package/dist/index.d.ts +4 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/subsystem/ComponentDeclaration.d.ts +58 -3
  26. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  27. package/dist/subsystem/ComponentDeclaration.js +168 -35
  28. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  29. package/dist/subsystem/SubsystemComponentGraph.d.ts +5 -0
  30. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  31. package/dist/subsystem/SubsystemComponentGraph.js +2 -2
  32. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/graphify/anchor.test.ts +124 -0
  35. package/src/graphify/anchor.ts +160 -0
  36. package/src/graphify/ids.ts +48 -0
  37. package/src/graphify/index.ts +26 -0
  38. package/src/graphify/kind.test.ts +108 -0
  39. package/src/graphify/kind.ts +114 -0
  40. package/src/graphify/signature.test.ts +278 -0
  41. package/src/graphify/signature.ts +354 -0
  42. package/src/index.ts +26 -0
  43. package/src/subsystem/ComponentDeclaration.tsx +287 -40
  44. package/src/subsystem/SubsystemComponentGraph.tsx +8 -1
@@ -0,0 +1,278 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { GraphifyEdge, GraphifyNode } from './types';
3
+ import {
4
+ compareSignatures,
5
+ extractGraphifySignature,
6
+ extractNamedTypes,
7
+ } from './signature';
8
+
9
+ function node(id: string, label: string): GraphifyNode {
10
+ return { id, label, file_type: 'code', source_file: 'a.ts' };
11
+ }
12
+
13
+ function ref(
14
+ source: string,
15
+ target: string,
16
+ context: 'parameter_type' | 'return_type' | 'inline_parameter',
17
+ ): GraphifyEdge {
18
+ return {
19
+ source,
20
+ target,
21
+ relation: 'references',
22
+ context,
23
+ confidence: 'EXTRACTED',
24
+ source_file: 'a.ts',
25
+ };
26
+ }
27
+
28
+ describe('extractNamedTypes', () => {
29
+ test('strips primitives and wrappers', () => {
30
+ expect(extractNamedTypes('Promise<BuiltSessionEvents>')).toEqual([
31
+ 'BuiltSessionEvents',
32
+ ]);
33
+ expect(extractNamedTypes('string')).toEqual([]);
34
+ expect(extractNamedTypes('{ tabId: string }')).toEqual([]);
35
+ });
36
+
37
+ test('splits unions and keeps named types', () => {
38
+ expect(extractNamedTypes('Foo | Bar | null')).toEqual(['Bar', 'Foo']);
39
+ });
40
+
41
+ test('drops inline objects but keeps sibling names', () => {
42
+ expect(
43
+ extractNamedTypes('Opts & { includeRaw?: boolean }'),
44
+ ).toEqual(['Opts']);
45
+ });
46
+ });
47
+
48
+ describe('extractGraphifySignature', () => {
49
+ test('collects parameter_type and return_type edges', () => {
50
+ const nodes = new Map([
51
+ ['fn', node('fn', 'build()')],
52
+ ['A', node('A', 'DataProcessor')],
53
+ ['R', node('R', 'Result')],
54
+ ]);
55
+ const edges = [
56
+ ref('fn', 'A', 'parameter_type'),
57
+ ref('fn', 'R', 'return_type'),
58
+ ];
59
+ const sig = extractGraphifySignature('fn', edges, nodes);
60
+ expect(sig.hasSignal).toBe(true);
61
+ expect(sig.parameters.map((p) => p.type)).toEqual(['DataProcessor']);
62
+ expect(sig.returnType).toBe('Result');
63
+ });
64
+
65
+ test('counts inline_parameter self-edges', () => {
66
+ const nodes = new Map([['fn', node('fn', 'build()')]]);
67
+ const sig = extractGraphifySignature(
68
+ 'fn',
69
+ [
70
+ ref('fn', 'fn', 'inline_parameter'),
71
+ ref('fn', 'fn', 'inline_parameter'),
72
+ ],
73
+ nodes,
74
+ );
75
+ expect(sig.inlineParameters).toBe(2);
76
+ expect(sig.hasSignal).toBe(false);
77
+ });
78
+ });
79
+
80
+ describe('compareSignatures', () => {
81
+ test('skips when graphify has no signature edges', () => {
82
+ const r = compareSignatures(
83
+ { parameters: [{ type: 'SessionReader' }], returnType: 'void' },
84
+ { parameters: [], hasSignal: false },
85
+ );
86
+ expect(r.skipped).toBe(true);
87
+ expect(r.match).toBe(true);
88
+ });
89
+
90
+ test('classifies skip: no claimable named types', () => {
91
+ const r = compareSignatures(
92
+ { parameters: [{ type: '{ tabId: string }' }] },
93
+ { parameters: [], hasSignal: false },
94
+ );
95
+ expect(r.skipped).toBe(true);
96
+ expect(r.skipCode).toBe('no_claimed_types');
97
+ });
98
+
99
+ test('classifies skip: claimed types only as generic_arg', () => {
100
+ const r = compareSignatures(
101
+ { returnType: 'Promise<BuiltSessionEvents>' },
102
+ {
103
+ parameters: [],
104
+ hasSignal: false,
105
+ genericArgs: [{ type: 'BuiltSessionEvents', nodeId: 'n' }],
106
+ },
107
+ );
108
+ expect(r.skipped).toBe(true);
109
+ expect(r.skipCode).toBe('generic_arg_only');
110
+ });
111
+
112
+ test('classifies skip: partial generic_arg coverage', () => {
113
+ const r = compareSignatures(
114
+ { parameters: [{ type: 'Foo' }], returnType: 'Bar' },
115
+ {
116
+ parameters: [],
117
+ hasSignal: false,
118
+ genericArgs: [{ type: 'Foo', nodeId: 'n' }],
119
+ },
120
+ );
121
+ expect(r.skipped).toBe(true);
122
+ expect(r.skipCode).toBe('partially_generic_arg');
123
+ });
124
+
125
+ test('classifies skip: claimed names have no edges at all', () => {
126
+ const r = compareSignatures(
127
+ { returnType: 'Element' },
128
+ { parameters: [], hasSignal: false },
129
+ );
130
+ expect(r.skipped).toBe(true);
131
+ expect(r.skipCode).toBe('unresolved_claimed_types');
132
+ });
133
+
134
+ test('matches named type bags', () => {
135
+ const inferred = {
136
+ hasSignal: true,
137
+ parameters: [
138
+ { type: 'DataProcessor', nodeId: 'a' },
139
+ { type: 'LintOptions', nodeId: 'b' },
140
+ ],
141
+ returnType: 'Result',
142
+ returnTypeNodeId: 'r',
143
+ };
144
+ const r = compareSignatures(
145
+ {
146
+ parameters: [
147
+ { type: 'DataProcessor' },
148
+ { type: 'Promise<LintOptions>' },
149
+ ],
150
+ returnType: 'Promise<Result>',
151
+ },
152
+ inferred,
153
+ );
154
+ expect(r.match).toBe(true);
155
+ expect(r.skipped).toBe(false);
156
+ });
157
+
158
+ test('hard-fails on parameter bag mismatch', () => {
159
+ const r = compareSignatures(
160
+ { parameters: [{ type: 'Foo' }], returnType: 'Result' },
161
+ {
162
+ hasSignal: true,
163
+ parameters: [{ type: 'Bar', nodeId: 'b' }],
164
+ returnType: 'Result',
165
+ },
166
+ );
167
+ expect(r.match).toBe(false);
168
+ expect(r.reason).toBe('parameter_types_mismatch');
169
+ });
170
+
171
+ test('empty claimed vs graphify types fails', () => {
172
+ const r = compareSignatures(
173
+ { parameters: [] },
174
+ {
175
+ hasSignal: true,
176
+ parameters: [{ type: 'DataProcessor', nodeId: 'a' }],
177
+ returnType: 'Result',
178
+ },
179
+ );
180
+ expect(r.match).toBe(false);
181
+ });
182
+
183
+ test('anon param claim + marker parity: params verified, unresolved return skips', () => {
184
+ // buildAgentSessionsView shape: 1 anonymous `opts` (marker present),
185
+ // named return type only in the claim (npm type, no graph edge).
186
+ const r = compareSignatures(
187
+ {
188
+ parameters: [
189
+ {
190
+ type: '{ sessionId: string; events: SessionEventRow[] | null }',
191
+ },
192
+ ],
193
+ returnType: 'AgentSessionsView',
194
+ },
195
+ {
196
+ parameters: [],
197
+ hasSignal: false,
198
+ inlineParameters: 1,
199
+ genericArgs: [],
200
+ },
201
+ );
202
+ expect(r.skipped).toBe(true);
203
+ expect(r.match).toBe(true);
204
+ expect(r.skipCode).toBe('unresolved_claimed_types');
205
+ expect(r.reason).toMatch(/params verified/);
206
+ });
207
+
208
+ test('anon params verified + wrapper-indirected return skips as generic_arg', () => {
209
+ // processSessionEvents shape: 1 anonymous opts + Promise-wrapped return.
210
+ const r = compareSignatures(
211
+ {
212
+ parameters: [
213
+ {
214
+ type: '{ sessionId: string; events: SessionEventRow[] | null }',
215
+ },
216
+ ],
217
+ returnType: 'Promise<BuiltSessionEvents>',
218
+ },
219
+ {
220
+ parameters: [],
221
+ hasSignal: false,
222
+ inlineParameters: 1,
223
+ genericArgs: [{ type: 'BuiltSessionEvents', nodeId: 'n' }],
224
+ },
225
+ );
226
+ expect(r.skipped).toBe(true);
227
+ expect(r.match).toBe(true);
228
+ expect(r.skipCode).toBe('generic_arg_only');
229
+ expect(r.reason).toMatch(/params verified/);
230
+ });
231
+
232
+ test('params verified but mixed generic+unresolved return stays a hard mismatch', () => {
233
+ const r = compareSignatures(
234
+ {
235
+ parameters: [{ type: '{ a: string }' }],
236
+ returnType: 'Promise<Foo> | Bar',
237
+ },
238
+ {
239
+ parameters: [],
240
+ hasSignal: false,
241
+ inlineParameters: 1,
242
+ genericArgs: [{ type: 'Foo', nodeId: 'n' }],
243
+ },
244
+ );
245
+ expect(r.skipped).toBe(false);
246
+ expect(r.match).toBe(false);
247
+ expect(r.reason).toBe('return_type_mismatch');
248
+ });
249
+
250
+ test('anon param claim with marker and no return claim: matches', () => {
251
+ // analyzeSessionInBackground shape: inline opts only, no return type.
252
+ const r = compareSignatures(
253
+ { parameters: [{ type: '{ sessionId: string; title: string }' }] },
254
+ { parameters: [], hasSignal: false, inlineParameters: 1 },
255
+ );
256
+ expect(r.skipped).toBe(false);
257
+ expect(r.match).toBe(true);
258
+ });
259
+
260
+ test('graph marker without matching claim is a params mismatch', () => {
261
+ const r = compareSignatures(
262
+ { parameters: [] },
263
+ { parameters: [], hasSignal: false, inlineParameters: 1 },
264
+ );
265
+ expect(r.skipped).toBe(false);
266
+ expect(r.match).toBe(false);
267
+ expect(r.reason).toBe('parameter_types_mismatch');
268
+ });
269
+
270
+ test('anon claim with no marker stays skipped (default graph, no signal)', () => {
271
+ const r = compareSignatures(
272
+ { parameters: [{ type: '{ tabId: string }' }] },
273
+ { parameters: [], hasSignal: false },
274
+ );
275
+ expect(r.skipped).toBe(true);
276
+ expect(r.skipCode).toBe('no_claimed_types');
277
+ });
278
+ });
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Signature / params verification against graphify `references` edges.
3
+ *
4
+ * Graphify does not store arity or parameter names — only `parameter_type` /
5
+ * `return_type` edges to type nodes (unions become multiple edges). Comparison
6
+ * is therefore **named type bags**, not positional arity.
7
+ *
8
+ * Many TS/JS functions have no such edges yet. When graphify has no signature
9
+ * signal, the check is skipped (not a hard fail) so Verify can still gate on
10
+ * kind. When graphify *does* emit types, claimed detail must match.
11
+ */
12
+
13
+ import type { GraphifyEdge, GraphifyNode } from './types';
14
+
15
+ /** Inferred signature from graphify edges (no param names). */
16
+ export interface GraphifyInferredSignature {
17
+ parameters: Array<{ type: string; nodeId: string }>;
18
+ returnType?: string;
19
+ returnTypeNodeId?: string;
20
+ /**
21
+ * Types reachable only via `generic_arg` edges (Promise<X>, Array<X>,
22
+ * Omit<X,…>, Map<…>). Kept out of `parameters`/`returnType` — not read
23
+ * by the bag comparison — so skipped cases can be told apart.
24
+ */
25
+ genericArgs?: Array<{ type: string; nodeId: string }>;
26
+ /**
27
+ * Count of `inline_parameter` self-edges (anonymous object-literal params).
28
+ * Graphify emits one per anon param only with its `--inline-params` flag.
29
+ */
30
+ inlineParameters?: number;
31
+ /** True when at least one parameter_type or return_type edge exists. */
32
+ hasSignal: boolean;
33
+ }
34
+
35
+ export interface ClaimedSignature {
36
+ parameters?: Array<{ name?: string; type: string }>;
37
+ returnType?: string;
38
+ }
39
+
40
+ /**
41
+ * Granular skip classification. `signature_skipped` means "graphify has no
42
+ * parameter_type/return_type edges to compare", but *why* matters for future
43
+ * work:
44
+ * - `no_claimed_types`: authored detail has no named types (primitives/inline).
45
+ * - `generic_arg_only`: claimed types resolve, but only as `generic_arg`
46
+ * (Promise/Omit/Map/Array wrappers) — this extractor does not read them.
47
+ * - `partially_generic_arg`: some, not all, claimed types resolve as generic_arg.
48
+ * - `unresolved_claimed_types`: claimed names have no edges at all
49
+ * (global/npm types, or a dropped same-file label collision).
50
+ */
51
+ export type SignatureSkipCode =
52
+ | 'no_claimed_types'
53
+ | 'generic_arg_only'
54
+ | 'partially_generic_arg'
55
+ | 'unresolved_claimed_types';
56
+
57
+ export interface SignatureCompareResult {
58
+ match: boolean;
59
+ /** Skipped because graphify had no parameter/return type edges. */
60
+ skipped: boolean;
61
+ reason?: string;
62
+ /** Granular skip classification when `skipped` is true. */
63
+ skipCode?: SignatureSkipCode;
64
+ claimed: { parameterTypes: string[]; returnTypes: string[] };
65
+ inferred: { parameterTypes: string[]; returnTypes: string[] };
66
+ }
67
+
68
+ const PRIMITIVES = new Set([
69
+ 'string',
70
+ 'number',
71
+ 'boolean',
72
+ 'void',
73
+ 'null',
74
+ 'undefined',
75
+ 'any',
76
+ 'unknown',
77
+ 'never',
78
+ 'object',
79
+ 'symbol',
80
+ 'bigint',
81
+ 'true',
82
+ 'false',
83
+ 'this',
84
+ ]);
85
+
86
+ /** Common generic wrappers — keep inner type names, drop the wrapper. */
87
+ const TYPE_WRAPPERS = new Set([
88
+ 'Promise',
89
+ 'Array',
90
+ 'ReadonlyArray',
91
+ 'Map',
92
+ 'Set',
93
+ 'WeakMap',
94
+ 'WeakSet',
95
+ 'Record',
96
+ 'Partial',
97
+ 'Required',
98
+ 'Readonly',
99
+ 'Awaited',
100
+ 'NonNullable',
101
+ 'ReturnType',
102
+ 'Parameters',
103
+ 'InstanceType',
104
+ 'Pick',
105
+ 'Omit',
106
+ 'Exclude',
107
+ 'Extract',
108
+ ]);
109
+
110
+ /**
111
+ * Pull comparable named type identifiers out of an authored / graphify type
112
+ * string. Skips primitives, inline object types, and known wrappers.
113
+ */
114
+ export function extractNamedTypes(typeStr: string): string[] {
115
+ if (!typeStr || !typeStr.trim()) return [];
116
+ let s = typeStr;
117
+ // Drop string/template literals so their contents aren't tokens.
118
+ s = s.replace(/`(?:\\.|[^`\\])*`/g, ' ');
119
+ s = s.replace(/'(?:\\.|[^'\\])*'/g, ' ');
120
+ s = s.replace(/"(?:\\.|[^"\\])*"/g, ' ');
121
+ // Drop inline object / mapped types (non-greedy, repeated for nesting).
122
+ for (let i = 0; i < 8; i++) {
123
+ const next = s.replace(/\{[^{}]*\}/g, ' ');
124
+ if (next === s) break;
125
+ s = next;
126
+ }
127
+ const found = new Set<string>();
128
+ const re = /[A-Za-z_][A-Za-z0-9_.]*/g;
129
+ let m: RegExpExecArray | null;
130
+ while ((m = re.exec(s)) !== null) {
131
+ const raw = m[0]!;
132
+ const base = raw.includes('.') ? (raw.split('.').pop() ?? raw) : raw;
133
+ if (PRIMITIVES.has(base)) continue;
134
+ if (TYPE_WRAPPERS.has(base)) continue;
135
+ // Skip lone lowercase keywords that aren't types we care about.
136
+ if (base === 'typeof' || base === 'keyof' || base === 'infer' || base === 'extends') {
137
+ continue;
138
+ }
139
+ found.add(base);
140
+ }
141
+ return [...found].sort();
142
+ }
143
+
144
+ function labelOf(
145
+ nodeId: string,
146
+ nodesById: ReadonlyMap<string, GraphifyNode>,
147
+ ): string {
148
+ const n = nodesById.get(nodeId);
149
+ const label = n && typeof n.label === 'string' ? n.label.trim() : '';
150
+ return label || nodeId;
151
+ }
152
+
153
+ /** Collect parameter_type / return_type edges for a callable node. */
154
+ export function extractGraphifySignature(
155
+ nodeId: string,
156
+ edges: readonly GraphifyEdge[],
157
+ nodesById: ReadonlyMap<string, GraphifyNode>,
158
+ ): GraphifyInferredSignature {
159
+ const id = String(nodeId);
160
+ const parameters: GraphifyInferredSignature['parameters'] = [];
161
+ const genericArgs: GraphifyInferredSignature['genericArgs'] = [];
162
+ let returnType: string | undefined;
163
+ let returnTypeNodeId: string | undefined;
164
+ let inlineParameters = 0;
165
+
166
+ for (const e of edges) {
167
+ if (String(e.source) !== id || e.relation !== 'references') continue;
168
+ const ctx = e.context;
169
+ const target = String(e.target);
170
+ if (ctx === 'parameter_type') {
171
+ parameters.push({ type: labelOf(target, nodesById), nodeId: target });
172
+ } else if (ctx === 'return_type' && returnType === undefined) {
173
+ returnType = labelOf(target, nodesById);
174
+ returnTypeNodeId = target;
175
+ } else if (ctx === 'generic_arg') {
176
+ genericArgs.push({ type: labelOf(target, nodesById), nodeId: target });
177
+ } else if (ctx === 'inline_parameter') {
178
+ inlineParameters += 1;
179
+ }
180
+ }
181
+
182
+ return {
183
+ parameters,
184
+ genericArgs,
185
+ returnType,
186
+ returnTypeNodeId,
187
+ inlineParameters,
188
+ hasSignal: parameters.length > 0 || returnType !== undefined,
189
+ };
190
+ }
191
+
192
+ function bagsEqual(a: string[], b: string[]): boolean {
193
+ if (a.length !== b.length) return false;
194
+ for (let i = 0; i < a.length; i++) {
195
+ if (a[i] !== b[i]) return false;
196
+ }
197
+ return true;
198
+ }
199
+
200
+ function claimedBags(claimed: ClaimedSignature | undefined): {
201
+ parameterTypes: string[];
202
+ returnTypes: string[];
203
+ } {
204
+ const parameterTypes = new Set<string>();
205
+ for (const p of claimed?.parameters ?? []) {
206
+ for (const t of extractNamedTypes(p.type)) parameterTypes.add(t);
207
+ }
208
+ const returnTypes = new Set<string>();
209
+ if (claimed?.returnType) {
210
+ for (const t of extractNamedTypes(claimed.returnType)) returnTypes.add(t);
211
+ }
212
+ return {
213
+ parameterTypes: [...parameterTypes].sort(),
214
+ returnTypes: [...returnTypes].sort(),
215
+ };
216
+ }
217
+
218
+ function inferredBags(inferred: GraphifyInferredSignature): {
219
+ parameterTypes: string[];
220
+ returnTypes: string[];
221
+ } {
222
+ const parameterTypes = new Set<string>();
223
+ for (const p of inferred.parameters) {
224
+ for (const t of extractNamedTypes(p.type)) parameterTypes.add(t);
225
+ }
226
+ const returnTypes = new Set<string>();
227
+ if (inferred.returnType) {
228
+ for (const t of extractNamedTypes(inferred.returnType)) returnTypes.add(t);
229
+ }
230
+ return {
231
+ parameterTypes: [...parameterTypes].sort(),
232
+ returnTypes: [...returnTypes].sort(),
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Compare authored function/method detail signature to graphify edges.
238
+ *
239
+ * - No graphify signal → skipped (match=true).
240
+ * - Otherwise named-type bags for params and return must match exactly.
241
+ */
242
+ export function compareSignatures(
243
+ claimed: ClaimedSignature | undefined,
244
+ inferred: GraphifyInferredSignature,
245
+ ): SignatureCompareResult {
246
+ const claimedBagsResult = claimedBags(claimed);
247
+ const inferredBagsResult = inferredBags(inferred);
248
+
249
+ // Anonymous object-literal params ("inline") carry no named types, so they
250
+ // only ever surface via graphify's `inline_parameter` markers. A claim of an
251
+ // inline param against a graph with no marker proves nothing (the store
252
+ // graph may predate the flag), so absence alone keeps the skip path. When
253
+ // the graph DOES emit a marker, anon-count parity is compared like any
254
+ // parameter signal.
255
+ const claimedAnonCount = (claimed?.parameters ?? []).filter((p) =>
256
+ (p.type ?? '').trim().startsWith('{'),
257
+ ).length;
258
+ const inferredAnonCount = inferred.inlineParameters ?? 0;
259
+ const hasAnonSignal = inferredAnonCount > 0;
260
+
261
+ if (!inferred.hasSignal && !hasAnonSignal) {
262
+ const claimedTypes = new Set<string>([
263
+ ...claimedBagsResult.parameterTypes,
264
+ ...claimedBagsResult.returnTypes,
265
+ ]);
266
+ const genericArgTypes = new Set(
267
+ (inferred.genericArgs ?? []).map((a) => a.type),
268
+ );
269
+ const sorted = [...claimedTypes].sort();
270
+ let skipCode: SignatureSkipCode;
271
+ let reason: string;
272
+ if (sorted.length === 0) {
273
+ skipCode = 'no_claimed_types';
274
+ reason = 'no claimable named types in authored signature (primitives/inline only)';
275
+ } else {
276
+ const found = sorted.filter((t) => genericArgTypes.has(t));
277
+ if (found.length === sorted.length) {
278
+ skipCode = 'generic_arg_only';
279
+ reason = `claimed types ([${sorted.join(', ')}]) present only as generic_arg (wrapper-indirected: Promise/Omit/Map/Array) — extractor does not read generic_arg`;
280
+ } else if (found.length > 0) {
281
+ skipCode = 'partially_generic_arg';
282
+ reason = `claimed types resolve only via generic_arg for ${found.join(', ')}; rest unresolved (${sorted.filter((t) => !genericArgTypes.has(t)).join(', ')})`;
283
+ } else {
284
+ skipCode = 'unresolved_claimed_types';
285
+ reason = `claimed types ([${sorted.join(', ')}]) have no parameter_type/return_type/generic_arg edges (global/npm types or dropped same-file label collision)`;
286
+ }
287
+ }
288
+ return {
289
+ match: true,
290
+ skipped: true,
291
+ reason,
292
+ skipCode,
293
+ claimed: claimedBagsResult,
294
+ inferred: inferredBagsResult,
295
+ };
296
+ }
297
+
298
+ const paramsOk = bagsEqual(
299
+ claimedBagsResult.parameterTypes,
300
+ inferredBagsResult.parameterTypes,
301
+ );
302
+ const anonParamsOk = claimedAnonCount === inferredAnonCount;
303
+ const returnOk = bagsEqual(
304
+ claimedBagsResult.returnTypes,
305
+ inferredBagsResult.returnTypes,
306
+ );
307
+ const match = paramsOk && anonParamsOk && returnOk;
308
+ if (paramsOk && anonParamsOk && !returnOk) {
309
+ // Params verified, but the claimed return types can't be backed. When
310
+ // every claimed return type is either wrapper-indirected (generic_arg:
311
+ // Promise<X>, Omit<X,…>) or entirely unresolved (global/npm/DOM types),
312
+ // that's a coverage gap, not a lie — mark the check skipped instead of
313
+ // hard-failing, mirroring the pre-signal skip classification. A mixed
314
+ // bag (some generic, some unresolved) stays a hard mismatch.
315
+ const claimReturn = claimedBagsResult.returnTypes;
316
+ if (claimReturn.length > 0) {
317
+ const genericArgTypes = new Set(
318
+ (inferred.genericArgs ?? []).map((a) => a.type),
319
+ );
320
+ const gen = claimReturn.filter((t) => genericArgTypes.has(t));
321
+ const unresolved = claimReturn.filter((t) => !genericArgTypes.has(t));
322
+ let skipCode: SignatureSkipCode | undefined;
323
+ let reason: string | undefined;
324
+ if (gen.length === claimReturn.length) {
325
+ skipCode = 'generic_arg_only';
326
+ reason = `claimed return types ([${claimReturn.join(', ')}]) present only as generic_arg (wrapper-indirected: Promise/Omit/Map/Array) — extractor does not read generic_arg; params verified`;
327
+ } else if (unresolved.length === claimReturn.length) {
328
+ skipCode = 'unresolved_claimed_types';
329
+ reason = `claimed return types ([${claimReturn.join(', ')}]) have no parameter_type/return_type/generic_arg edges (global/npm types or dropped same-file label collision); params verified`;
330
+ }
331
+ if (skipCode) {
332
+ return {
333
+ match: true,
334
+ skipped: true,
335
+ reason,
336
+ skipCode,
337
+ claimed: claimedBagsResult,
338
+ inferred: inferredBagsResult,
339
+ };
340
+ }
341
+ }
342
+ }
343
+ return {
344
+ match,
345
+ skipped: false,
346
+ reason: match
347
+ ? undefined
348
+ : !paramsOk || !anonParamsOk
349
+ ? 'parameter_types_mismatch'
350
+ : 'return_type_mismatch',
351
+ claimed: claimedBagsResult,
352
+ inferred: inferredBagsResult,
353
+ };
354
+ }
package/src/index.ts CHANGED
@@ -250,11 +250,37 @@ export {
250
250
  normalizeGraphifyLabel,
251
251
  createGraphifyTypeResolver,
252
252
  resolveGraphifyTypeRef,
253
+ normalizeGraphifyId,
254
+ makeGraphifyId,
255
+ graphifyFileStem,
256
+ normalizeSourcePath,
257
+ resolveComponentAnchor,
258
+ symbolLabelVariants,
259
+ inferGraphifyKind,
260
+ kindsMatch,
261
+ extractNamedTypes,
262
+ extractGraphifySignature,
263
+ compareSignatures,
264
+ } from './graphify';
265
+ export type {
266
+ ComponentAnchorInput,
267
+ ComponentAnchorResult,
268
+ InferredGraphifyKind,
269
+ InferGraphifyKindResult,
270
+ GraphifyInferredSignature,
271
+ ClaimedSignature,
272
+ SignatureCompareResult,
253
273
  } from './graphify';
254
274
 
255
275
  // Subsystem component graph
256
276
  export { SubsystemComponentGraph } from './subsystem/SubsystemComponentGraph';
257
277
  export type { SubsystemComponentGraphProps } from './subsystem/SubsystemComponentGraph';
278
+ export type {
279
+ ComponentVerificationState,
280
+ ComponentVerificationPhase,
281
+ ComponentDeclarationProps,
282
+ } from './subsystem/ComponentDeclaration';
283
+ export { ComponentDeclaration } from './subsystem/ComponentDeclaration';
258
284
  export { MECHANISM_COLOR, KIND_COLOR, MECHANISM_STYLE } from './subsystem/model';
259
285
  export {
260
286
  purlRepoKey,