@vmz/vmz 0.0.1 → 0.0.2

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 (75) hide show
  1. package/README.md +48 -2
  2. package/bin/vmz.js +4 -0
  3. package/dist/application-cmd.d.ts +22 -0
  4. package/dist/application-cmd.js +348 -0
  5. package/dist/bundler-adapter.d.ts +64 -0
  6. package/dist/bundler-adapter.js +111 -0
  7. package/dist/cli.d.ts +15 -0
  8. package/dist/cli.js +370 -0
  9. package/dist/dev-session.d.ts +35 -0
  10. package/dist/dev-session.js +290 -0
  11. package/dist/document-build.d.ts +99 -0
  12. package/dist/document-build.js +274 -0
  13. package/dist/document-check.d.ts +44 -0
  14. package/dist/document-check.js +246 -0
  15. package/dist/document-cmd.d.ts +9 -0
  16. package/dist/document-cmd.js +147 -0
  17. package/dist/document-designs.d.ts +9 -0
  18. package/dist/document-designs.js +126 -0
  19. package/dist/document-enrich.d.ts +23 -0
  20. package/dist/document-enrich.js +234 -0
  21. package/dist/document-evidence.d.ts +49 -0
  22. package/dist/document-evidence.js +501 -0
  23. package/dist/document-integrate.d.ts +35 -0
  24. package/dist/document-integrate.js +89 -0
  25. package/dist/document-interactive.d.ts +69 -0
  26. package/dist/document-interactive.js +255 -0
  27. package/dist/document-locale.d.ts +31 -0
  28. package/dist/document-locale.js +59 -0
  29. package/dist/document-markdown.d.ts +13 -0
  30. package/dist/document-markdown.js +39 -0
  31. package/dist/document-scan.d.ts +21 -0
  32. package/dist/document-scan.js +151 -0
  33. package/dist/document-schema.d.ts +87 -0
  34. package/dist/document-schema.js +88 -0
  35. package/dist/explain-cmd.d.ts +5 -0
  36. package/dist/explain-cmd.js +123 -0
  37. package/dist/index.d.ts +808 -0
  38. package/dist/index.js +569 -0
  39. package/dist/locale-check.d.ts +106 -0
  40. package/dist/locale-check.js +737 -0
  41. package/dist/locale-cmd.d.ts +5 -0
  42. package/dist/locale-cmd.js +443 -0
  43. package/dist/locale-delivery.d.ts +298 -0
  44. package/dist/locale-delivery.js +444 -0
  45. package/dist/locale-router.d.ts +207 -0
  46. package/dist/locale-router.js +508 -0
  47. package/dist/locale-runtime.d.ts +406 -0
  48. package/dist/locale-runtime.js +542 -0
  49. package/dist/locale-schema.d.ts +9 -0
  50. package/dist/locale-schema.js +10 -0
  51. package/dist/locale-tooling.d.ts +118 -0
  52. package/dist/locale-tooling.js +358 -0
  53. package/dist/log.d.ts +19 -0
  54. package/dist/log.js +42 -0
  55. package/dist/packages.d.ts +27 -0
  56. package/dist/packages.js +147 -0
  57. package/dist/plugin-host.d.ts +30 -0
  58. package/dist/plugin-host.js +370 -0
  59. package/dist/refactor-cmd.d.ts +8 -0
  60. package/dist/refactor-cmd.js +156 -0
  61. package/dist/resolve.d.ts +25 -0
  62. package/dist/resolve.js +56 -0
  63. package/dist/test-cmd.d.ts +9 -0
  64. package/dist/test-cmd.js +343 -0
  65. package/dist/test-compile.d.ts +2 -0
  66. package/dist/test-compile.js +3 -0
  67. package/dist/test-discover.d.ts +2 -0
  68. package/dist/test-discover.js +3 -0
  69. package/dist/test-logic.d.ts +2 -0
  70. package/dist/test-logic.js +3 -0
  71. package/dist/test-protocol.d.ts +2 -0
  72. package/dist/test-protocol.js +3 -0
  73. package/dist/watch-diff.d.ts +17 -0
  74. package/dist/watch-diff.js +56 -0
  75. package/package.json +81 -3
@@ -0,0 +1,501 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Document D2 Evidence — fence check + API refs from Program Graph.
4
+ * Design: 规划设计/vmz/19 §4 · §8 D2
5
+ *
6
+ * Not a Doc IR: filesystem/manifest projection + Workspace/Program Graph queries.
7
+ */
8
+ import { createRequire } from 'node:module';
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { DIAG, DOCUMENT_EVIDENCE_SCHEMA } from './document-schema.js';
13
+ const require = createRequire(import.meta.url);
14
+ /**
15
+ * @param {string} info
16
+ * @returns {{ lang: string, run: string | null, source: string | null, playground: boolean }}
17
+ */
18
+ export function parseFenceInfo(info) {
19
+ const parts = String(info || '')
20
+ .trim()
21
+ .split(/\s+/)
22
+ .filter(Boolean);
23
+ const lang = (parts[0] || '').toLowerCase();
24
+ /** @type {string | null} */
25
+ let run = null;
26
+ /** @type {string | null} */
27
+ let source = null;
28
+ let playground = false;
29
+ for (const p of parts.slice(1)) {
30
+ if (p === 'run')
31
+ run = 'compile';
32
+ else if (p.startsWith('run='))
33
+ run = p.slice(4) || 'compile';
34
+ else if (p.startsWith('source='))
35
+ source = p.slice(7).replace(/^["']|["']$/g, '');
36
+ else if (p === 'playground')
37
+ playground = true;
38
+ }
39
+ return { lang, run, source, playground };
40
+ }
41
+ /**
42
+ * @param {string} href
43
+ * @returns {string | null} symbol query (chunkId or name)
44
+ */
45
+ export function parseApiHref(href) {
46
+ const h = String(href || '').trim();
47
+ if (h.startsWith('vmz-api:'))
48
+ return h.slice('vmz-api:'.length).replace(/^\/+/, '');
49
+ if (h.startsWith('api:'))
50
+ return h.slice('api:'.length).replace(/^\/+/, '');
51
+ return null;
52
+ }
53
+ /**
54
+ * @param {string} projectRoot
55
+ * @returns {Array<{ chunkId: string, name: string, path: string, capabilities: string[], programPath: string }>}
56
+ */
57
+ export function loadProgramApiIndex(projectRoot) {
58
+ const outDir = path.join(projectRoot, 'dist');
59
+ /** @type {Array<{ chunkId: string, name: string, path: string, capabilities: string[], programPath: string }>} */
60
+ const rows = [];
61
+ if (!fs.existsSync(outDir))
62
+ return rows;
63
+ walkFiles(outDir, (file) => {
64
+ if (!file.endsWith('.program.json'))
65
+ return;
66
+ let root;
67
+ try {
68
+ root = JSON.parse(fs.readFileSync(file, 'utf8'));
69
+ }
70
+ catch {
71
+ return;
72
+ }
73
+ const units = Array.isArray(root.units) ? root.units : [];
74
+ for (const unit of units) {
75
+ const chunkId = unit?.deployment?.chunkId || unit?.name || path.basename(file, '.program.json');
76
+ const name = unit?.name || chunkId;
77
+ const caps = [];
78
+ const list = unit?.server?.capabilities;
79
+ if (Array.isArray(list)) {
80
+ for (const c of list) {
81
+ if (c?.method)
82
+ caps.push(String(c.method));
83
+ }
84
+ }
85
+ rows.push({
86
+ chunkId: String(chunkId),
87
+ name: String(name),
88
+ path: String(root.source || file),
89
+ capabilities: caps,
90
+ programPath: file,
91
+ });
92
+ }
93
+ });
94
+ return rows;
95
+ }
96
+ /**
97
+ * Resolve API symbol against Program Graph index.
98
+ * @param {ReturnType<typeof loadProgramApiIndex>} index
99
+ * @param {string} query
100
+ */
101
+ export function resolveApiSymbol(index, query) {
102
+ const q = String(query || '').trim();
103
+ if (!q)
104
+ return { status: 'missing', matches: [] };
105
+ const exact = index.filter((r) => r.chunkId === q || r.name === q);
106
+ if (exact.length === 1)
107
+ return { status: 'ok', matches: exact };
108
+ if (exact.length > 1)
109
+ return { status: 'ambiguous', matches: exact };
110
+ const loose = index.filter((r) => r.chunkId.endsWith(`/${q}`) || r.chunkId.endsWith(q) || r.name.toLowerCase() === q.toLowerCase());
111
+ if (loose.length === 1)
112
+ return { status: 'ok', matches: loose };
113
+ if (loose.length > 1)
114
+ return { status: 'ambiguous', matches: loose };
115
+ return { status: 'missing', matches: [] };
116
+ }
117
+ /**
118
+ * @param {import('./document-schema.js').DocumentManifest} manifest
119
+ * @param {{
120
+ * analyzeMarkdown: Function,
121
+ * projectRoot: string,
122
+ * createWorkspace?: Function,
123
+ * ensureProgramGraph?: boolean,
124
+ * }} ctx
125
+ */
126
+ export async function enrichDocumentEvidence(manifest, ctx) {
127
+ const projectRoot = path.resolve(ctx.projectRoot || manifest.root);
128
+ /** @type {import('./document-schema.js').DocumentDiagnostic[]} */
129
+ const diagnostics = [...(manifest.diagnostics || [])];
130
+ /** @type {any[]} */
131
+ const fenceRecords = [];
132
+ /** @type {any[]} */
133
+ const apiRefs = [];
134
+ /** @type {any[]} */
135
+ const testSelections = [];
136
+ // Collect fences + api links from pages.
137
+ /** @type {Array<{ page: any, fences: any[], apiQueries: string[], sourcePath: string }>} */
138
+ const pages = [];
139
+ for (const page of manifest.pages) {
140
+ const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
141
+ const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
142
+ const analyzed = ctx.analyzeMarkdown(source);
143
+ const fences = Array.isArray(analyzed.fences) ? analyzed.fences : [];
144
+ const apiQueries = [];
145
+ for (const link of analyzed.links || []) {
146
+ const q = parseApiHref(link.href);
147
+ if (q)
148
+ apiQueries.push(q);
149
+ }
150
+ pages.push({ page, fences, apiQueries, sourcePath: page.sourcePath });
151
+ }
152
+ const needsGraph = pages.some((p) => p.apiQueries.length > 0) || pages.some((p) => p.fences.some((f) => parseFenceInfo(f.info).lang === 'vmz'));
153
+ if (needsGraph && ctx.ensureProgramGraph !== false && typeof ctx.createWorkspace === 'function') {
154
+ try {
155
+ await ensureSrcProgramGraph(projectRoot, ctx.createWorkspace, diagnostics);
156
+ }
157
+ catch (e) {
158
+ diagnostics.push({
159
+ code: DIAG.FENCE_CHECK,
160
+ severity: 'error',
161
+ message: `project build for evidence failed: ${e.message || e}`,
162
+ path: projectRoot,
163
+ });
164
+ }
165
+ }
166
+ const apiIndex = loadProgramApiIndex(projectRoot);
167
+ for (const { page, fences, apiQueries, sourcePath } of pages) {
168
+ for (const fence of fences) {
169
+ const meta = parseFenceInfo(fence.info);
170
+ const rec = {
171
+ lang: meta.lang,
172
+ info: fence.info,
173
+ lineStart: fence.lineStart,
174
+ lineEnd: fence.lineEnd,
175
+ pageKey: page.identity.pageKey,
176
+ locale: page.identity.locale,
177
+ path: sourcePath,
178
+ run: meta.run,
179
+ source: meta.source,
180
+ playground: meta.playground,
181
+ status: 'skipped',
182
+ };
183
+ if (!meta.lang ||
184
+ meta.lang === 'text' ||
185
+ meta.lang === 'md' ||
186
+ meta.lang === 'bash' ||
187
+ meta.lang === 'sh' ||
188
+ meta.lang === 'shell' ||
189
+ meta.lang === 'json' ||
190
+ meta.lang === 'css' ||
191
+ meta.lang === 'html') {
192
+ rec.status = 'highlight';
193
+ fenceRecords.push(rec);
194
+ continue;
195
+ }
196
+ if (meta.lang === 'vmz') {
197
+ const result = await checkVmzFence({
198
+ projectRoot,
199
+ fence,
200
+ meta,
201
+ createWorkspace: ctx.createWorkspace,
202
+ sourcePath,
203
+ });
204
+ Object.assign(rec, result.record);
205
+ diagnostics.push(...result.diagnostics);
206
+ if (result.testSelection)
207
+ testSelections.push(result.testSelection);
208
+ fenceRecords.push(rec);
209
+ continue;
210
+ }
211
+ if (meta.lang === 'ts' || meta.lang === 'typescript' || meta.lang === 'js' || meta.lang === 'javascript') {
212
+ const result = checkScriptFence({ fence, meta, sourcePath, page });
213
+ Object.assign(rec, result.record);
214
+ diagnostics.push(...result.diagnostics);
215
+ fenceRecords.push(rec);
216
+ continue;
217
+ }
218
+ rec.status = 'unsupported';
219
+ diagnostics.push({
220
+ code: DIAG.FENCE_UNSUPPORTED,
221
+ severity: 'warning',
222
+ message: `fence lang \`${meta.lang}\` is highlight-only (no sandbox contribution)`,
223
+ path: `${sourcePath}:${fence.lineStart}`,
224
+ });
225
+ fenceRecords.push(rec);
226
+ }
227
+ for (const query of apiQueries) {
228
+ const resolved = resolveApiSymbol(apiIndex, query);
229
+ const ref = {
230
+ query,
231
+ pageKey: page.identity.pageKey,
232
+ locale: page.identity.locale,
233
+ path: sourcePath,
234
+ status: resolved.status,
235
+ matches: resolved.matches.map((m) => ({
236
+ chunkId: m.chunkId,
237
+ name: m.name,
238
+ source: m.path,
239
+ capabilities: m.capabilities,
240
+ stableId: { kind: 'chunk', id: m.chunkId },
241
+ })),
242
+ };
243
+ if (resolved.status === 'missing') {
244
+ diagnostics.push({
245
+ code: DIAG.API_MISSING,
246
+ severity: 'error',
247
+ message: `API symbol not found in Program Graph: ${query}`,
248
+ path: sourcePath,
249
+ });
250
+ }
251
+ else if (resolved.status === 'ambiguous') {
252
+ diagnostics.push({
253
+ code: DIAG.API_AMBIGUOUS,
254
+ severity: 'error',
255
+ message: `API symbol ambiguous (${resolved.matches.map((m) => m.chunkId).join(', ')}): ${query}`,
256
+ path: sourcePath,
257
+ });
258
+ }
259
+ apiRefs.push(ref);
260
+ }
261
+ }
262
+ const hasErrors = diagnostics.some((d) => d.severity === 'error');
263
+ const evidence = {
264
+ schema: DOCUMENT_EVIDENCE_SCHEMA,
265
+ fences: fenceRecords,
266
+ apiRefs,
267
+ testSelections,
268
+ status: hasErrors ? 'failed' : fenceRecords.length || apiRefs.length ? 'ready' : 'empty',
269
+ };
270
+ return { diagnostics, evidence };
271
+ }
272
+ /**
273
+ * @param {{ projectRoot: string, fence: any, meta: any, createWorkspace?: Function, sourcePath: string }} opts
274
+ */
275
+ async function checkVmzFence(opts) {
276
+ /** @type {import('./document-schema.js').DocumentDiagnostic[]} */
277
+ const diagnostics = [];
278
+ const { fence, meta, projectRoot, sourcePath } = opts;
279
+ let body = fence.content;
280
+ let label = `inline@${sourcePath}:${fence.lineStart}`;
281
+ if (meta.source) {
282
+ const abs = path.isAbsolute(meta.source) ? meta.source : path.join(projectRoot, meta.source);
283
+ if (!fs.existsSync(abs)) {
284
+ diagnostics.push({
285
+ code: DIAG.FENCE_SOURCE_MISSING,
286
+ severity: 'error',
287
+ message: `fence source missing: ${meta.source}`,
288
+ path: `${sourcePath}:${fence.lineStart}`,
289
+ });
290
+ return {
291
+ record: { status: 'failed', detail: 'source_missing' },
292
+ diagnostics,
293
+ testSelection: null,
294
+ };
295
+ }
296
+ body = fs.readFileSync(abs, 'utf8');
297
+ label = meta.source;
298
+ }
299
+ if (typeof opts.createWorkspace !== 'function') {
300
+ diagnostics.push({
301
+ code: DIAG.FENCE_CHECK,
302
+ severity: 'error',
303
+ message: 'createWorkspace unavailable for vmz fence check',
304
+ path: `${sourcePath}:${fence.lineStart}`,
305
+ });
306
+ return { record: { status: 'failed' }, diagnostics, testSelection: null };
307
+ }
308
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'vmz-d2-fence-'));
309
+ try {
310
+ const rel = 'src/components/FenceExample.vmz';
311
+ const abs = path.join(tmp, rel);
312
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
313
+ // Ensure minimal valid SFC if fence is a fragment; prefer full SFC bodies in docs.
314
+ const content = body.includes('<template') ? body : wrapVmzFragment(body);
315
+ fs.writeFileSync(abs, content, 'utf8');
316
+ const outDir = path.join(tmp, 'dist');
317
+ const ws = opts.createWorkspace({ root: tmp, outDir });
318
+ const report = ws.check(true);
319
+ const errors = (report.diagnostics || []).filter((d) => d.severity === 'error' || d.severity === 'Error');
320
+ if (errors.length) {
321
+ diagnostics.push({
322
+ code: DIAG.FENCE_CHECK,
323
+ severity: 'error',
324
+ message: `vmz fence check failed (${label}): ${errors[0]?.message || 'error'}`,
325
+ path: `${sourcePath}:${fence.lineStart}`,
326
+ });
327
+ ws.dispose?.();
328
+ return { record: { status: 'failed', detail: 'check' }, diagnostics, testSelection: null };
329
+ }
330
+ /** @type {any} */
331
+ let testSelection = null;
332
+ if (meta.run) {
333
+ const mode = meta.run === 'logic' || meta.run === 'browser' ? meta.run : 'compile';
334
+ const build = ws.build(false);
335
+ const buildErrs = (build.diagnostics || []).filter((d) => d.severity === 'error' || d.severity === 'Error');
336
+ if (buildErrs.length) {
337
+ diagnostics.push({
338
+ code: DIAG.FENCE_RUN_FAILED,
339
+ severity: 'error',
340
+ message: `vmz fence run=${mode} build failed (${label}): ${buildErrs[0]?.message || 'error'}`,
341
+ path: `${sourcePath}:${fence.lineStart}`,
342
+ });
343
+ ws.dispose?.();
344
+ return { record: { status: 'failed', detail: 'run_build' }, diagnostics, testSelection: null };
345
+ }
346
+ const prog = path.join(outDir, 'components', 'FenceExample.program.json');
347
+ if (!fs.existsSync(prog)) {
348
+ diagnostics.push({
349
+ code: DIAG.FENCE_RUN_FAILED,
350
+ severity: 'error',
351
+ message: `vmz fence run=${mode} missing program.json (${label})`,
352
+ path: `${sourcePath}:${fence.lineStart}`,
353
+ });
354
+ ws.dispose?.();
355
+ return { record: { status: 'failed', detail: 'run_program' }, diagnostics, testSelection: null };
356
+ }
357
+ testSelection = {
358
+ schema: 'vmz.dx.test_selection.v0',
359
+ reason: `document fence run=${mode} @ ${sourcePath}:${fence.lineStart}`,
360
+ testIds: [`document.fence.${pageKeySafe(sourcePath)}.${fence.lineStart}`],
361
+ affectedChunkIds: ['components/FenceExample'],
362
+ status: 'ready',
363
+ mode,
364
+ };
365
+ ws.dispose?.();
366
+ return {
367
+ record: { status: 'ok', detail: `run=${mode}`, source: label },
368
+ diagnostics,
369
+ testSelection,
370
+ };
371
+ }
372
+ ws.dispose?.();
373
+ return { record: { status: 'ok', detail: 'check', source: label }, diagnostics, testSelection: null };
374
+ }
375
+ finally {
376
+ fs.rmSync(tmp, { recursive: true, force: true });
377
+ }
378
+ }
379
+ function wrapVmzFragment(body) {
380
+ const trimmed = String(body || '').trim();
381
+ if (trimmed.startsWith('<')) {
382
+ return `<template>\n${trimmed}\n</template>\n<script client>\nexport default class FenceExample {}\n</script>\n`;
383
+ }
384
+ return `<template><p>ok</p></template>\n<script client>\n${trimmed}\n</script>\n`;
385
+ }
386
+ function pageKeySafe(p) {
387
+ return String(p || 'page').replace(/[^\w.-]+/g, '_');
388
+ }
389
+ /**
390
+ * TS/JS fence: oxc-aligned surface via TypeScript parse (syntax only; no execute).
391
+ */
392
+ function checkScriptFence({ fence, meta, sourcePath, page }) {
393
+ /** @type {import('./document-schema.js').DocumentDiagnostic[]} */
394
+ const diagnostics = [];
395
+ try {
396
+ const ts = require('typescript');
397
+ const isTs = meta.lang === 'ts' || meta.lang === 'typescript';
398
+ const fileName = isTs ? 'fence.ts' : 'fence.js';
399
+ const kind = isTs ? ts.ScriptKind.TS : ts.ScriptKind.JS;
400
+ const sf = ts.createSourceFile(fileName, fence.content, ts.ScriptTarget.Latest, true, kind);
401
+ // createSourceFile does not throw on syntax errors — scan for parse diagnostics via transpile.
402
+ const out = ts.transpileModule(fence.content, {
403
+ compilerOptions: {
404
+ target: ts.ScriptTarget.ES2022,
405
+ module: ts.ModuleKind.ESNext,
406
+ strict: false,
407
+ },
408
+ reportDiagnostics: true,
409
+ fileName,
410
+ });
411
+ const errs = (out.diagnostics || []).filter((d) => d.category === ts.DiagnosticCategory.Error);
412
+ if (errs.length) {
413
+ const msg = ts.flattenDiagnosticMessageText(errs[0].messageText, '\n');
414
+ diagnostics.push({
415
+ code: DIAG.FENCE_CHECK,
416
+ severity: 'error',
417
+ message: `${meta.lang} fence check failed: ${msg}`,
418
+ path: `${sourcePath}:${fence.lineStart}`,
419
+ });
420
+ return { record: { status: 'failed', detail: 'syntax' }, diagnostics };
421
+ }
422
+ // Touch sf to keep parse path honest.
423
+ if (!sf || sf.kind == null) {
424
+ diagnostics.push({
425
+ code: DIAG.FENCE_CHECK,
426
+ severity: 'error',
427
+ message: `${meta.lang} fence parse produced empty SourceFile`,
428
+ path: `${sourcePath}:${fence.lineStart}`,
429
+ });
430
+ return { record: { status: 'failed' }, diagnostics };
431
+ }
432
+ return { record: { status: 'ok', detail: 'syntax' }, diagnostics };
433
+ }
434
+ catch (e) {
435
+ diagnostics.push({
436
+ code: DIAG.FENCE_CHECK,
437
+ severity: 'error',
438
+ message: `${meta.lang} fence check unavailable: ${e.message || e}`,
439
+ path: `${sourcePath}:${fence.lineStart}`,
440
+ pageKey: page?.identity?.pageKey,
441
+ });
442
+ return { record: { status: 'failed', detail: 'engine' }, diagnostics };
443
+ }
444
+ }
445
+ function walkFiles(dir, fn) {
446
+ if (!fs.existsSync(dir))
447
+ return;
448
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
449
+ const full = path.join(dir, ent.name);
450
+ if (ent.isDirectory())
451
+ walkFiles(full, fn);
452
+ else if (ent.isFile())
453
+ fn(full);
454
+ }
455
+ }
456
+ /**
457
+ * Build only project src .vmz files into dist for API Program Graph queries.
458
+ * Avoids coupling document evidence to site /designs theme diagnostics.
459
+ */
460
+ async function ensureSrcProgramGraph(projectRoot, createWorkspace, diagnostics) {
461
+ const srcDir = path.join(projectRoot, 'src');
462
+ if (!fs.existsSync(srcDir))
463
+ return;
464
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'vmz-d2-api-'));
465
+ try {
466
+ copyDir(srcDir, path.join(tmp, 'src'));
467
+ const outDir = path.join(tmp, 'dist');
468
+ const ws = createWorkspace({ root: tmp, outDir });
469
+ const report = ws.build(false);
470
+ const errors = (report.diagnostics || []).filter((d) => d.severity === 'error' || d.severity === 'Error');
471
+ if (errors.length) {
472
+ diagnostics.push({
473
+ code: DIAG.FENCE_CHECK,
474
+ severity: 'error',
475
+ message: `src Program Graph build failed: ${errors[0]?.message || 'error'}`,
476
+ path: projectRoot,
477
+ });
478
+ ws.dispose?.();
479
+ return;
480
+ }
481
+ ws.dispose?.();
482
+ // Materialize program artifacts into project dist for loadProgramApiIndex.
483
+ const destDist = path.join(projectRoot, 'dist');
484
+ copyDir(outDir, destDist);
485
+ }
486
+ finally {
487
+ fs.rmSync(tmp, { recursive: true, force: true });
488
+ }
489
+ }
490
+ function copyDir(from, to) {
491
+ fs.mkdirSync(to, { recursive: true });
492
+ for (const ent of fs.readdirSync(from, { withFileTypes: true })) {
493
+ const src = path.join(from, ent.name);
494
+ const dst = path.join(to, ent.name);
495
+ if (ent.isDirectory())
496
+ copyDir(src, dst);
497
+ else
498
+ fs.copyFileSync(src, dst);
499
+ }
500
+ }
501
+ /** Lazy note: callers pass `createWorkspace` from `./index.js` (see document-cmd / document-build). */
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Integrated DocumentMount — build /documents into the host app dist so
3
+ * routeBase (e.g. /d) is served as static HTML next to SSR pages.
4
+ * Design: 规划设计/vmz/19 · Integrated DocumentMount (same project, no separate package).
5
+ */
6
+ /**
7
+ * @param {string} projectRoot
8
+ */
9
+ export declare function projectHasDocuments(projectRoot: any): boolean;
10
+ /**
11
+ * Build integrated documents into the application outDir (URL-aligned).
12
+ * @param {{ projectRoot: string, outDir: string, strict?: boolean }} opts
13
+ * @returns {Promise<{ ok: boolean, skipped?: boolean, pages?: number, error?: string }>}
14
+ */
15
+ export declare function buildIntegratedDocuments(opts: any): Promise<{
16
+ ok: boolean;
17
+ skipped: boolean;
18
+ error?: undefined;
19
+ pages?: undefined;
20
+ } | {
21
+ ok: boolean;
22
+ error: string;
23
+ pages: number;
24
+ skipped?: undefined;
25
+ } | {
26
+ ok: boolean;
27
+ pages: number;
28
+ skipped?: undefined;
29
+ error?: undefined;
30
+ } | {
31
+ ok: boolean;
32
+ error: string;
33
+ skipped?: undefined;
34
+ pages?: undefined;
35
+ }>;
@@ -0,0 +1,89 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Integrated DocumentMount — build /documents into the host app dist so
4
+ * routeBase (e.g. /d) is served as static HTML next to SSR pages.
5
+ * Design: 规划设计/vmz/19 · Integrated DocumentMount (same project, no separate package).
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { buildDocuments } from './document-build.js';
10
+ import { resolveDocumentsRoot } from './document-check.js';
11
+ import { log } from './log.js';
12
+ /**
13
+ * @param {string} projectRoot
14
+ */
15
+ export function projectHasDocuments(projectRoot) {
16
+ const root = resolveDocumentsRoot(projectRoot);
17
+ return fs.existsSync(root) && fs.statSync(root).isDirectory();
18
+ }
19
+ /**
20
+ * Build integrated documents into the application outDir (URL-aligned).
21
+ * @param {{ projectRoot: string, outDir: string, strict?: boolean }} opts
22
+ * @returns {Promise<{ ok: boolean, skipped?: boolean, pages?: number, error?: string }>}
23
+ */
24
+ export async function buildIntegratedDocuments(opts) {
25
+ const projectRoot = path.resolve(opts.projectRoot);
26
+ const outDir = path.resolve(opts.outDir);
27
+ if (!projectHasDocuments(projectRoot)) {
28
+ return { ok: true, skipped: true };
29
+ }
30
+ try {
31
+ const result = await buildDocuments({
32
+ projectRoot,
33
+ outDir,
34
+ strict: Boolean(opts.strict),
35
+ });
36
+ if (!result.ok) {
37
+ const errs = (result.manifest?.diagnostics || []).filter((d) => d.severity === 'error');
38
+ for (const d of errs.slice(0, 12)) {
39
+ log.error(`${d.code}: ${d.message}`);
40
+ }
41
+ return { ok: false, error: 'document diagnostics', pages: 0 };
42
+ }
43
+ writeMountRootRedirects(result.manifest, outDir);
44
+ log.info(`document mount: pages=${result.pages.length} → ${path.relative(process.cwd(), outDir) || '.'}`);
45
+ return { ok: true, pages: result.pages.length };
46
+ }
47
+ catch (e) {
48
+ const msg = e instanceof Error ? e.message : String(e);
49
+ log.error(`document mount failed: ${msg}`);
50
+ return { ok: false, error: msg };
51
+ }
52
+ }
53
+ /**
54
+ * Emit `{routeBase}/index.html` → defaultLocale landing (for /d/ and /docs/).
55
+ * @param {import('./document-schema.js').DocumentManifest} manifest
56
+ * @param {string} outDir
57
+ */
58
+ function writeMountRootRedirects(manifest, outDir) {
59
+ const defaultLocale = manifest.defaultLocale || manifest.locales?.[0];
60
+ if (!defaultLocale)
61
+ return;
62
+ for (const mount of manifest.mounts || []) {
63
+ if (!mount?.routeBase || mount.routeBase === '/')
64
+ continue;
65
+ const base = String(mount.routeBase).replace(/\/$/, '');
66
+ const target = `${base}/${defaultLocale}/`;
67
+ const relDir = base.replace(/^\//, '');
68
+ const abs = path.join(outDir, relDir, 'index.html');
69
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
70
+ const html = `<!DOCTYPE html>
71
+ <html lang="${escapeAttr(defaultLocale)}">
72
+ <head>
73
+ <meta charset="utf-8" />
74
+ <meta http-equiv="refresh" content="0;url=${escapeAttr(target)}" />
75
+ <link rel="canonical" href="${escapeAttr(target)}" />
76
+ <title>Documents</title>
77
+ </head>
78
+ <body>
79
+ <p><a href="${escapeAttr(target)}">Continue to ${escapeAttr(defaultLocale)} docs</a></p>
80
+ </body>
81
+ </html>
82
+ `;
83
+ fs.writeFileSync(abs, html, 'utf8');
84
+ }
85
+ }
86
+ /** @param {string} s */
87
+ function escapeAttr(s) {
88
+ return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
89
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Strip tags / collapse whitespace for search body text.
3
+ * @param {string} html
4
+ */
5
+ export declare function htmlToSearchText(html: any): string;
6
+ /**
7
+ * @param {{
8
+ * manifest: any,
9
+ * enriched: { byId: Map<string, any> },
10
+ * evidence: any,
11
+ * version?: string | null,
12
+ * }} opts
13
+ */
14
+ export declare function buildDocumentSearch(opts: any): {
15
+ schema: string;
16
+ status: string;
17
+ version: any;
18
+ records: any[];
19
+ };
20
+ /**
21
+ * Island-only resume plan for document surfaces.
22
+ * @param {{
23
+ * evidence: any,
24
+ * searchHref?: string,
25
+ * fenceBodies?: Map<string, string>,
26
+ * }} opts
27
+ */
28
+ export declare function buildDocumentIslands(opts: any): {
29
+ schema: string;
30
+ hydrate: string;
31
+ fullPageHydrate: boolean;
32
+ islands: {
33
+ name: string;
34
+ kind: string;
35
+ resume: string;
36
+ index: any;
37
+ }[];
38
+ status: string;
39
+ };
40
+ /**
41
+ * Relative href from an HTML page to a root artifact (posix).
42
+ * @param {string} htmlRel
43
+ * @param {string} artifactName
44
+ */
45
+ export declare function artifactHrefFromHtml(htmlRel: any, artifactName: any): string;
46
+ /**
47
+ * Stable map key for fence body lookup.
48
+ * @param {{ locale: string, pageKey: string, lineStart: number }} f
49
+ */
50
+ export declare function fenceBodyKey(f: any): string;
51
+ /**
52
+ * Collect fence bodies from analyzeMarkdown results for playground islands.
53
+ * @param {Map<string, { fences?: any[] }>} analyzedByPageId locale:pageKey → analyze result
54
+ * @param {any[]} pages manifest.pages
55
+ */
56
+ export declare function collectFenceBodies(analyzedByPageId: any, pages: any): Map<any, any>;
57
+ /**
58
+ * Render SSR island shells (no script — resume later).
59
+ * @param {{
60
+ * islands: any,
61
+ * searchIndexHref: string,
62
+ * pageKey: string,
63
+ * locale: string,
64
+ * }} opts
65
+ */
66
+ export declare function renderIslandShellsHtml(opts: any): {
67
+ searchHtml: string;
68
+ playgroundHtml: any;
69
+ };