@deepseek-ai/dsh-typert-generator 0.0.1-rc.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 +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +41 -0
- package/README.zh.md +41 -0
- package/lib/index.js +3922 -0
- package/lib/invariant.js +24 -0
- package/lib/types/analyzer.d.ts +143 -0
- package/lib/types/analyzer.js +2723 -0
- package/lib/types/cordis-catalog.d.ts +148 -0
- package/lib/types/cordis-catalog.js +695 -0
- package/lib/types/emitter.d.ts +55 -0
- package/lib/types/emitter.js +745 -0
- package/lib/types/index.d.ts +16 -0
- package/lib/types/index.js +12 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/model.d.ts +409 -0
- package/lib/types/model.js +48 -0
- package/lib/types/renderer.d.ts +90 -0
- package/lib/types/renderer.js +351 -0
- package/lib/types/tsdown-plugin.d.ts +34 -0
- package/lib/types/tsdown-plugin.js +136 -0
- package/lib/types/workspace.d.ts +36 -0
- package/lib/types/workspace.js +114 -0
- package/package.json +54 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cordis catalog-specific projection over the compiler-independent Typert
|
|
3
|
+
* model. This module owns Cordis validation and text projection mechanics;
|
|
4
|
+
* callers supply repository-specific type classifications and inherited data.
|
|
5
|
+
* @module @deepseek-ai/dsh-typert-generator
|
|
6
|
+
*/
|
|
7
|
+
import { WorkspaceAnalyzer, WorkspaceCaches } from "./analyzer.js";
|
|
8
|
+
import { childTypeNodeIds } from "./model.js";
|
|
9
|
+
import { TypeGraphRenderer } from "./renderer.js";
|
|
10
|
+
/** The fenced-block info string for generated signature blocks (skipped by
|
|
11
|
+
* doc-typecheck, since a bare signature fragment is not standalone-compilable). */
|
|
12
|
+
const FENCE = 'ts cordis-catalog';
|
|
13
|
+
/** Append fail-closed signature type-link violations from the retained type tree. */
|
|
14
|
+
function checkTypeLinks(where, names, policy, violations) {
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
if (Object.hasOwn(policy.linkedTypePages, name)
|
|
17
|
+
|| policy.foundationTypeNames.has(name)
|
|
18
|
+
|| Object.hasOwn(policy.typeLinkExemptions, name))
|
|
19
|
+
continue;
|
|
20
|
+
violations.push(`${where} references unclassified type '${name}'. Add it to linkedTypePages with its documentation page, `
|
|
21
|
+
+ 'to foundationTypeNames if TypeScript or the framework owns it, or to typeLinkExemptions with '
|
|
22
|
+
+ 'the non-catalog documentation owner.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Throw one aggregated diagnostic for every unclassified signature type. */
|
|
26
|
+
function reportTypeLinkViolations(gate, violations) {
|
|
27
|
+
if (violations.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
throw new Error(`${gate}: ${violations.length} signature type-link coverage violation(s):\n`
|
|
30
|
+
+ violations.map(violation => ` ${violation}`).join('\n'));
|
|
31
|
+
}
|
|
32
|
+
/** Repository-specific Cordis validation and projection over one Typert face. */
|
|
33
|
+
export class CordisCatalogProjector {
|
|
34
|
+
face;
|
|
35
|
+
sourceDeclarations;
|
|
36
|
+
policy;
|
|
37
|
+
renderer;
|
|
38
|
+
/**
|
|
39
|
+
* @param face - analyzed host face containing package business semantics.
|
|
40
|
+
* @param sourceDeclarations - exported declarations available to the runtime type closure.
|
|
41
|
+
* @param policy - caller-owned type classifications and inherited Cordis data.
|
|
42
|
+
*/
|
|
43
|
+
constructor(face, sourceDeclarations, policy) {
|
|
44
|
+
this.face = face;
|
|
45
|
+
this.sourceDeclarations = sourceDeclarations;
|
|
46
|
+
this.policy = policy;
|
|
47
|
+
if (face.face !== 'host')
|
|
48
|
+
throw new Error(`cordis catalog requires the host face, received ${face.face}`);
|
|
49
|
+
this.renderer = new TypeGraphRenderer(face.graph);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate and project the host model's Cordis surface.
|
|
53
|
+
* @returns every validated service and event projected from the host model.
|
|
54
|
+
*/
|
|
55
|
+
project() {
|
|
56
|
+
return {
|
|
57
|
+
events: this.collectEvents(),
|
|
58
|
+
services: this.collectServices(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Render the model-facing static API consumed by `tool-cordis`.
|
|
63
|
+
* @param model - validated Cordis catalog projection from this projector.
|
|
64
|
+
* @returns the model-facing TypeScript catalog source.
|
|
65
|
+
*/
|
|
66
|
+
renderRuntimeApi(model) {
|
|
67
|
+
return renderRuntimeApi(model.services, model.events, this.runtimeTypes(model.services), this.policy.inheritedServices);
|
|
68
|
+
}
|
|
69
|
+
collectEvents() {
|
|
70
|
+
const entries = [];
|
|
71
|
+
const violations = [];
|
|
72
|
+
const typeLinkViolations = [];
|
|
73
|
+
for (const packageModel of this.face.packages) {
|
|
74
|
+
for (const event of packageModel.events) {
|
|
75
|
+
const source = pointer(event.location);
|
|
76
|
+
const where = `event '${event.name}' (${source})`;
|
|
77
|
+
const node = this.renderer.node(event.signature);
|
|
78
|
+
if (node.kind !== 'function') {
|
|
79
|
+
violations.push(`${where} is not represented by a callable type.`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
checkTypeLinks(where, signatureTypeNames(this.renderer, node.signature), this.policy, typeLinkViolations);
|
|
83
|
+
const parsed = parseJsDoc(event.jsDoc ?? '');
|
|
84
|
+
const mode = event.mode;
|
|
85
|
+
if (!isMode(mode)) {
|
|
86
|
+
violations.push(`${where} is missing an @mode tag. Add '@mode emit|waterfall|parallel|serial' to its JSDoc (see AGENTS.md).`);
|
|
87
|
+
}
|
|
88
|
+
const last = node.signature.parameters.at(-1);
|
|
89
|
+
const hasNext = last?.name === 'next';
|
|
90
|
+
if (isMode(mode) && hasNext && mode !== 'waterfall') {
|
|
91
|
+
violations.push(`${where} has a trailing 'next' parameter (structurally a waterfall) but is tagged '@mode ${mode}'. Fix the tag or the signature.`);
|
|
92
|
+
}
|
|
93
|
+
if (isMode(mode) && !hasNext && mode === 'waterfall') {
|
|
94
|
+
violations.push(`${where} is tagged '@mode waterfall' but has no trailing 'next' parameter. A waterfall delegates via next().`);
|
|
95
|
+
}
|
|
96
|
+
if (parsed.doc === '') {
|
|
97
|
+
violations.push(`${where} has no description prose. Say what happened / what a listener may do, above the block tags.`);
|
|
98
|
+
}
|
|
99
|
+
checkParams(where, 'event', node.signature.parameters, parsed.params, parameter => parameter.receiver || (hasNext && parameter === last), violations);
|
|
100
|
+
if (isMode(mode)) {
|
|
101
|
+
entries.push({
|
|
102
|
+
name: event.name,
|
|
103
|
+
scope: event.name.split('/')[0] ?? event.name,
|
|
104
|
+
signature: event.text,
|
|
105
|
+
jsDoc: event.jsDoc ?? '',
|
|
106
|
+
mode,
|
|
107
|
+
doc: parsed.doc,
|
|
108
|
+
source,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
reportViolations('gen-cordis-catalog', violations);
|
|
114
|
+
reportTypeLinkViolations('gen-cordis-catalog', typeLinkViolations);
|
|
115
|
+
return entries;
|
|
116
|
+
}
|
|
117
|
+
collectServices() {
|
|
118
|
+
const entries = [];
|
|
119
|
+
const violations = [];
|
|
120
|
+
const typeLinkViolations = [];
|
|
121
|
+
for (const packageModel of this.face.packages) {
|
|
122
|
+
for (const service of packageModel.services) {
|
|
123
|
+
const declaration = this.renderer.declaration(service.symbol);
|
|
124
|
+
if (declaration.kind !== 'class'
|
|
125
|
+
|| !/^packages\/[^/]+\/[^/]+\/src\/[^/]+\.ts$/.test(service.location.file)
|
|
126
|
+
|| declaration.location.file !== service.location.file)
|
|
127
|
+
continue;
|
|
128
|
+
const doc = parseJsDoc(declaration.jsDoc ?? '').doc;
|
|
129
|
+
const source = pointer(declaration.location);
|
|
130
|
+
if (doc === '') {
|
|
131
|
+
violations.push(`service ctx.${service.key} (${source}): class ${declaration.name} has no JSDoc.`);
|
|
132
|
+
}
|
|
133
|
+
const methods = [];
|
|
134
|
+
for (const memberId of service.members) {
|
|
135
|
+
const member = this.renderer.member(memberId);
|
|
136
|
+
if (member.kind !== 'method' || member.name.startsWith('['))
|
|
137
|
+
continue;
|
|
138
|
+
const where = `service method ctx.${service.key}.${member.name} (${pointer(member.location)})`;
|
|
139
|
+
checkTypeLinks(where, signatureTypeNames(this.renderer, member.signature), this.policy, typeLinkViolations);
|
|
140
|
+
methods.push({ signature: member.text, jsDoc: member.jsDoc ?? '' });
|
|
141
|
+
if (member.jsDoc === undefined) {
|
|
142
|
+
violations.push(`${where} has no JSDoc.`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const parsed = parseJsDoc(member.jsDoc);
|
|
146
|
+
if (parsed.doc === '')
|
|
147
|
+
violations.push(`${where} has no description prose above its block tags.`);
|
|
148
|
+
checkParams(where, 'service', member.signature.parameters, parsed.params, parameter => parameter.receiver, violations);
|
|
149
|
+
checkReturns(where, member.signature, parsed.returns, this.renderer, violations);
|
|
150
|
+
}
|
|
151
|
+
entries.push({
|
|
152
|
+
key: service.key,
|
|
153
|
+
type: declaration.name,
|
|
154
|
+
abstract: declaration.abstract,
|
|
155
|
+
doc,
|
|
156
|
+
methods,
|
|
157
|
+
source,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
reportViolations('gen-cordis-catalog', violations);
|
|
162
|
+
reportTypeLinkViolations('gen-cordis-catalog', typeLinkViolations);
|
|
163
|
+
return entries.sort((left, right) => left.key.localeCompare(right.key));
|
|
164
|
+
}
|
|
165
|
+
runtimeTypes(services) {
|
|
166
|
+
const declarations = new Map();
|
|
167
|
+
const ambiguous = new Set();
|
|
168
|
+
for (const declaration of this.sourceDeclarations) {
|
|
169
|
+
if (declaration.face !== 'host' || declaration.kind === 'enum'
|
|
170
|
+
|| !/^packages\/[^/]+\/[^/]+\/src\/[^/]+\.ts$/.test(declaration.location.file))
|
|
171
|
+
continue;
|
|
172
|
+
if (declarations.has(declaration.name)) {
|
|
173
|
+
ambiguous.add(declaration.name);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
declarations.set(declaration.name, declaration.text.length > MAX_DECL_CHARS
|
|
177
|
+
? `${declaration.text.slice(0, MAX_DECL_CHARS)} /* …truncated — full shape in source */`
|
|
178
|
+
: declaration.text);
|
|
179
|
+
}
|
|
180
|
+
for (const name of ambiguous)
|
|
181
|
+
declarations.delete(name);
|
|
182
|
+
return referencedTypes(services.flatMap(service => service.methods.map(method => method.signature)), declarations);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Analyze the host project once and return both the model and its projection.
|
|
187
|
+
* @param scanRoot - workspace root containing `tsconfig.host.json`.
|
|
188
|
+
* @param policy - caller-owned type classifications and inherited Cordis data.
|
|
189
|
+
* @returns the configured projector and its validated catalog model.
|
|
190
|
+
*/
|
|
191
|
+
export function projectCordisCatalog(scanRoot, policy) {
|
|
192
|
+
const caches = new WorkspaceCaches();
|
|
193
|
+
const discovery = new WorkspaceAnalyzer({
|
|
194
|
+
root: scanRoot,
|
|
195
|
+
faces: ['host'],
|
|
196
|
+
checkDiagnostics: false,
|
|
197
|
+
caches,
|
|
198
|
+
}).discoverPackages();
|
|
199
|
+
const packages = discovery.filter(candidate => candidate.faces.includes('host'))
|
|
200
|
+
.map(candidate => candidate.package);
|
|
201
|
+
const workspace = new WorkspaceAnalyzer({
|
|
202
|
+
root: scanRoot,
|
|
203
|
+
faces: ['host'],
|
|
204
|
+
packages,
|
|
205
|
+
checkDiagnostics: false,
|
|
206
|
+
caches,
|
|
207
|
+
}).analyzeInBatches();
|
|
208
|
+
const face = workspace.faces.find(candidate => candidate.face === 'host');
|
|
209
|
+
if (face === undefined)
|
|
210
|
+
throw new Error('gen-cordis-catalog: Typert produced no host face');
|
|
211
|
+
const sourceDeclarations = new WorkspaceAnalyzer({
|
|
212
|
+
root: scanRoot,
|
|
213
|
+
faces: ['host'],
|
|
214
|
+
checkDiagnostics: false,
|
|
215
|
+
caches,
|
|
216
|
+
}).indexSourceDeclarations();
|
|
217
|
+
const projector = new CordisCatalogProjector(face, sourceDeclarations, policy);
|
|
218
|
+
return { projector, model: projector.project() };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Collect all modeled events for relationship-document consumers.
|
|
222
|
+
* @param scanRoot - workspace root containing `tsconfig.host.json`.
|
|
223
|
+
* @param policy - caller-owned Cordis catalog policy.
|
|
224
|
+
* @returns all validated event entries.
|
|
225
|
+
*/
|
|
226
|
+
export function collectEvents(scanRoot, policy) {
|
|
227
|
+
return [...projectCordisCatalog(scanRoot, policy).model.events];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Collect all modeled services for relationship-document consumers.
|
|
231
|
+
* @param scanRoot - workspace root containing `tsconfig.host.json`.
|
|
232
|
+
* @param policy - caller-owned Cordis catalog policy.
|
|
233
|
+
* @returns all validated service entries.
|
|
234
|
+
*/
|
|
235
|
+
export function collectServices(scanRoot, policy) {
|
|
236
|
+
return [...projectCordisCatalog(scanRoot, policy).model.services];
|
|
237
|
+
}
|
|
238
|
+
function parseJsDoc(raw) {
|
|
239
|
+
const lines = raw
|
|
240
|
+
.replace(/^\/\*\*/, '')
|
|
241
|
+
.replace(/\*\/$/, '')
|
|
242
|
+
.split('\n')
|
|
243
|
+
.map(line => line.replace(/^\s*\*?\s?/, '').replace(/\s+$/, ''));
|
|
244
|
+
const blocks = [];
|
|
245
|
+
let paragraph = [];
|
|
246
|
+
let list = [];
|
|
247
|
+
let item = [];
|
|
248
|
+
let inTags = false;
|
|
249
|
+
const join = (parts) => parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
250
|
+
const flushItem = () => {
|
|
251
|
+
if (item.length > 0)
|
|
252
|
+
list.push(join(item));
|
|
253
|
+
item = [];
|
|
254
|
+
};
|
|
255
|
+
const flushList = () => {
|
|
256
|
+
flushItem();
|
|
257
|
+
if (list.length > 0)
|
|
258
|
+
blocks.push(list.join('\n'));
|
|
259
|
+
list = [];
|
|
260
|
+
};
|
|
261
|
+
const flushParagraph = () => {
|
|
262
|
+
flushList();
|
|
263
|
+
if (paragraph.length > 0)
|
|
264
|
+
blocks.push(join(paragraph));
|
|
265
|
+
paragraph = [];
|
|
266
|
+
};
|
|
267
|
+
for (const line of lines) {
|
|
268
|
+
const tagLine = line.trimStart();
|
|
269
|
+
if (tagLine.startsWith('@')) {
|
|
270
|
+
flushParagraph();
|
|
271
|
+
inTags = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (inTags)
|
|
275
|
+
continue;
|
|
276
|
+
if (line.trim() === '') {
|
|
277
|
+
flushParagraph();
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (/^-\s+/.test(line)) {
|
|
281
|
+
flushItem();
|
|
282
|
+
if (paragraph.length > 0) {
|
|
283
|
+
blocks.push(join(paragraph));
|
|
284
|
+
paragraph = [];
|
|
285
|
+
}
|
|
286
|
+
item.push(line);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (item.length > 0)
|
|
290
|
+
item.push(line);
|
|
291
|
+
else
|
|
292
|
+
paragraph.push(line);
|
|
293
|
+
}
|
|
294
|
+
flushParagraph();
|
|
295
|
+
const params = new Map();
|
|
296
|
+
let returns = null;
|
|
297
|
+
let sink;
|
|
298
|
+
for (const line of lines) {
|
|
299
|
+
const param = /^@param\s+(\[?[\w$]+\]?)\s*(?:[-—–]\s*)?(.*)$/.exec(line);
|
|
300
|
+
if (param !== null) {
|
|
301
|
+
const name = (param[1] ?? '').replace(/^\[|\]$/g, '');
|
|
302
|
+
let value = param[2] ?? '';
|
|
303
|
+
params.set(name, value);
|
|
304
|
+
sink = (text) => {
|
|
305
|
+
value = value === '' ? text : `${value} ${text}`;
|
|
306
|
+
params.set(name, value);
|
|
307
|
+
};
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const returnsTag = /^@returns?(?:\s+[-—–]?\s*(.*))?$/.exec(line);
|
|
311
|
+
if (returnsTag !== null) {
|
|
312
|
+
let value = returnsTag[1] ?? '';
|
|
313
|
+
returns = value;
|
|
314
|
+
sink = (text) => {
|
|
315
|
+
value = value === '' ? text : `${value} ${text}`;
|
|
316
|
+
returns = value;
|
|
317
|
+
};
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (line.startsWith('@') || line.trim() === '')
|
|
321
|
+
sink = undefined;
|
|
322
|
+
else
|
|
323
|
+
sink?.(line.trim());
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
doc: blocks.join('\n\n').replace(/\{@link\s+([^}]+)\}/g, '$1').trim(),
|
|
327
|
+
params,
|
|
328
|
+
returns,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function checkParams(where, surface, parameters, tags, isExempt, violations) {
|
|
332
|
+
for (const parameter of parameters) {
|
|
333
|
+
if (parameter.binding !== 'identifier') {
|
|
334
|
+
violations.push(`${where}: parameter '${parameter.name}' is a binding pattern; the ${surface} surface needs simple identifier parameters so @param can name them.`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (isExempt(parameter))
|
|
338
|
+
continue;
|
|
339
|
+
const description = tags.get(parameter.name);
|
|
340
|
+
if (description === undefined)
|
|
341
|
+
violations.push(`${where} is missing @param ${parameter.name}.`);
|
|
342
|
+
else if (description.trim() === '')
|
|
343
|
+
violations.push(`${where}: @param ${parameter.name} has an empty description.`);
|
|
344
|
+
}
|
|
345
|
+
for (const tag of tags.keys()) {
|
|
346
|
+
if (!parameters.some(parameter => parameter.binding === 'identifier' && parameter.name === tag)) {
|
|
347
|
+
violations.push(`${where}: @param ${tag} does not match any parameter (stale tag?).`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function checkReturns(where, signature, returns, renderer, violations) {
|
|
352
|
+
const type = renderer.renderType(signature.returns);
|
|
353
|
+
if (type === 'void' || type === 'Promise<void>')
|
|
354
|
+
return;
|
|
355
|
+
if (returns === null)
|
|
356
|
+
violations.push(`${where} is missing @returns (return type: ${type}).`);
|
|
357
|
+
else if (returns.trim() === '')
|
|
358
|
+
violations.push(`${where}: @returns has an empty description.`);
|
|
359
|
+
}
|
|
360
|
+
function reportViolations(gate, violations) {
|
|
361
|
+
if (violations.length === 0)
|
|
362
|
+
return;
|
|
363
|
+
throw new Error(`${gate}: ${String(violations.length)} JSDoc completeness violation(s) (see AGENTS.md):\n`
|
|
364
|
+
+ violations.map(violation => ` ${violation}`).join('\n'));
|
|
365
|
+
}
|
|
366
|
+
function pointer(location) {
|
|
367
|
+
return `${location.file}:${String(location.line)}`;
|
|
368
|
+
}
|
|
369
|
+
function isMode(mode) {
|
|
370
|
+
return mode === 'emit' || mode === 'waterfall' || mode === 'parallel' || mode === 'serial';
|
|
371
|
+
}
|
|
372
|
+
function signatureTypeNames(renderer, signature) {
|
|
373
|
+
const names = new Set();
|
|
374
|
+
const visited = new Set();
|
|
375
|
+
const visitSignature = (current) => {
|
|
376
|
+
for (const parameter of current.typeParameters) {
|
|
377
|
+
if (parameter.constraint !== undefined)
|
|
378
|
+
visit(parameter.constraint);
|
|
379
|
+
if (parameter.default !== undefined)
|
|
380
|
+
visit(parameter.default);
|
|
381
|
+
}
|
|
382
|
+
for (const parameter of current.parameters)
|
|
383
|
+
visit(parameter.type);
|
|
384
|
+
visit(current.returns);
|
|
385
|
+
};
|
|
386
|
+
const visitMember = (member) => {
|
|
387
|
+
if (member.kind === 'property')
|
|
388
|
+
visit(member.type);
|
|
389
|
+
else
|
|
390
|
+
visitSignature(member.signature);
|
|
391
|
+
};
|
|
392
|
+
const visit = (id) => {
|
|
393
|
+
if (visited.has(id))
|
|
394
|
+
return;
|
|
395
|
+
visited.add(id);
|
|
396
|
+
const node = renderer.node(id);
|
|
397
|
+
if (node.kind === 'reference' && node.target.kind !== 'type-parameter')
|
|
398
|
+
names.add(node.name);
|
|
399
|
+
if (node.kind === 'type-query')
|
|
400
|
+
names.add(node.expression);
|
|
401
|
+
for (const child of childTypeNodeIds(node))
|
|
402
|
+
visit(child);
|
|
403
|
+
if (node.kind === 'object')
|
|
404
|
+
for (const member of node.members)
|
|
405
|
+
visitMember(member);
|
|
406
|
+
if (node.kind === 'function' || node.kind === 'constructor')
|
|
407
|
+
visitSignature(node.signature);
|
|
408
|
+
};
|
|
409
|
+
visitSignature(signature);
|
|
410
|
+
return [...names].sort();
|
|
411
|
+
}
|
|
412
|
+
/** Declarations longer than this render as a truncated stub. */
|
|
413
|
+
const MAX_DECL_CHARS = 1500;
|
|
414
|
+
/** Render one value as a single-quoted TypeScript literal. */
|
|
415
|
+
function quote(value) {
|
|
416
|
+
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'").replaceAll('\n', '\\n')}'`;
|
|
417
|
+
}
|
|
418
|
+
/** Resolve and sort the word-bounded transitive type closure referenced by seed text. */
|
|
419
|
+
function referencedTypes(seeds, declarations) {
|
|
420
|
+
const included = new Map();
|
|
421
|
+
let frontier = [...seeds];
|
|
422
|
+
while (frontier.length > 0) {
|
|
423
|
+
const next = [];
|
|
424
|
+
for (const [name, declaration] of declarations) {
|
|
425
|
+
if (included.has(name))
|
|
426
|
+
continue;
|
|
427
|
+
const pattern = new RegExp(`\\b${name}\\b`);
|
|
428
|
+
if (frontier.some(text => pattern.test(text))) {
|
|
429
|
+
included.set(name, declaration);
|
|
430
|
+
next.push(declaration);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
frontier = next;
|
|
434
|
+
}
|
|
435
|
+
return [...included]
|
|
436
|
+
.map(([name, declaration]) => ({ name, declaration }))
|
|
437
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
438
|
+
}
|
|
439
|
+
function firstSentence(doc) {
|
|
440
|
+
const line = doc.split('\n', 1)[0] ?? '';
|
|
441
|
+
const match = /^(.*?[.!?])(?:\s|$)/.exec(line);
|
|
442
|
+
return (match?.[1] ?? line).trim();
|
|
443
|
+
}
|
|
444
|
+
/** Render the byte-compatible model-facing API catalog. */
|
|
445
|
+
function renderRuntimeApi(services, events, types, inheritedServices) {
|
|
446
|
+
const lines = [
|
|
447
|
+
'/**',
|
|
448
|
+
' * Generated by scripts/gen-cordis-api.ts — do not edit by hand; run',
|
|
449
|
+
' * `pnpm run gen-cordis-api` to regenerate (freshness-gated by',
|
|
450
|
+
' * `pnpm run verify-cordis-api` in doc-sync).',
|
|
451
|
+
' *',
|
|
452
|
+
' * The machine-readable cordis API catalog `cordis_inspect` serves to the',
|
|
453
|
+
' * model: harness services (summary + public method signatures/JSDoc),',
|
|
454
|
+
' * harness events (mode + signature/JSDoc), and the inherited `ctx` surface. Produced by',
|
|
455
|
+
' * the same AST walk as docs/cordis-catalog, so this data and the rendered',
|
|
456
|
+
' * docs cannot diverge.',
|
|
457
|
+
' *',
|
|
458
|
+
' * @module @deepseek-ai/dsh-tool-cordis/api-catalog',
|
|
459
|
+
' */',
|
|
460
|
+
'',
|
|
461
|
+
'/** One public service method and its source-owned contract. */',
|
|
462
|
+
'export interface ServiceApiMethod {',
|
|
463
|
+
' /** Public method signature with its body stripped. */',
|
|
464
|
+
' signature: string',
|
|
465
|
+
' /** Original method JSDoc, with only container indentation removed. */',
|
|
466
|
+
' jsDoc: string',
|
|
467
|
+
'}',
|
|
468
|
+
'',
|
|
469
|
+
'/** One harness `ctx.<key>` service: its one-line summary and public methods. */',
|
|
470
|
+
'export interface ServiceApiEntry {',
|
|
471
|
+
' /** The `ctx.<key>` name, e.g. `tools`. */',
|
|
472
|
+
' key: string',
|
|
473
|
+
' /** First sentence of the service class JSDoc. */',
|
|
474
|
+
' summary: string',
|
|
475
|
+
' /** Public methods, bodies stripped, in source order. */',
|
|
476
|
+
' methods: readonly ServiceApiMethod[]',
|
|
477
|
+
'}',
|
|
478
|
+
'',
|
|
479
|
+
'/** One harness event: its dispatch mode, exact signature, and one-line summary. */',
|
|
480
|
+
'export interface EventApiEntry {',
|
|
481
|
+
' /** The scoped event name, e.g. `agent/status`. */',
|
|
482
|
+
' name: string',
|
|
483
|
+
' /** The dispatch mode from the declaration\'s `@mode` tag. */',
|
|
484
|
+
' mode: string',
|
|
485
|
+
' /** The exact listener signature, whitespace-normalized. */',
|
|
486
|
+
' signature: string',
|
|
487
|
+
' /** Original event JSDoc, with only container indentation removed. */',
|
|
488
|
+
' jsDoc: string',
|
|
489
|
+
' /** First sentence of the event JSDoc. */',
|
|
490
|
+
' summary: string',
|
|
491
|
+
'}',
|
|
492
|
+
'',
|
|
493
|
+
'/** One inherited (cordis core + loader/hmr/timer) `ctx` member group with its summary. */',
|
|
494
|
+
'export interface InheritedApiEntry {',
|
|
495
|
+
' /** The `ctx` member name(s), e.g. `ctx.on / ctx.once`. */',
|
|
496
|
+
' name: string',
|
|
497
|
+
' /** One-line summary of what the member does. */',
|
|
498
|
+
' summary: string',
|
|
499
|
+
'}',
|
|
500
|
+
'',
|
|
501
|
+
'/** One named type shape the service signatures reference. */',
|
|
502
|
+
'export interface TypeApiEntry {',
|
|
503
|
+
' /** The exported type/interface name, e.g. `BashRunResult`. */',
|
|
504
|
+
' name: string',
|
|
505
|
+
' /** The full declaration text, comments stripped. */',
|
|
506
|
+
' declaration: string',
|
|
507
|
+
'}',
|
|
508
|
+
'',
|
|
509
|
+
'/** Every harness `ctx.<key>` service, sorted by key. */',
|
|
510
|
+
'export const SERVICE_API: readonly ServiceApiEntry[] = [',
|
|
511
|
+
];
|
|
512
|
+
for (const service of services) {
|
|
513
|
+
lines.push(' {');
|
|
514
|
+
lines.push(` key: ${quote(service.key)},`);
|
|
515
|
+
lines.push(` summary: ${quote(firstSentence(service.doc))},`);
|
|
516
|
+
if (service.methods.length === 0) {
|
|
517
|
+
lines.push(' methods: [],');
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
lines.push(' methods: [');
|
|
521
|
+
for (const method of service.methods) {
|
|
522
|
+
lines.push(' {');
|
|
523
|
+
lines.push(` signature: ${quote(method.signature)},`);
|
|
524
|
+
lines.push(` jsDoc: ${quote(method.jsDoc)},`);
|
|
525
|
+
lines.push(' },');
|
|
526
|
+
}
|
|
527
|
+
lines.push(' ],');
|
|
528
|
+
}
|
|
529
|
+
lines.push(' },');
|
|
530
|
+
}
|
|
531
|
+
lines.push(']', '', '/** Every harness event, sorted by name. */', 'export const EVENT_API: readonly EventApiEntry[] = [');
|
|
532
|
+
for (const event of [...events].sort((left, right) => left.name.localeCompare(right.name))) {
|
|
533
|
+
lines.push(' {');
|
|
534
|
+
lines.push(` name: ${quote(event.name)},`);
|
|
535
|
+
lines.push(` mode: ${quote(event.mode)},`);
|
|
536
|
+
lines.push(` signature: ${quote(event.signature)},`);
|
|
537
|
+
lines.push(` jsDoc: ${quote(event.jsDoc)},`);
|
|
538
|
+
lines.push(` summary: ${quote(firstSentence(event.doc))},`);
|
|
539
|
+
lines.push(' },');
|
|
540
|
+
}
|
|
541
|
+
lines.push(']', '', '/** Shapes of every exported type the SERVICE_API signatures reference (transitively), sorted by name. */', 'export const TYPE_API: readonly TypeApiEntry[] = [');
|
|
542
|
+
for (const type of types) {
|
|
543
|
+
lines.push(' {');
|
|
544
|
+
lines.push(` name: ${quote(type.name)},`);
|
|
545
|
+
lines.push(` declaration: ${quote(type.declaration)},`);
|
|
546
|
+
lines.push(' },');
|
|
547
|
+
}
|
|
548
|
+
lines.push(']', '', '/** The inherited `ctx` surface (cordis core + loader/hmr/timer), in curated order. */', 'export const INHERITED_CTX_API: readonly InheritedApiEntry[] = [');
|
|
549
|
+
for (const inherited of inheritedServices) {
|
|
550
|
+
lines.push(` { name: ${quote(inherited.name)}, summary: ${quote(inherited.summary)} },`);
|
|
551
|
+
}
|
|
552
|
+
lines.push(']', '');
|
|
553
|
+
return lines.join('\n');
|
|
554
|
+
}
|
|
555
|
+
/** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */
|
|
556
|
+
export const REGION_BEGIN = '<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->';
|
|
557
|
+
/** Closing region delimiter matching {@link REGION_BEGIN}. */
|
|
558
|
+
export const REGION_END = '<!-- END GENERATED cordis-surface -->';
|
|
559
|
+
/**
|
|
560
|
+
* Render the cross-link "Types:" line for a signature relative to one
|
|
561
|
+
* subsystems page, or '' if none apply. A type whose primary page IS the
|
|
562
|
+
* rendering page would link as a fragmentless self-link readers already sit
|
|
563
|
+
* on, so it is dropped instead.
|
|
564
|
+
*/
|
|
565
|
+
function typeLinks(signature, onPage, linkedTypePages) {
|
|
566
|
+
const seen = new Set();
|
|
567
|
+
for (const name of Object.keys(linkedTypePages)) {
|
|
568
|
+
if (new RegExp(`\\b${name}\\b`).test(signature))
|
|
569
|
+
seen.add(name);
|
|
570
|
+
}
|
|
571
|
+
const links = [...seen].sort()
|
|
572
|
+
.filter(name => linkedTypePages[name] !== onPage)
|
|
573
|
+
.map(name => `[${name}](${linkedTypePages[name]})`);
|
|
574
|
+
if (links.length === 0)
|
|
575
|
+
return '';
|
|
576
|
+
return `Types: ${links.join(' · ')}`;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* GitHub's heading-slug algorithm (lowercase; drop everything but letters,
|
|
580
|
+
* numbers, spaces, hyphens; spaces become hyphens). Region headings carry
|
|
581
|
+
* backticks and em-dashes, which VitePress slugifies differently, so each
|
|
582
|
+
* generated heading is preceded by an explicit `<a id>` carrying this slug —
|
|
583
|
+
* the historical flat-catalog anchor — making `#ctx<key>--<class>` fragments
|
|
584
|
+
* resolve identically on GitHub and the published site.
|
|
585
|
+
*/
|
|
586
|
+
function githubSlug(heading) {
|
|
587
|
+
return heading.toLowerCase().replace(/[^\p{L}\p{N} -]/gu, '').replaceAll(' ', '-');
|
|
588
|
+
}
|
|
589
|
+
/** The explicit-anchor line emitted before one generated heading. */
|
|
590
|
+
function anchorFor(headingText) {
|
|
591
|
+
return [`<a id="${githubSlug(headingText)}"></a>`, ''];
|
|
592
|
+
}
|
|
593
|
+
/** Render one harness event entry onto its owning page, nested under its scope heading. */
|
|
594
|
+
function renderEvent(e, onPage, linkedTypePages) {
|
|
595
|
+
const out = [...anchorFor(`${e.name} — ${e.mode}`), `#### \`${e.name}\` — ${e.mode}`, ''];
|
|
596
|
+
if (e.doc)
|
|
597
|
+
out.push(e.doc, '');
|
|
598
|
+
out.push('```' + FENCE, e.jsDoc, e.signature, '```', '');
|
|
599
|
+
const links = typeLinks(e.signature, onPage, linkedTypePages);
|
|
600
|
+
if (links)
|
|
601
|
+
out.push(links, '');
|
|
602
|
+
out.push(`Source: [\`${e.source}\`](../../${e.source.split(':')[0]})`, '');
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
/** Render one harness service entry onto its owning page. */
|
|
606
|
+
function renderService(s, onPage, linkedTypePages) {
|
|
607
|
+
const kind = s.abstract ? ' (abstract seam)' : '';
|
|
608
|
+
const out = [...anchorFor(`ctx.${s.key} — ${s.type}${kind}`), `### \`ctx.${s.key}\` — \`${s.type}\`${kind}`, ''];
|
|
609
|
+
if (s.doc)
|
|
610
|
+
out.push(s.doc, '');
|
|
611
|
+
if (s.methods.length) {
|
|
612
|
+
const declarations = s.methods.flatMap((method, index) => [
|
|
613
|
+
...(index > 0 ? [''] : []),
|
|
614
|
+
method.jsDoc,
|
|
615
|
+
method.signature,
|
|
616
|
+
]);
|
|
617
|
+
out.push('```' + FENCE, ...declarations, '```', '');
|
|
618
|
+
const links = typeLinks(s.methods.map(method => method.signature).join('\n'), onPage, linkedTypePages);
|
|
619
|
+
if (links)
|
|
620
|
+
out.push(links, '');
|
|
621
|
+
}
|
|
622
|
+
out.push(`Source: [\`${s.source}\`](../../${s.source.split(':')[0]})`, '');
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
/** The shared generated-file banner comment. */
|
|
626
|
+
const BANNER = [
|
|
627
|
+
'<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.',
|
|
628
|
+
' Run `pnpm run gen-cordis-catalog` to regenerate. -->',
|
|
629
|
+
'',
|
|
630
|
+
];
|
|
631
|
+
/** The shared GENERATED + freshness-gate + fence notice paragraph. */
|
|
632
|
+
const GATE_NOTICE = 'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.';
|
|
633
|
+
/**
|
|
634
|
+
* Render one page's generated `cordis-surface` region: the services mapped to
|
|
635
|
+
* the page, then the event scopes mapped to it, markers included. Pure and
|
|
636
|
+
* deterministic given sorted inputs; identical bytes land in both pair sides.
|
|
637
|
+
* @param page - the owning `docs/subsystems/` page basename, e.g. `core.md`.
|
|
638
|
+
* @param services - validated services mapped to this page.
|
|
639
|
+
* @param events - validated events whose scopes map to this page.
|
|
640
|
+
* @param policy - type links supplied by the caller.
|
|
641
|
+
* @returns the complete marker-delimited region text.
|
|
642
|
+
*/
|
|
643
|
+
export function renderPageRegion(page, services, events, policy) {
|
|
644
|
+
const lines = [
|
|
645
|
+
REGION_BEGIN,
|
|
646
|
+
'',
|
|
647
|
+
'<a id="cordis-surface"></a>',
|
|
648
|
+
'',
|
|
649
|
+
'## Cordis surface',
|
|
650
|
+
'',
|
|
651
|
+
'Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).',
|
|
652
|
+
'',
|
|
653
|
+
];
|
|
654
|
+
for (const s of services)
|
|
655
|
+
lines.push(...renderService(s, page, policy.linkedTypePages));
|
|
656
|
+
const scopes = [...new Set(events.map(e => e.scope))].sort();
|
|
657
|
+
for (const scope of scopes) {
|
|
658
|
+
lines.push(...anchorFor(`${scope}/* events`), `### \`${scope}/*\` events`, '');
|
|
659
|
+
for (const e of events.filter(x => x.scope === scope).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
660
|
+
lines.push(...renderEvent(e, page, policy.linkedTypePages));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
while (lines.at(-1) === '')
|
|
664
|
+
lines.pop();
|
|
665
|
+
lines.push(REGION_END);
|
|
666
|
+
return lines.join('\n');
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Render the inherited (pinned vendor) tier as its own generated page.
|
|
670
|
+
* @param policy - inherited events and services supplied by the caller.
|
|
671
|
+
* @returns the complete generated Markdown document.
|
|
672
|
+
*/
|
|
673
|
+
export function renderInheritedPage(policy) {
|
|
674
|
+
const lines = [
|
|
675
|
+
...BANNER,
|
|
676
|
+
'# Inherited Cordis Surface',
|
|
677
|
+
'',
|
|
678
|
+
'The framework `ctx` members and events every plugin sees beyond the harness tier — pinned vendor source ([vendoring policy](../../vendor/README.md)), summarized tersely so the harness pages stay focused on repository-owned vocabulary. Detailed Context, Fiber, Registry, and Service APIs are generated in [context.md](context.md), [fiber.md](fiber.md), [registry.md](registry.md), and [service.md](service.md); the event-dispatch methods in [events.md](events.md).',
|
|
679
|
+
'',
|
|
680
|
+
GATE_NOTICE,
|
|
681
|
+
'',
|
|
682
|
+
'## Inherited `ctx` members (cordis core + loader/hmr/timer)',
|
|
683
|
+
'',
|
|
684
|
+
];
|
|
685
|
+
for (const s of policy.inheritedServices) {
|
|
686
|
+
lines.push(`- \`${s.name}\` — ${s.summary} ([\`${s.source}\`](../../${s.source.split(':')[0]}))`);
|
|
687
|
+
}
|
|
688
|
+
lines.push('', '## Inherited events (cordis core + loader/hmr/timer)', '');
|
|
689
|
+
for (const e of policy.inheritedEvents) {
|
|
690
|
+
lines.push(`- \`${e.name}\` — ${e.summary} ([\`${e.source}\`](../../${e.source.split(':')[0]}))`);
|
|
691
|
+
}
|
|
692
|
+
lines.push('');
|
|
693
|
+
return lines.join('\n');
|
|
694
|
+
}
|
|
695
|
+
//# sourceMappingURL=cordis-catalog.js.map
|