@llui/vite-plugin 0.0.31 → 0.0.33
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/README.md +8 -9
- package/dist/binding-descriptors.d.ts +98 -16
- package/dist/binding-descriptors.d.ts.map +1 -1
- package/dist/binding-descriptors.js +319 -61
- package/dist/binding-descriptors.js.map +1 -1
- package/dist/cross-file-resolver.d.ts +109 -0
- package/dist/cross-file-resolver.d.ts.map +1 -0
- package/dist/cross-file-resolver.js +457 -0
- package/dist/cross-file-resolver.js.map +1 -0
- package/dist/index.d.ts +0 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +241 -33
- package/dist/index.js.map +1 -1
- package/dist/msg-annotations.d.ts +47 -5
- package/dist/msg-annotations.d.ts.map +1 -1
- package/dist/msg-annotations.js +85 -9
- package/dist/msg-annotations.js.map +1 -1
- package/dist/msg-schema.d.ts +81 -5
- package/dist/msg-schema.d.ts.map +1 -1
- package/dist/msg-schema.js +201 -13
- package/dist/msg-schema.js.map +1 -1
- package/dist/transform.d.ts +47 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +155 -61
- package/dist/transform.js.map +1 -1
- package/package.json +1 -1
- package/dist/diagnostics.d.ts +0 -14
- package/dist/diagnostics.js +0 -846
- package/dist/diagnostics.js.map +0 -1
|
@@ -1,8 +1,39 @@
|
|
|
1
|
+
export type DispatchMode = 'shared' | 'human-only' | 'agent-only';
|
|
1
2
|
export type MessageAnnotations = {
|
|
2
3
|
intent: string | null;
|
|
3
4
|
alwaysAffordable: boolean;
|
|
4
5
|
requiresConfirm: boolean;
|
|
5
|
-
|
|
6
|
+
dispatchMode: DispatchMode;
|
|
7
|
+
/**
|
|
8
|
+
* Concrete example dispatches the LLM can copy from. Populated by
|
|
9
|
+
* `@example("text")` JSDoc tags. Each tag becomes one entry, in
|
|
10
|
+
* source order, so authors can mix scenarios ("typical case",
|
|
11
|
+
* "edge case with auth", etc.) without nesting them in a single
|
|
12
|
+
* string.
|
|
13
|
+
*/
|
|
14
|
+
examples: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Non-blocking caution. Surfaced verbatim to the agent at affordance
|
|
17
|
+
* time so the LLM can weigh the consequence ("this overwrites the
|
|
18
|
+
* cloud version", "fires analytics that can't be retracted") before
|
|
19
|
+
* dispatching. Distinct from `requiresConfirm`, which is a runtime
|
|
20
|
+
* gate the user must acknowledge.
|
|
21
|
+
*/
|
|
22
|
+
warning: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Effect kinds this variant emits when dispatched, declared by the
|
|
25
|
+
* author via `@emits("kind1", "kind2")`. Lets the agent reason
|
|
26
|
+
* about side effects ("this dispatch hits the cloud, so I should
|
|
27
|
+
* batch") without the compiler having to walk update.ts. Authored
|
|
28
|
+
* rather than auto-extracted because real apps emit effects
|
|
29
|
+
* through helpers (`track('foo')`, `saveDelta(d)`) — auto-detecting
|
|
30
|
+
* those would require helper-return-shape analysis with
|
|
31
|
+
* ergonomically-painful failure modes; the declarative form trades
|
|
32
|
+
* automatic discovery for accuracy and simplicity.
|
|
33
|
+
*
|
|
34
|
+
* Empty when no `@emits` tag is present.
|
|
35
|
+
*/
|
|
36
|
+
emits: string[];
|
|
6
37
|
};
|
|
7
38
|
/**
|
|
8
39
|
* Walk a Msg-like discriminated-union type alias and extract JSDoc
|
|
@@ -13,11 +44,22 @@ export type MessageAnnotations = {
|
|
|
13
44
|
* @intent("human readable")
|
|
14
45
|
* @alwaysAffordable
|
|
15
46
|
* @requiresConfirm
|
|
16
|
-
* @humanOnly
|
|
47
|
+
* @humanOnly — sugar for dispatchMode: 'human-only'
|
|
48
|
+
* @agentOnly — sugar for dispatchMode: 'agent-only'
|
|
17
49
|
*
|
|
18
50
|
* Unknown tags are ignored; malformed @intent (no quoted string) is
|
|
19
|
-
* treated as "no intent".
|
|
20
|
-
*
|
|
51
|
+
* treated as "no intent". `@humanOnly` and `@agentOnly` are mutually
|
|
52
|
+
* exclusive — if both are present (which the ESLint rule
|
|
53
|
+
* `agent-exclusive-annotations` reports as an error), the parser
|
|
54
|
+
* falls back to `'shared'` so a misconfigured Msg variant doesn't
|
|
55
|
+
* silently lock out one audience.
|
|
21
56
|
*/
|
|
22
|
-
export declare function extractMsgAnnotations(source: string
|
|
57
|
+
export declare function extractMsgAnnotations(source: string,
|
|
58
|
+
/**
|
|
59
|
+
* Name of the type alias to extract from. Defaults to `'Msg'` for
|
|
60
|
+
* convention. Passed by the cross-file resolver when the alias has
|
|
61
|
+
* been renamed through imports/re-exports — its local name in the
|
|
62
|
+
* declaring file may differ from `'Msg'`.
|
|
63
|
+
*/
|
|
64
|
+
typeName?: string): Record<string, MessageAnnotations> | null;
|
|
23
65
|
//# sourceMappingURL=msg-annotations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"msg-annotations.d.ts","sourceRoot":"","sources":["../src/msg-annotations.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,gBAAgB,EAAE,OAAO,CAAA;IACzB,eAAe,EAAE,OAAO,CAAA;IACxB,
|
|
1
|
+
{"version":3,"file":"msg-annotations.d.ts","sourceRoot":"","sources":["../src/msg-annotations.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,YAAY,CAAA;AAEjE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,gBAAgB,EAAE,OAAO,CAAA;IACzB,eAAe,EAAE,OAAO,CAAA;IACxB,YAAY,EAAE,YAAY,CAAA;IAC1B;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB;;;;;;;;;;;;OAYG;IACH,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAYD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM;AACd;;;;;GAKG;AACH,QAAQ,GAAE,MAAc,GACvB,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAgC3C"}
|
package/dist/msg-annotations.js
CHANGED
|
@@ -3,7 +3,10 @@ const DEFAULT = {
|
|
|
3
3
|
intent: null,
|
|
4
4
|
alwaysAffordable: false,
|
|
5
5
|
requiresConfirm: false,
|
|
6
|
-
|
|
6
|
+
dispatchMode: 'shared',
|
|
7
|
+
examples: [],
|
|
8
|
+
warning: null,
|
|
9
|
+
emits: [],
|
|
7
10
|
};
|
|
8
11
|
/**
|
|
9
12
|
* Walk a Msg-like discriminated-union type alias and extract JSDoc
|
|
@@ -14,21 +17,36 @@ const DEFAULT = {
|
|
|
14
17
|
* @intent("human readable")
|
|
15
18
|
* @alwaysAffordable
|
|
16
19
|
* @requiresConfirm
|
|
17
|
-
* @humanOnly
|
|
20
|
+
* @humanOnly — sugar for dispatchMode: 'human-only'
|
|
21
|
+
* @agentOnly — sugar for dispatchMode: 'agent-only'
|
|
18
22
|
*
|
|
19
23
|
* Unknown tags are ignored; malformed @intent (no quoted string) is
|
|
20
|
-
* treated as "no intent".
|
|
21
|
-
*
|
|
24
|
+
* treated as "no intent". `@humanOnly` and `@agentOnly` are mutually
|
|
25
|
+
* exclusive — if both are present (which the ESLint rule
|
|
26
|
+
* `agent-exclusive-annotations` reports as an error), the parser
|
|
27
|
+
* falls back to `'shared'` so a misconfigured Msg variant doesn't
|
|
28
|
+
* silently lock out one audience.
|
|
22
29
|
*/
|
|
23
|
-
export function extractMsgAnnotations(source
|
|
30
|
+
export function extractMsgAnnotations(source,
|
|
31
|
+
/**
|
|
32
|
+
* Name of the type alias to extract from. Defaults to `'Msg'` for
|
|
33
|
+
* convention. Passed by the cross-file resolver when the alias has
|
|
34
|
+
* been renamed through imports/re-exports — its local name in the
|
|
35
|
+
* declaring file may differ from `'Msg'`.
|
|
36
|
+
*/
|
|
37
|
+
typeName = 'Msg') {
|
|
24
38
|
const sf = ts.createSourceFile('msg.ts', source, ts.ScriptTarget.Latest, true);
|
|
25
39
|
const aliases = [];
|
|
26
40
|
sf.forEachChild((n) => {
|
|
27
41
|
if (ts.isTypeAliasDeclaration(n))
|
|
28
42
|
aliases.push(n);
|
|
29
43
|
});
|
|
30
|
-
const named = aliases.find((a) => a.name.text ===
|
|
31
|
-
|
|
44
|
+
const named = aliases.find((a) => a.name.text === typeName);
|
|
45
|
+
// Fallback: only when looking for the conventional 'Msg' name AND the
|
|
46
|
+
// file has no `type Msg = …`; pick any union type alias. With an
|
|
47
|
+
// explicit `typeName` from the resolver, we don't fall back — that
|
|
48
|
+
// would silently match the wrong alias.
|
|
49
|
+
const alias = named ?? (typeName === 'Msg' ? aliases.find((a) => ts.isUnionTypeNode(a.type)) : undefined);
|
|
32
50
|
if (!alias || !ts.isUnionTypeNode(alias.type))
|
|
33
51
|
return null;
|
|
34
52
|
const result = {};
|
|
@@ -75,17 +93,75 @@ function readLeadingJSDoc(source, scanPos) {
|
|
|
75
93
|
}
|
|
76
94
|
function parseAnnotations(comment) {
|
|
77
95
|
if (!comment)
|
|
78
|
-
return { ...DEFAULT };
|
|
96
|
+
return { ...DEFAULT, examples: [] };
|
|
79
97
|
const intent = readIntent(comment);
|
|
98
|
+
const human = /@humanOnly\b/.test(comment);
|
|
99
|
+
const agent = /@agentOnly\b/.test(comment);
|
|
100
|
+
// Mutual-exclusion fallback: both tags present means a config bug;
|
|
101
|
+
// the ESLint rule reports it. At parse time, default to 'shared' so
|
|
102
|
+
// we don't silently lock out one audience based on tag order.
|
|
103
|
+
const dispatchMode = human && !agent ? 'human-only' : agent && !human ? 'agent-only' : 'shared';
|
|
80
104
|
return {
|
|
81
105
|
intent,
|
|
82
106
|
alwaysAffordable: /@alwaysAffordable\b/.test(comment),
|
|
83
107
|
requiresConfirm: /@requiresConfirm\b/.test(comment),
|
|
84
|
-
|
|
108
|
+
dispatchMode,
|
|
109
|
+
examples: readExamples(comment),
|
|
110
|
+
warning: readWarning(comment),
|
|
111
|
+
emits: readEmits(comment),
|
|
85
112
|
};
|
|
86
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Match `@emits("k1", "k2", ...)` — comma-separated list of effect
|
|
116
|
+
* kind strings. Each entry can use straight or curly quotes; the
|
|
117
|
+
* separator is `,` with arbitrary whitespace. Returns the kinds in
|
|
118
|
+
* source order (deduped). Empty when the tag is absent or has no
|
|
119
|
+
* quoted strings.
|
|
120
|
+
*/
|
|
121
|
+
function readEmits(comment) {
|
|
122
|
+
// Match the whole `@emits(...)` parenthesized group so we can
|
|
123
|
+
// re-parse the inner content for individual quoted strings. The
|
|
124
|
+
// outer match is non-greedy on the closing paren to avoid eating
|
|
125
|
+
// through later JSDoc.
|
|
126
|
+
const outer = comment.match(/@emits\s*\(([^)]*)\)/);
|
|
127
|
+
if (!outer || outer[1] === undefined)
|
|
128
|
+
return [];
|
|
129
|
+
const inner = outer[1];
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const out = [];
|
|
132
|
+
const re = /["“]([^"”]*)["”]/g;
|
|
133
|
+
let m;
|
|
134
|
+
while ((m = re.exec(inner)) !== null) {
|
|
135
|
+
const v = m[1];
|
|
136
|
+
if (v === undefined || seen.has(v))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(v);
|
|
139
|
+
out.push(v);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
87
143
|
function readIntent(comment) {
|
|
88
144
|
const match = comment.match(/@intent\s*\(\s*["\u201c]([^"\u201d]*)["\u201d]\s*\)/);
|
|
89
145
|
return match?.[1] ?? null;
|
|
90
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Match every `@example("\u2026")` (and curly-quote variant) in source
|
|
149
|
+
* order. Multiple tags on one variant are common \u2014 typical-case,
|
|
150
|
+
* edge-case-with-auth, etc. \u2014 so the parser collects all of them
|
|
151
|
+
* rather than picking the first.
|
|
152
|
+
*/
|
|
153
|
+
function readExamples(comment) {
|
|
154
|
+
const out = [];
|
|
155
|
+
const re = /@example\s*\(\s*["\u201c]([^"\u201d]*)["\u201d]\s*\)/g;
|
|
156
|
+
let m;
|
|
157
|
+
while ((m = re.exec(comment)) !== null) {
|
|
158
|
+
if (m[1] !== undefined)
|
|
159
|
+
out.push(m[1]);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
function readWarning(comment) {
|
|
164
|
+
const match = comment.match(/@warning\s*\(\s*["\u201c]([^"\u201d]*)["\u201d]\s*\)/);
|
|
165
|
+
return match?.[1] ?? null;
|
|
166
|
+
}
|
|
91
167
|
//# sourceMappingURL=msg-annotations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"msg-annotations.js","sourceRoot":"","sources":["../src/msg-annotations.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript'\n\nexport type MessageAnnotations = {\n intent: string | null\n alwaysAffordable: boolean\n requiresConfirm: boolean\n humanOnly: boolean\n}\n\nconst DEFAULT: MessageAnnotations = {\n intent: null,\n alwaysAffordable: false,\n requiresConfirm: false,\n humanOnly: false,\n}\n\n/**\n * Walk a Msg-like discriminated-union type alias and extract JSDoc\n * annotations attached to each union member. Returns null if no\n * recognizable union is found so callers can skip emission cleanly.\n *\n * Expected JSDoc grammar (order-independent):\n * @intent(\"human readable\")\n * @alwaysAffordable\n * @requiresConfirm\n * @humanOnly\n *\n * Unknown tags are ignored; malformed @intent (no quoted string) is\n * treated as \"no intent\". The four flags are booleans; any occurrence\n * of the tag sets it true.\n */\nexport function extractMsgAnnotations(source: string): Record<string, MessageAnnotations> | null {\n const sf = ts.createSourceFile('msg.ts', source, ts.ScriptTarget.Latest, true)\n const aliases: ts.TypeAliasDeclaration[] = []\n sf.forEachChild((n) => {\n if (ts.isTypeAliasDeclaration(n)) aliases.push(n)\n })\n const named = aliases.find((a) => a.name.text === 'Msg')\n const alias = named ?? aliases.find((a) => ts.isUnionTypeNode(a.type))\n if (!alias || !ts.isUnionTypeNode(alias.type)) return null\n\n const result: Record<string, MessageAnnotations> = {}\n const types = alias.type.types\n for (let i = 0; i < types.length; i++) {\n const member = types[i]\n if (member === undefined || !ts.isTypeLiteralNode(member)) continue\n const variant = readDiscriminantLiteral(member)\n if (!variant) continue\n // Leading JSDoc for union member i is scanned from the end of the\n // previous element (or union.pos for the first member), because\n // TypeScript's parser places comment ranges relative to the token\n // that follows them — and the | bar is not part of the TypeLiteralNode.\n const prev = types[i - 1]\n const scanPos = i === 0 || prev === undefined ? alias.type.pos : prev.end\n const comment = readLeadingJSDoc(source, scanPos)\n result[variant] = parseAnnotations(comment)\n }\n return Object.keys(result).length === 0 ? null : result\n}\n\nfunction readDiscriminantLiteral(lit: ts.TypeLiteralNode): string | null {\n for (const m of lit.members) {\n if (!ts.isPropertySignature(m)) continue\n if (!m.name || !ts.isIdentifier(m.name) || m.name.text !== 'type') continue\n if (!m.type || !ts.isLiteralTypeNode(m.type)) continue\n const literal = m.type.literal\n if (ts.isStringLiteral(literal)) return literal.text\n }\n return null\n}\n\nfunction readLeadingJSDoc(source: string, scanPos: number): string {\n const ranges = ts.getLeadingCommentRanges(source, scanPos) ?? []\n const docs = ranges\n .filter((r) => r.kind === ts.SyntaxKind.MultiLineCommentTrivia)\n .map((r) => source.slice(r.pos, r.end))\n .filter((txt) => txt.startsWith('/**'))\n return docs.join('\\n')\n}\n\nfunction parseAnnotations(comment: string): MessageAnnotations {\n if (!comment) return { ...DEFAULT }\n const intent = readIntent(comment)\n return {\n intent,\n alwaysAffordable: /@alwaysAffordable\\b/.test(comment),\n requiresConfirm: /@requiresConfirm\\b/.test(comment),\n humanOnly: /@humanOnly\\b/.test(comment),\n }\n}\n\nfunction readIntent(comment: string): string | null {\n const match = comment.match(/@intent\\s*\\(\\s*[\"\\u201c]([^\"\\u201d]*)[\"\\u201d]\\s*\\)/)\n return match?.[1] ?? null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"msg-annotations.js","sourceRoot":"","sources":["../src/msg-annotations.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript'\n\nexport type DispatchMode = 'shared' | 'human-only' | 'agent-only'\n\nexport type MessageAnnotations = {\n intent: string | null\n alwaysAffordable: boolean\n requiresConfirm: boolean\n dispatchMode: DispatchMode\n /**\n * Concrete example dispatches the LLM can copy from. Populated by\n * `@example(\"text\")` JSDoc tags. Each tag becomes one entry, in\n * source order, so authors can mix scenarios (\"typical case\",\n * \"edge case with auth\", etc.) without nesting them in a single\n * string.\n */\n examples: string[]\n /**\n * Non-blocking caution. Surfaced verbatim to the agent at affordance\n * time so the LLM can weigh the consequence (\"this overwrites the\n * cloud version\", \"fires analytics that can't be retracted\") before\n * dispatching. Distinct from `requiresConfirm`, which is a runtime\n * gate the user must acknowledge.\n */\n warning: string | null\n /**\n * Effect kinds this variant emits when dispatched, declared by the\n * author via `@emits(\"kind1\", \"kind2\")`. Lets the agent reason\n * about side effects (\"this dispatch hits the cloud, so I should\n * batch\") without the compiler having to walk update.ts. Authored\n * rather than auto-extracted because real apps emit effects\n * through helpers (`track('foo')`, `saveDelta(d)`) — auto-detecting\n * those would require helper-return-shape analysis with\n * ergonomically-painful failure modes; the declarative form trades\n * automatic discovery for accuracy and simplicity.\n *\n * Empty when no `@emits` tag is present.\n */\n emits: string[]\n}\n\nconst DEFAULT: MessageAnnotations = {\n intent: null,\n alwaysAffordable: false,\n requiresConfirm: false,\n dispatchMode: 'shared',\n examples: [],\n warning: null,\n emits: [],\n}\n\n/**\n * Walk a Msg-like discriminated-union type alias and extract JSDoc\n * annotations attached to each union member. Returns null if no\n * recognizable union is found so callers can skip emission cleanly.\n *\n * Expected JSDoc grammar (order-independent):\n * @intent(\"human readable\")\n * @alwaysAffordable\n * @requiresConfirm\n * @humanOnly — sugar for dispatchMode: 'human-only'\n * @agentOnly — sugar for dispatchMode: 'agent-only'\n *\n * Unknown tags are ignored; malformed @intent (no quoted string) is\n * treated as \"no intent\". `@humanOnly` and `@agentOnly` are mutually\n * exclusive — if both are present (which the ESLint rule\n * `agent-exclusive-annotations` reports as an error), the parser\n * falls back to `'shared'` so a misconfigured Msg variant doesn't\n * silently lock out one audience.\n */\nexport function extractMsgAnnotations(\n source: string,\n /**\n * Name of the type alias to extract from. Defaults to `'Msg'` for\n * convention. Passed by the cross-file resolver when the alias has\n * been renamed through imports/re-exports — its local name in the\n * declaring file may differ from `'Msg'`.\n */\n typeName: string = 'Msg',\n): Record<string, MessageAnnotations> | null {\n const sf = ts.createSourceFile('msg.ts', source, ts.ScriptTarget.Latest, true)\n const aliases: ts.TypeAliasDeclaration[] = []\n sf.forEachChild((n) => {\n if (ts.isTypeAliasDeclaration(n)) aliases.push(n)\n })\n const named = aliases.find((a) => a.name.text === typeName)\n // Fallback: only when looking for the conventional 'Msg' name AND the\n // file has no `type Msg = …`; pick any union type alias. With an\n // explicit `typeName` from the resolver, we don't fall back — that\n // would silently match the wrong alias.\n const alias =\n named ?? (typeName === 'Msg' ? aliases.find((a) => ts.isUnionTypeNode(a.type)) : undefined)\n if (!alias || !ts.isUnionTypeNode(alias.type)) return null\n\n const result: Record<string, MessageAnnotations> = {}\n const types = alias.type.types\n for (let i = 0; i < types.length; i++) {\n const member = types[i]\n if (member === undefined || !ts.isTypeLiteralNode(member)) continue\n const variant = readDiscriminantLiteral(member)\n if (!variant) continue\n // Leading JSDoc for union member i is scanned from the end of the\n // previous element (or union.pos for the first member), because\n // TypeScript's parser places comment ranges relative to the token\n // that follows them — and the | bar is not part of the TypeLiteralNode.\n const prev = types[i - 1]\n const scanPos = i === 0 || prev === undefined ? alias.type.pos : prev.end\n const comment = readLeadingJSDoc(source, scanPos)\n result[variant] = parseAnnotations(comment)\n }\n return Object.keys(result).length === 0 ? null : result\n}\n\nfunction readDiscriminantLiteral(lit: ts.TypeLiteralNode): string | null {\n for (const m of lit.members) {\n if (!ts.isPropertySignature(m)) continue\n if (!m.name || !ts.isIdentifier(m.name) || m.name.text !== 'type') continue\n if (!m.type || !ts.isLiteralTypeNode(m.type)) continue\n const literal = m.type.literal\n if (ts.isStringLiteral(literal)) return literal.text\n }\n return null\n}\n\nfunction readLeadingJSDoc(source: string, scanPos: number): string {\n const ranges = ts.getLeadingCommentRanges(source, scanPos) ?? []\n const docs = ranges\n .filter((r) => r.kind === ts.SyntaxKind.MultiLineCommentTrivia)\n .map((r) => source.slice(r.pos, r.end))\n .filter((txt) => txt.startsWith('/**'))\n return docs.join('\\n')\n}\n\nfunction parseAnnotations(comment: string): MessageAnnotations {\n if (!comment) return { ...DEFAULT, examples: [] }\n const intent = readIntent(comment)\n const human = /@humanOnly\\b/.test(comment)\n const agent = /@agentOnly\\b/.test(comment)\n // Mutual-exclusion fallback: both tags present means a config bug;\n // the ESLint rule reports it. At parse time, default to 'shared' so\n // we don't silently lock out one audience based on tag order.\n const dispatchMode: DispatchMode =\n human && !agent ? 'human-only' : agent && !human ? 'agent-only' : 'shared'\n return {\n intent,\n alwaysAffordable: /@alwaysAffordable\\b/.test(comment),\n requiresConfirm: /@requiresConfirm\\b/.test(comment),\n dispatchMode,\n examples: readExamples(comment),\n warning: readWarning(comment),\n emits: readEmits(comment),\n }\n}\n\n/**\n * Match `@emits(\"k1\", \"k2\", ...)` — comma-separated list of effect\n * kind strings. Each entry can use straight or curly quotes; the\n * separator is `,` with arbitrary whitespace. Returns the kinds in\n * source order (deduped). Empty when the tag is absent or has no\n * quoted strings.\n */\nfunction readEmits(comment: string): string[] {\n // Match the whole `@emits(...)` parenthesized group so we can\n // re-parse the inner content for individual quoted strings. The\n // outer match is non-greedy on the closing paren to avoid eating\n // through later JSDoc.\n const outer = comment.match(/@emits\\s*\\(([^)]*)\\)/)\n if (!outer || outer[1] === undefined) return []\n const inner = outer[1]\n const seen = new Set<string>()\n const out: string[] = []\n const re = /[\"“]([^\"”]*)[\"”]/g\n let m: RegExpExecArray | null\n while ((m = re.exec(inner)) !== null) {\n const v = m[1]\n if (v === undefined || seen.has(v)) continue\n seen.add(v)\n out.push(v)\n }\n return out\n}\n\nfunction readIntent(comment: string): string | null {\n const match = comment.match(/@intent\\s*\\(\\s*[\"\\u201c]([^\"\\u201d]*)[\"\\u201d]\\s*\\)/)\n return match?.[1] ?? null\n}\n\n/**\n * Match every `@example(\"\\u2026\")` (and curly-quote variant) in source\n * order. Multiple tags on one variant are common \\u2014 typical-case,\n * edge-case-with-auth, etc. \\u2014 so the parser collects all of them\n * rather than picking the first.\n */\nfunction readExamples(comment: string): string[] {\n const out: string[] = []\n const re = /@example\\s*\\(\\s*[\"\\u201c]([^\"\\u201d]*)[\"\\u201d]\\s*\\)/g\n let m: RegExpExecArray | null\n while ((m = re.exec(comment)) !== null) {\n if (m[1] !== undefined) out.push(m[1])\n }\n return out\n}\n\nfunction readWarning(comment: string): string | null {\n const match = comment.match(/@warning\\s*\\(\\s*[\"\\u201c]([^\"\\u201d]*)[\"\\u201d]\\s*\\)/)\n return match?.[1] ?? null\n}\n"]}
|
package/dist/msg-schema.d.ts
CHANGED
|
@@ -1,9 +1,85 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
/**
|
|
3
|
+
* The "bare type" of a field. Covers four cases:
|
|
4
|
+
* - primitive keyword as a string: `'string'`, `'number'`, `'boolean'`, `'unknown'`
|
|
5
|
+
* - string-literal union: `{enum: ['a', 'b']}`
|
|
6
|
+
* - nested object shape: `{kind: 'object', shape: {...}}` — emitted when
|
|
7
|
+
* a field's type is a local interface/type alias the extractor could
|
|
8
|
+
* follow (depth-limited; cross-file references stay `'unknown'`).
|
|
9
|
+
* - array of element type: `{kind: 'array', element: <bare type>}`.
|
|
10
|
+
*
|
|
11
|
+
* The synthesizer in `@llui/agent`'s `list_actions` walks these to build
|
|
12
|
+
* copy-paste-ready payload examples; the validator in `send_message`
|
|
13
|
+
* walks them too (treating object/array as "any" since deep validation
|
|
14
|
+
* is the reducer's job).
|
|
15
|
+
*/
|
|
16
|
+
export type MsgFieldType = string | {
|
|
17
|
+
enum: string[];
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'object';
|
|
20
|
+
shape: Record<string, MsgField>;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'array';
|
|
23
|
+
element: MsgFieldType;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Rich per-field descriptor. Emitted only when there's something
|
|
27
|
+
* beyond the bare type to communicate — optionality, an explicit
|
|
28
|
+
* priority hint, or a freeform agent hint. When everything but `type`
|
|
29
|
+
* is unset, the producer emits the bare `MsgFieldType` instead so
|
|
30
|
+
* variants without annotations stay byte-cheap in the bundle.
|
|
31
|
+
*/
|
|
32
|
+
export interface MsgFieldRich {
|
|
33
|
+
type: MsgFieldType;
|
|
34
|
+
/** Mirrors TypeScript's `?:` optional marker. Required fields omit this. */
|
|
35
|
+
optional?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Strength signal for optional fields. Borrows RFC 2119's `SHOULD`:
|
|
38
|
+
* the LLM ought to fill it in unless it has a specific reason not
|
|
39
|
+
* to. Required fields don't carry a priority — TS already conveys
|
|
40
|
+
* "must" via the type system. Currently the only level; future
|
|
41
|
+
* extensions could add `'recommended'` or similar.
|
|
42
|
+
*/
|
|
43
|
+
priority?: 'should';
|
|
44
|
+
/** Freeform consequence-shaped explanation. Surfaced verbatim to
|
|
45
|
+
* the LLM at affordance time. */
|
|
46
|
+
hint?: string;
|
|
47
|
+
}
|
|
48
|
+
export type MsgField = MsgFieldType | MsgFieldRich;
|
|
1
49
|
export interface MsgSchema {
|
|
2
50
|
discriminant: string;
|
|
3
|
-
variants: Record<string, Record<string,
|
|
4
|
-
enum: string[];
|
|
5
|
-
}>>;
|
|
51
|
+
variants: Record<string, Record<string, MsgField>>;
|
|
6
52
|
}
|
|
7
|
-
|
|
8
|
-
export declare function
|
|
53
|
+
/** True when `f` is a rich descriptor (object with `type` key). */
|
|
54
|
+
export declare function isRichField(f: MsgField): f is MsgFieldRich;
|
|
55
|
+
/** Extracts the bare type from either descriptor form. */
|
|
56
|
+
export declare function fieldType(f: MsgField): MsgFieldType;
|
|
57
|
+
export declare function extractMsgSchema(source: string, typeName?: string): MsgSchema | null;
|
|
58
|
+
export declare function extractEffectSchema(source: string, typeName?: string): MsgSchema | null;
|
|
59
|
+
/**
|
|
60
|
+
* Index of type aliases and interfaces visible from a source file,
|
|
61
|
+
* keyed by name. Lets the field-type resolver follow `Criterion[]` →
|
|
62
|
+
* `interface Criterion { … }` and emit a nested object shape rather
|
|
63
|
+
* than `'unknown'`.
|
|
64
|
+
*
|
|
65
|
+
* The cross-file resolver pipeline (`cross-file-resolver.ts`) builds
|
|
66
|
+
* an enriched index that includes types imported from sibling files —
|
|
67
|
+
* follow `GridSorting` → `'rank' | 'crit-X' | 'crit-Y'` → `{enum: […]}`
|
|
68
|
+
* even when the alias lives in `./state.ts` not the Msg-defining file.
|
|
69
|
+
*/
|
|
70
|
+
export type TypeIndex = Map<string, ts.TypeNode | ts.InterfaceDeclaration>;
|
|
71
|
+
/**
|
|
72
|
+
* Build a single field descriptor from a property signature: type,
|
|
73
|
+
* optionality, and any `@should("…")` JSDoc hint. Emits the compact
|
|
74
|
+
* bare form when there's nothing extra to communicate; otherwise the
|
|
75
|
+
* rich `{type, optional?, priority?, hint?}` shape.
|
|
76
|
+
*
|
|
77
|
+
* Exported so the cross-file resolver (which walks the same property
|
|
78
|
+
* signatures when the Msg type lives in a different file from the
|
|
79
|
+
* `component()` call) can produce identical descriptors. Without
|
|
80
|
+
* sharing this helper, JSDoc hints would silently disappear whenever
|
|
81
|
+
* a Msg union got resolved across module boundaries.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildFieldDescriptor(member: ts.PropertySignature, source: string, typeIndex?: TypeIndex): MsgField;
|
|
84
|
+
export declare function resolveFieldType(type: ts.TypeNode, typeIndex?: TypeIndex, depth?: number): MsgFieldType;
|
|
9
85
|
//# sourceMappingURL=msg-schema.d.ts.map
|
package/dist/msg-schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"msg-schema.d.ts","sourceRoot":"","sources":["../src/msg-schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"msg-schema.d.ts","sourceRoot":"","sources":["../src/msg-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAA;AAE3B;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GACpB,MAAM,GACN;IAAE,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAAA;AAE5C;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,YAAY,CAAA;IAClB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB;sCACkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,YAAY,CAAA;AAElD,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;CACnD;AAED,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,IAAI,YAAY,CAE1D;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,CAAC,EAAE,QAAQ,GAAG,YAAY,CAEnD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAc,GAAG,SAAS,GAAG,IAAI,CAE3F;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAiB,GAAG,SAAS,GAAG,IAAI,CAEjG;AAqBD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,GAAG,EAAE,CAAC,oBAAoB,CAAC,CAAA;AAsD1E;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,EAAE,CAAC,iBAAiB,EAC5B,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,SAAqB,GAC/B,QAAQ,CAkBV;AAeD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,EAAE,CAAC,QAAQ,EACjB,SAAS,GAAE,SAAqB,EAChC,KAAK,SAAkB,GACtB,YAAY,CA0Fd"}
|
package/dist/msg-schema.js
CHANGED
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/** True when `f` is a rich descriptor (object with `type` key). */
|
|
3
|
+
export function isRichField(f) {
|
|
4
|
+
return typeof f === 'object' && f !== null && !Array.isArray(f) && 'type' in f;
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
/** Extracts the bare type from either descriptor form. */
|
|
7
|
+
export function fieldType(f) {
|
|
8
|
+
return isRichField(f) ? f.type : f;
|
|
9
|
+
}
|
|
10
|
+
export function extractMsgSchema(source, typeName = 'Msg') {
|
|
11
|
+
return extractDiscriminatedUnionSchema(source, typeName);
|
|
12
|
+
}
|
|
13
|
+
export function extractEffectSchema(source, typeName = 'Effect') {
|
|
14
|
+
return extractDiscriminatedUnionSchema(source, typeName);
|
|
7
15
|
}
|
|
8
16
|
function extractDiscriminatedUnionSchema(source, typeName) {
|
|
9
17
|
const sf = ts.createSourceFile('input.ts', source, ts.ScriptTarget.Latest, true);
|
|
18
|
+
const typeIndex = buildTypeIndex(sf);
|
|
10
19
|
for (const stmt of sf.statements) {
|
|
11
20
|
if (!ts.isTypeAliasDeclaration(stmt))
|
|
12
21
|
continue;
|
|
13
22
|
if (stmt.name.text !== typeName)
|
|
14
23
|
continue;
|
|
15
24
|
const variants = {};
|
|
16
|
-
collectVariants(stmt.type, variants);
|
|
25
|
+
collectVariants(stmt.type, variants, source, typeIndex);
|
|
17
26
|
if (Object.keys(variants).length === 0)
|
|
18
27
|
return null;
|
|
19
28
|
return { discriminant: 'type', variants };
|
|
20
29
|
}
|
|
21
30
|
return null;
|
|
22
31
|
}
|
|
23
|
-
function
|
|
32
|
+
function buildTypeIndex(sf) {
|
|
33
|
+
const index = new Map();
|
|
34
|
+
for (const stmt of sf.statements) {
|
|
35
|
+
if (ts.isTypeAliasDeclaration(stmt)) {
|
|
36
|
+
index.set(stmt.name.text, stmt.type);
|
|
37
|
+
}
|
|
38
|
+
else if (ts.isInterfaceDeclaration(stmt)) {
|
|
39
|
+
index.set(stmt.name.text, stmt);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return index;
|
|
43
|
+
}
|
|
44
|
+
function collectVariants(type, variants, source, typeIndex) {
|
|
24
45
|
if (ts.isUnionTypeNode(type)) {
|
|
25
46
|
for (const member of type.types) {
|
|
26
|
-
collectVariants(member, variants);
|
|
47
|
+
collectVariants(member, variants, source, typeIndex);
|
|
27
48
|
}
|
|
28
49
|
return;
|
|
29
50
|
}
|
|
@@ -42,18 +63,57 @@ function collectVariants(type, variants) {
|
|
|
42
63
|
}
|
|
43
64
|
continue;
|
|
44
65
|
}
|
|
45
|
-
|
|
46
|
-
fields[name] = 'unknown';
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
fields[name] = resolveFieldType(memberType);
|
|
66
|
+
fields[name] = buildFieldDescriptor(member, source, typeIndex);
|
|
50
67
|
}
|
|
51
68
|
if (discriminantValue) {
|
|
52
69
|
variants[discriminantValue] = fields;
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Build a single field descriptor from a property signature: type,
|
|
75
|
+
* optionality, and any `@should("…")` JSDoc hint. Emits the compact
|
|
76
|
+
* bare form when there's nothing extra to communicate; otherwise the
|
|
77
|
+
* rich `{type, optional?, priority?, hint?}` shape.
|
|
78
|
+
*
|
|
79
|
+
* Exported so the cross-file resolver (which walks the same property
|
|
80
|
+
* signatures when the Msg type lives in a different file from the
|
|
81
|
+
* `component()` call) can produce identical descriptors. Without
|
|
82
|
+
* sharing this helper, JSDoc hints would silently disappear whenever
|
|
83
|
+
* a Msg union got resolved across module boundaries.
|
|
84
|
+
*/
|
|
85
|
+
export function buildFieldDescriptor(member, source, typeIndex = new Map()) {
|
|
86
|
+
const baseType = member.type
|
|
87
|
+
? resolveFieldType(member.type, typeIndex, MAX_FIELD_DEPTH)
|
|
88
|
+
: 'unknown';
|
|
89
|
+
const optional = member.questionToken !== undefined;
|
|
90
|
+
const jsdoc = readMemberJSDoc(source, member);
|
|
91
|
+
const hint = readShouldHint(jsdoc);
|
|
92
|
+
if (!optional && hint === null) {
|
|
93
|
+
return baseType;
|
|
94
|
+
}
|
|
95
|
+
const rich = { type: baseType };
|
|
96
|
+
if (optional)
|
|
97
|
+
rich.optional = true;
|
|
98
|
+
if (hint !== null) {
|
|
99
|
+
rich.priority = 'should';
|
|
100
|
+
rich.hint = hint;
|
|
101
|
+
}
|
|
102
|
+
return rich;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Recursion bound for nested type resolution. Stops the extractor
|
|
106
|
+
* before it spirals on self-referential or mutually-recursive types
|
|
107
|
+
* (`type Tree = { children: Tree[] }`). At depth 0 every reference
|
|
108
|
+
* collapses to `'unknown'`; the synthesizer emits `null` and the
|
|
109
|
+
* agent falls back to free-form filling.
|
|
110
|
+
*
|
|
111
|
+
* 3 covers the common cases (Msg payload → Criterion → ValueMeta),
|
|
112
|
+
* keeps the bundle bounded, and is well under the Tarjan-style
|
|
113
|
+
* depths needed for actual recursive types.
|
|
114
|
+
*/
|
|
115
|
+
const MAX_FIELD_DEPTH = 3;
|
|
116
|
+
export function resolveFieldType(type, typeIndex = new Map(), depth = MAX_FIELD_DEPTH) {
|
|
57
117
|
// Primitive keywords
|
|
58
118
|
if (type.kind === ts.SyntaxKind.StringKeyword)
|
|
59
119
|
return 'string';
|
|
@@ -78,6 +138,134 @@ function resolveFieldType(type) {
|
|
|
78
138
|
return { enum: literals };
|
|
79
139
|
}
|
|
80
140
|
}
|
|
141
|
+
// Below this point, all branches need depth budget. Bail out cheaply.
|
|
142
|
+
if (depth <= 0)
|
|
143
|
+
return 'unknown';
|
|
144
|
+
// Inline object literal — `{a: number; b: string}` directly.
|
|
145
|
+
if (ts.isTypeLiteralNode(type)) {
|
|
146
|
+
return { kind: 'object', shape: collectInlineShape(type, typeIndex, depth - 1) };
|
|
147
|
+
}
|
|
148
|
+
// Array type — `T[]` and `readonly T[]`.
|
|
149
|
+
if (ts.isArrayTypeNode(type)) {
|
|
150
|
+
return { kind: 'array', element: resolveFieldType(type.elementType, typeIndex, depth - 1) };
|
|
151
|
+
}
|
|
152
|
+
// Generic Array<T> (less common in app code but compiler may produce it).
|
|
153
|
+
if (ts.isTypeReferenceNode(type) &&
|
|
154
|
+
ts.isIdentifier(type.typeName) &&
|
|
155
|
+
type.typeName.text === 'Array' &&
|
|
156
|
+
type.typeArguments?.length === 1 &&
|
|
157
|
+
type.typeArguments[0]) {
|
|
158
|
+
return {
|
|
159
|
+
kind: 'array',
|
|
160
|
+
element: resolveFieldType(type.typeArguments[0], typeIndex, depth - 1),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// ReadonlyArray<T> → same shape; the readonly modifier is purely a
|
|
164
|
+
// TypeScript-side concern that the agent never observes at runtime.
|
|
165
|
+
if (ts.isTypeReferenceNode(type) &&
|
|
166
|
+
ts.isIdentifier(type.typeName) &&
|
|
167
|
+
type.typeName.text === 'ReadonlyArray' &&
|
|
168
|
+
type.typeArguments?.length === 1 &&
|
|
169
|
+
type.typeArguments[0]) {
|
|
170
|
+
return {
|
|
171
|
+
kind: 'array',
|
|
172
|
+
element: resolveFieldType(type.typeArguments[0], typeIndex, depth - 1),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// `readonly T[]` parses as TypeOperator(readonly) wrapping ArrayType.
|
|
176
|
+
if (ts.isTypeOperatorNode(type) && type.operator === ts.SyntaxKind.ReadonlyKeyword) {
|
|
177
|
+
return resolveFieldType(type.type, typeIndex, depth);
|
|
178
|
+
}
|
|
179
|
+
// Named type reference — chase it through the local index.
|
|
180
|
+
if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
|
|
181
|
+
const target = typeIndex.get(type.typeName.text);
|
|
182
|
+
if (target) {
|
|
183
|
+
if (ts.isInterfaceDeclaration(target)) {
|
|
184
|
+
return {
|
|
185
|
+
kind: 'object',
|
|
186
|
+
shape: collectInterfaceShape(target, typeIndex, depth - 1),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Type alias — recurse on its body. `type Foo = …` could resolve
|
|
190
|
+
// to anything (object literal, array, union, primitive); each
|
|
191
|
+
// already has its own branch above.
|
|
192
|
+
return resolveFieldType(target, typeIndex, depth - 1);
|
|
193
|
+
}
|
|
194
|
+
// Reference to a type the index doesn't know about — typically
|
|
195
|
+
// imported from another module. Cross-file resolution is the
|
|
196
|
+
// separate cross-file-resolver pipeline's job; leave this as
|
|
197
|
+
// unknown rather than fabricating a misleading shape.
|
|
198
|
+
return 'unknown';
|
|
199
|
+
}
|
|
81
200
|
return 'unknown';
|
|
82
201
|
}
|
|
202
|
+
function collectInlineShape(lit, typeIndex, depth) {
|
|
203
|
+
const shape = {};
|
|
204
|
+
for (const member of lit.members) {
|
|
205
|
+
if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name))
|
|
206
|
+
continue;
|
|
207
|
+
const name = member.name.text;
|
|
208
|
+
const baseType = member.type
|
|
209
|
+
? resolveFieldType(member.type, typeIndex, depth)
|
|
210
|
+
: 'unknown';
|
|
211
|
+
const optional = member.questionToken !== undefined;
|
|
212
|
+
if (!optional) {
|
|
213
|
+
shape[name] = baseType;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
shape[name] = { type: baseType, optional: true };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return shape;
|
|
220
|
+
}
|
|
221
|
+
function collectInterfaceShape(iface, typeIndex, depth) {
|
|
222
|
+
const shape = {};
|
|
223
|
+
for (const member of iface.members) {
|
|
224
|
+
if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name))
|
|
225
|
+
continue;
|
|
226
|
+
const name = member.name.text;
|
|
227
|
+
const baseType = member.type
|
|
228
|
+
? resolveFieldType(member.type, typeIndex, depth)
|
|
229
|
+
: 'unknown';
|
|
230
|
+
const optional = member.questionToken !== undefined;
|
|
231
|
+
if (!optional) {
|
|
232
|
+
shape[name] = baseType;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
shape[name] = { type: baseType, optional: true };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return shape;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Read the leading JSDoc block immediately above `member`. The
|
|
242
|
+
* TypeScript parser doesn't attach JSDoc to interior property
|
|
243
|
+
* signatures, so we re-scan the source between the previous member's
|
|
244
|
+
* end (or the type-literal's `{`) and this member's start, and return
|
|
245
|
+
* the last `/** … *\/` block found there. Returns `''` when none.
|
|
246
|
+
*/
|
|
247
|
+
function readMemberJSDoc(source, member) {
|
|
248
|
+
const ranges = ts.getLeadingCommentRanges(source, member.pos) ?? [];
|
|
249
|
+
// Walk in order, keeping only `/** */` blocks. Multiple back-to-back
|
|
250
|
+
// JSDocs concatenate (matches msg-annotations.ts's existing behavior).
|
|
251
|
+
const docs = ranges
|
|
252
|
+
.filter((r) => r.kind === ts.SyntaxKind.MultiLineCommentTrivia)
|
|
253
|
+
.map((r) => source.slice(r.pos, r.end))
|
|
254
|
+
.filter((txt) => txt.startsWith('/**'));
|
|
255
|
+
return docs.join('\n');
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Match `@should("…")` (and curly-quote variant) anywhere in the
|
|
259
|
+
* JSDoc. Mirrors msg-annotations.ts's `@intent` parser — same grammar,
|
|
260
|
+
* same tolerance for either ASCII or curly quotes.
|
|
261
|
+
*
|
|
262
|
+
* Returns the unescaped string content, or null when the tag is
|
|
263
|
+
* absent or malformed.
|
|
264
|
+
*/
|
|
265
|
+
function readShouldHint(comment) {
|
|
266
|
+
if (!comment)
|
|
267
|
+
return null;
|
|
268
|
+
const match = comment.match(/@should\s*\(\s*["“]([^"”]*)["”]\s*\)/);
|
|
269
|
+
return match?.[1] ?? null;
|
|
270
|
+
}
|
|
83
271
|
//# sourceMappingURL=msg-schema.js.map
|