@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,147 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * `vmz document` / `vmz docs` CLI (D0 check · D1 build).
4
+ * Design: 规划设计/vmz/19 §1 · §8
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { buildDocuments } from './document-build.js';
9
+ import { checkDocuments, manifestHasErrors } from './document-check.js';
10
+ import { enrichDocumentContent } from './document-enrich.js';
11
+ import { enrichDocumentEvidence } from './document-evidence.js';
12
+ import { resolveMarkdownEngine } from './document-markdown.js';
13
+ import { createWorkspace } from './index.js';
14
+ import { log } from './log.js';
15
+ import { parseArgs } from './cli.js';
16
+ function printDocumentHelp() {
17
+ console.log(`vmz document — project /documents domain (D0–D3)
18
+
19
+ Usage:
20
+ vmz document check [project] Check locale tree + links/anchors + fence/API evidence
21
+ vmz document build [project] Static HTML + view + evidence + search/islands + /designs CSS
22
+ vmz docs check|build […] Alias of document
23
+
24
+ Options:
25
+ --root <dir> Project root (default: . or positional path)
26
+ --out <dir> Build output (default: <project>/dist/documents)
27
+ --strict Fail on missing/orphan translations & require defaultLocale
28
+ --json [file] Emit DocumentManifest JSON to stdout or file
29
+ `);
30
+ }
31
+ /**
32
+ * @param {string[]} argv
33
+ * @returns {Promise<number>}
34
+ */
35
+ export async function cmdDocument(argv) {
36
+ const [sub, ...rest] = argv;
37
+ if (!sub) {
38
+ if (process.stdout.isTTY) {
39
+ printDocumentHelp();
40
+ return 0;
41
+ }
42
+ log.error('vmz document requires a subcommand in non-interactive/CI contexts');
43
+ printDocumentHelp();
44
+ return 1;
45
+ }
46
+ if (sub === 'help' || sub === '-h' || sub === '--help') {
47
+ printDocumentHelp();
48
+ return 0;
49
+ }
50
+ if (sub.startsWith('-')) {
51
+ log.error('vmz document requires a subcommand (check|build)');
52
+ printDocumentHelp();
53
+ return 1;
54
+ }
55
+ const args = parseArgs(rest);
56
+ switch (sub) {
57
+ case 'check':
58
+ return cmdDocumentCheck(args);
59
+ case 'build':
60
+ return cmdDocumentBuild(args);
61
+ case 'dev':
62
+ case 'serve':
63
+ case 'test':
64
+ case 'clean':
65
+ log.error(`vmz document ${sub} is not implemented yet`);
66
+ return 1;
67
+ default:
68
+ log.error(`unknown document subcommand \`${sub}\``);
69
+ printDocumentHelp();
70
+ return 1;
71
+ }
72
+ }
73
+ /** @param {Record<string, string | boolean> & { _: string[] }} args */
74
+ async function cmdDocumentCheck(args) {
75
+ const project = (typeof args.root === 'string' && args.root) || (typeof args._[0] === 'string' && args._[0]) || '.';
76
+ const projectRoot = path.resolve(project);
77
+ const strict = Boolean(args.strict);
78
+ const manifest = checkDocuments({ projectRoot, strict });
79
+ try {
80
+ const engine = await resolveMarkdownEngine({});
81
+ const enriched = enrichDocumentContent(manifest, {
82
+ analyzeMarkdown: engine.analyzeMarkdown,
83
+ projectRoot,
84
+ });
85
+ manifest.diagnostics = enriched.diagnostics;
86
+ const evidence = await enrichDocumentEvidence(manifest, {
87
+ analyzeMarkdown: engine.analyzeMarkdown,
88
+ projectRoot,
89
+ createWorkspace,
90
+ });
91
+ manifest.diagnostics = evidence.diagnostics;
92
+ manifest.evidence = evidence.evidence;
93
+ }
94
+ catch (e) {
95
+ log.warn(`document check: markdown/evidence unavailable (${e.message}); skipping enrich`);
96
+ }
97
+ const jsonOut = args.json;
98
+ if (jsonOut) {
99
+ const text = JSON.stringify(manifest, null, 2);
100
+ if (typeof jsonOut === 'string') {
101
+ fs.writeFileSync(jsonOut, text + '\n', 'utf8');
102
+ log.info(`wrote ${jsonOut}`);
103
+ }
104
+ else {
105
+ console.log(text);
106
+ }
107
+ }
108
+ else {
109
+ for (const d of manifest.diagnostics) {
110
+ const loc = d.path ? ` (${d.path})` : '';
111
+ if (d.severity === 'error')
112
+ log.error(`${d.code}: ${d.message}${loc}`);
113
+ else
114
+ console.warn(`vmz warn ${d.code}: ${d.message}${loc}`);
115
+ }
116
+ log.info(`document check: locales=${manifest.locales.join(',') || '(none)'} pages=${manifest.pages.length} defaultLocale=${manifest.defaultLocale ?? '(none)'}`);
117
+ }
118
+ return manifestHasErrors(manifest) ? 1 : 0;
119
+ }
120
+ /** @param {Record<string, string | boolean> & { _: string[] }} args */
121
+ async function cmdDocumentBuild(args) {
122
+ const project = (typeof args.root === 'string' && args.root) || (typeof args._[0] === 'string' && args._[0]) || '.';
123
+ const projectRoot = path.resolve(project);
124
+ const outDir = (typeof args.out === 'string' && args.out) || path.join(projectRoot, 'dist', 'documents');
125
+ const strict = Boolean(args.strict);
126
+ let result;
127
+ try {
128
+ result = await buildDocuments({ projectRoot, outDir, strict });
129
+ }
130
+ catch (e) {
131
+ log.error(`document build failed: ${e.message || e}`);
132
+ return 1;
133
+ }
134
+ for (const d of result.manifest.diagnostics || []) {
135
+ const loc = d.path ? ` (${d.path})` : '';
136
+ if (d.severity === 'error')
137
+ log.error(`${d.code}: ${d.message}${loc}`);
138
+ else
139
+ console.warn(`vmz warn ${d.code}: ${d.message}${loc}`);
140
+ }
141
+ if (!result.ok) {
142
+ log.error('document build aborted due to diagnostics');
143
+ return 1;
144
+ }
145
+ log.info(`document build: pages=${result.pages.length} out=${path.relative(process.cwd(), result.outDir) || '.'} designs=${result.manifest.build?.designsCss ?? '(none)'}`);
146
+ return 0;
147
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @param {string} projectRoot
3
+ * @returns {{ css: string, source: string | null, href: string | null }}
4
+ */
5
+ export declare function resolveDocumentDesignsCss(projectRoot: any): {
6
+ css: string;
7
+ source: string;
8
+ href: string;
9
+ };
@@ -0,0 +1,126 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Document D1 — wire project `/designs` into static document assets.
4
+ * Prefer compiled `vmz-designs.css` / `vmz.css`; else emit a minimal token CSS.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ /**
9
+ * @param {string} projectRoot
10
+ * @returns {{ css: string, source: string | null, href: string | null }}
11
+ */
12
+ export function resolveDocumentDesignsCss(projectRoot) {
13
+ const designsDir = path.join(projectRoot, 'designs');
14
+ if (!fs.existsSync(designsDir) || !fs.statSync(designsDir).isDirectory()) {
15
+ return { css: '', source: null, href: null };
16
+ }
17
+ /** @type {string[]} */
18
+ const parts = [];
19
+ /** @type {string[]} */
20
+ const sources = [];
21
+ // Layout first so @import (fonts) stays at stylesheet top; theme tokens follow.
22
+ const layoutCandidates = [path.join(designsDir, 'document', 'chrome.css'), path.join(designsDir, 'styles', 'document.css')];
23
+ for (const p of layoutCandidates) {
24
+ if (!fs.existsSync(p))
25
+ continue;
26
+ parts.push(fs.readFileSync(p, 'utf8'));
27
+ sources.push(path.relative(projectRoot, p).replace(/\\/g, '/'));
28
+ break;
29
+ }
30
+ const distCandidates = [path.join(projectRoot, 'dist', 'vmz-designs.css'), path.join(projectRoot, 'dist', 'vmz.css')];
31
+ for (const p of distCandidates) {
32
+ if (fs.existsSync(p)) {
33
+ parts.push(fs.readFileSync(p, 'utf8'));
34
+ sources.push(path.relative(projectRoot, p).replace(/\\/g, '/'));
35
+ break;
36
+ }
37
+ }
38
+ if (parts.length) {
39
+ return {
40
+ css: parts.join('\n\n'),
41
+ source: sources.join('+'),
42
+ href: 'assets/vmz-designs.css',
43
+ };
44
+ }
45
+ const styleCandidates = [path.join(designsDir, 'styles', 'index.css')];
46
+ for (const p of styleCandidates) {
47
+ if (fs.existsSync(p)) {
48
+ return {
49
+ css: fs.readFileSync(p, 'utf8'),
50
+ source: path.relative(projectRoot, p).replace(/\\/g, '/'),
51
+ href: 'assets/vmz-designs.css',
52
+ };
53
+ }
54
+ }
55
+ const emitted = emitMinimalDesignsCss(designsDir);
56
+ if (emitted) {
57
+ return { css: emitted, source: 'designs/', href: 'assets/vmz-designs.css' };
58
+ }
59
+ return { css: '', source: 'designs/', href: null };
60
+ }
61
+ /** @param {string} designsDir */
62
+ function emitMinimalDesignsCss(designsDir) {
63
+ /** @type {Record<string, string>} */
64
+ const vars = {};
65
+ const tokenDir = path.join(designsDir, 'tokens');
66
+ if (fs.existsSync(tokenDir)) {
67
+ walkJson(tokenDir, (obj, prefix) => flattenTokens(obj, prefix, vars));
68
+ }
69
+ const themeJson = path.join(designsDir, 'theme.json');
70
+ if (fs.existsSync(themeJson)) {
71
+ try {
72
+ flattenTokens(JSON.parse(fs.readFileSync(themeJson, 'utf8')), '', vars);
73
+ }
74
+ catch {
75
+ /* ignore */
76
+ }
77
+ }
78
+ const keys = Object.keys(vars).sort();
79
+ if (!keys.length) {
80
+ // Presence of /designs still warrants a readable baseline sheet.
81
+ return `/* vmz.document designs baseline */\n:root { color-scheme: light; }\nbody { font-family: system-ui, sans-serif; line-height: 1.5; margin: 0; }\nmain { max-width: 48rem; margin: 0 auto; padding: 1.5rem; }\nnav { padding: 1rem 1.5rem; border-bottom: 1px solid #ddd; }\n`;
82
+ }
83
+ const lines = keys.map((k) => ` ${cssVar(k)}: ${vars[k]};`);
84
+ return `/* vmz.document designs from /designs */\n:root {\n${lines.join('\n')}\n}\nbody { font-family: var(--font-sans, system-ui, sans-serif); line-height: 1.5; margin: 0; color: var(--color-fg, #111); background: var(--color-bg, #fff); }\nmain { max-width: 48rem; margin: 0 auto; padding: 1.5rem; }\nnav { padding: 1rem 1.5rem; border-bottom: 1px solid var(--color-border, #ddd); }\n`;
85
+ }
86
+ function cssVar(key) {
87
+ const name = String(key)
88
+ .replace(/[^a-zA-Z0-9_-]+/g, '-')
89
+ .replace(/^-|-$/g, '');
90
+ return `--${name}`;
91
+ }
92
+ function flattenTokens(obj, prefix, out) {
93
+ if (obj == null || typeof obj !== 'object' || Array.isArray(obj))
94
+ return;
95
+ for (const [k, v] of Object.entries(obj)) {
96
+ const key = prefix ? `${prefix}-${k}` : k;
97
+ if (v != null && typeof v === 'object' && !Array.isArray(v)) {
98
+ if ('value' in v && (typeof v.value === 'string' || typeof v.value === 'number')) {
99
+ out[key] = String(v.value);
100
+ }
101
+ else {
102
+ flattenTokens(v, key, out);
103
+ }
104
+ }
105
+ else if (typeof v === 'string' || typeof v === 'number') {
106
+ out[key] = String(v);
107
+ }
108
+ }
109
+ }
110
+ function walkJson(dir, fn) {
111
+ if (!fs.existsSync(dir))
112
+ return;
113
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
114
+ const full = path.join(dir, ent.name);
115
+ if (ent.isDirectory())
116
+ walkJson(full, fn);
117
+ else if (ent.isFile() && ent.name.endsWith('.json')) {
118
+ try {
119
+ fn(JSON.parse(fs.readFileSync(full, 'utf8')), '');
120
+ }
121
+ catch {
122
+ /* ignore */
123
+ }
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @param {string} routeBase e.g. /docs
3
+ * @param {string} locale
4
+ * @param {string} pageKey
5
+ */
6
+ export declare function pageRoute(routeBase: any, locale: any, pageKey: any): string;
7
+ /**
8
+ * Static file path relative to out dir (posix).
9
+ * @param {string} routeBase
10
+ * @param {string} locale
11
+ * @param {string} pageKey
12
+ */
13
+ export declare function pageHtmlRel(routeBase: any, locale: any, pageKey: any): string;
14
+ /**
15
+ * @param {import('./document-schema.js').DocumentManifest} manifest
16
+ * @param {{ analyzeMarkdown: Function, projectRoot: string, designsCssHref?: string | null }} ctx
17
+ */
18
+ export declare function enrichDocumentContent(manifest: any, ctx: any): {
19
+ byId: Map<any, any>;
20
+ navByLocale: {};
21
+ diagnostics: any[];
22
+ routeBase: any;
23
+ };
@@ -0,0 +1,234 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Document D1 — enrich manifest with routes, anchors, nav; diagnose links.
4
+ * Design: 规划设计/vmz/19 §5–6
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { DIAG } from './document-schema.js';
9
+ /**
10
+ * @param {string} routeBase e.g. /docs
11
+ * @param {string} locale
12
+ * @param {string} pageKey
13
+ */
14
+ export function pageRoute(routeBase, locale, pageKey) {
15
+ const base = String(routeBase || '/').replace(/\/$/, '') || '';
16
+ const key = pageKey === 'index' ? '' : pageKey.replace(/\\/g, '/');
17
+ const parts = [base.replace(/^\//, ''), locale, key].filter((p) => p !== '');
18
+ return '/' + parts.join('/');
19
+ }
20
+ /**
21
+ * Static file path relative to out dir (posix).
22
+ * @param {string} routeBase
23
+ * @param {string} locale
24
+ * @param {string} pageKey
25
+ */
26
+ export function pageHtmlRel(routeBase, locale, pageKey) {
27
+ const base = String(routeBase || '/')
28
+ .replace(/^\//, '')
29
+ .replace(/\/$/, '');
30
+ const file = pageKey === 'index' ? 'index.html' : `${pageKey.replace(/\\/g, '/')}.html`;
31
+ return [base, locale, file].filter(Boolean).join('/');
32
+ }
33
+ /**
34
+ * @param {import('./document-schema.js').DocumentManifest} manifest
35
+ * @param {{ analyzeMarkdown: Function, projectRoot: string, designsCssHref?: string | null }} ctx
36
+ */
37
+ export function enrichDocumentContent(manifest, ctx) {
38
+ const routeBase = manifest.mounts?.[0]?.routeBase || '/docs';
39
+ /** @type {Map<string, { html: string, headings: any[], links: any[], title: string, route: string, anchors: string[] }>} */
40
+ const byId = new Map();
41
+ /** @type {import('./document-schema.js').DocumentDiagnostic[]} */
42
+ const diagnostics = [...(manifest.diagnostics || [])];
43
+ /** @type {Map<string, string>} */
44
+ const routeOwners = new Map();
45
+ for (const page of manifest.pages) {
46
+ const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
47
+ const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
48
+ const analyzed = ctx.analyzeMarkdown(source);
49
+ const route = pageRoute(routeBase, page.identity.locale, page.identity.pageKey);
50
+ const anchors = analyzed.headings.map((h) => h.id);
51
+ const title = analyzed.headings.find((h) => h.level === 1)?.text || analyzed.headings[0]?.text || page.identity.pageKey;
52
+ // Duplicate anchors on page
53
+ const seen = new Set();
54
+ for (const id of anchors) {
55
+ if (seen.has(id)) {
56
+ diagnostics.push({
57
+ code: DIAG.ANCHOR_DUPLICATE,
58
+ severity: 'error',
59
+ message: `duplicate anchor #${id} on ${page.identity.locale}/${page.identity.pageKey}`,
60
+ path: page.sourcePath,
61
+ });
62
+ }
63
+ seen.add(id);
64
+ }
65
+ if (routeOwners.has(route)) {
66
+ diagnostics.push({
67
+ code: DIAG.ROUTE_DUPLICATE,
68
+ severity: 'error',
69
+ message: `duplicate route ${route} (also ${routeOwners.get(route)})`,
70
+ path: page.sourcePath,
71
+ });
72
+ }
73
+ else {
74
+ routeOwners.set(route, `${page.identity.locale}:${page.identity.pageKey}`);
75
+ }
76
+ page.route = route;
77
+ page.anchors = anchors;
78
+ page.title = title;
79
+ byId.set(`${page.identity.locale}:${page.identity.pageKey}`, {
80
+ html: analyzed.html,
81
+ headings: analyzed.headings,
82
+ links: analyzed.links,
83
+ title,
84
+ route,
85
+ anchors,
86
+ });
87
+ }
88
+ // Nav per locale (directory order: pageKey sorted)
89
+ /** @type {Record<string, Array<{ pageKey: string, title: string, href: string }>>} */
90
+ const navByLocale = {};
91
+ for (const loc of manifest.locales) {
92
+ const pages = manifest.pages
93
+ .filter((p) => p.identity.locale === loc)
94
+ .slice()
95
+ .sort((a, b) => a.identity.pageKey.localeCompare(b.identity.pageKey));
96
+ navByLocale[loc] = pages.map((p) => ({
97
+ pageKey: p.identity.pageKey,
98
+ title: p.title || p.identity.pageKey,
99
+ href: p.route,
100
+ }));
101
+ }
102
+ // Link checks
103
+ const pageKeySet = new Map(); // locale -> Set pageKey
104
+ for (const p of manifest.pages) {
105
+ const set = pageKeySet.get(p.identity.locale) || new Set();
106
+ set.add(p.identity.pageKey);
107
+ pageKeySet.set(p.identity.locale, set);
108
+ }
109
+ for (const page of manifest.pages) {
110
+ const info = byId.get(`${page.identity.locale}:${page.identity.pageKey}`);
111
+ if (!info)
112
+ continue;
113
+ for (const link of info.links) {
114
+ const href = String(link.href || '').trim();
115
+ if (!href || href.startsWith('mailto:') || href.startsWith('http://') || href.startsWith('https://')) {
116
+ continue;
117
+ }
118
+ // D2 API refs are resolved by evidence, not as page links.
119
+ if (href.startsWith('vmz-api:') || href.startsWith('api:')) {
120
+ continue;
121
+ }
122
+ if (href.startsWith('#')) {
123
+ const id = href.slice(1);
124
+ if (!info.anchors.includes(id)) {
125
+ diagnostics.push({
126
+ code: DIAG.LINK_BROKEN,
127
+ severity: 'error',
128
+ message: `broken anchor ${href} on ${page.identity.locale}/${page.identity.pageKey}`,
129
+ path: page.sourcePath,
130
+ });
131
+ }
132
+ continue;
133
+ }
134
+ const resolved = resolveDocHref(href, page.identity.pageKey, page.identity.locale, routeBase, pageKeySet);
135
+ if (!resolved.ok) {
136
+ diagnostics.push({
137
+ code: DIAG.LINK_BROKEN,
138
+ severity: 'error',
139
+ message: `broken link ${JSON.stringify(href)}: ${resolved.reason}`,
140
+ path: page.sourcePath,
141
+ });
142
+ }
143
+ else if (resolved.anchor) {
144
+ const target = byId.get(`${resolved.locale}:${resolved.pageKey}`);
145
+ const anchors = target?.anchors || [];
146
+ if (!anchors.includes(resolved.anchor)) {
147
+ diagnostics.push({
148
+ code: DIAG.LINK_BROKEN,
149
+ severity: 'error',
150
+ message: `broken link ${JSON.stringify(href)}: missing anchor #${resolved.anchor}`,
151
+ path: page.sourcePath,
152
+ });
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return { byId, navByLocale, diagnostics, routeBase };
158
+ }
159
+ /**
160
+ * @param {string} href
161
+ * @param {string} fromPageKey
162
+ * @param {string} locale
163
+ * @param {string} routeBase
164
+ * @param {Map<string, Set<string>>} pageKeySet
165
+ */
166
+ function resolveDocHref(href, fromPageKey, locale, routeBase, pageKeySet) {
167
+ let pathPart = href;
168
+ let anchor = null;
169
+ const hash = href.indexOf('#');
170
+ if (hash >= 0) {
171
+ pathPart = href.slice(0, hash);
172
+ anchor = href.slice(hash + 1);
173
+ }
174
+ pathPart = pathPart.replace(/\.md$/i, '').replace(/\\/g, '/');
175
+ if (pathPart.startsWith('/')) {
176
+ // Absolute site path under mount: /docs/zh-hans/guide/install
177
+ const want = pathPart.replace(/\/$/, '') || '/';
178
+ const prefix = pageRoute(routeBase, locale, 'index').replace(/\/$/, '');
179
+ // Accept full route or locale-relative
180
+ for (const [loc, keys] of pageKeySet) {
181
+ for (const pk of keys) {
182
+ const r = pageRoute(routeBase, loc, pk).replace(/\/$/, '');
183
+ if (r === want || want.endsWith(`/${loc}/${pk === 'index' ? '' : pk}`.replace(/\/$/, ''))) {
184
+ return { ok: true, locale: loc, pageKey: pk, anchor, anchors: [] };
185
+ }
186
+ }
187
+ }
188
+ // Still ok if matches any known route shape for this locale
189
+ const keys = pageKeySet.get(locale) || new Set();
190
+ const stripped = want.replace(new RegExp(`^${escapeRe(routeBase.replace(/\/$/, ''))}/${escapeRe(locale)}/?`), '');
191
+ const pk = stripped === '' ? 'index' : stripped;
192
+ if (keys.has(pk))
193
+ return { ok: true, locale, pageKey: pk, anchor, anchors: [] };
194
+ return { ok: false, reason: 'unknown route' };
195
+ }
196
+ // Relative to current page directory
197
+ const fromDir = pageKeyDir(fromPageKey);
198
+ const joined = path.posix.normalize(path.posix.join(fromDir || '.', pathPart));
199
+ let pk = joined === '.' || joined === '' ? 'index' : joined.replace(/^\.\//, '');
200
+ pk = normalizePageKey(pk);
201
+ const keys = pageKeySet.get(locale) || new Set();
202
+ if (!keys.has(pk)) {
203
+ return { ok: false, reason: `no PageKey ${pk} in ${locale}` };
204
+ }
205
+ return { ok: true, locale, pageKey: pk, anchor, anchors: [] };
206
+ }
207
+ /**
208
+ * Directory used for relative Markdown links.
209
+ * `index` → repo root; bare `guide` (from guide/index.md) → `guide`;
210
+ * `guide/install` → `guide`.
211
+ */
212
+ function pageKeyDir(pageKey) {
213
+ if (!pageKey || pageKey === 'index')
214
+ return '';
215
+ if (!pageKey.includes('/'))
216
+ return pageKey;
217
+ return pageKey.slice(0, pageKey.lastIndexOf('/'));
218
+ }
219
+ /** Strip trailing `/index` so guide/index.md links resolve to PageKey `guide`. */
220
+ function normalizePageKey(pageKey) {
221
+ let pk = String(pageKey || '')
222
+ .replace(/\\/g, '/')
223
+ .replace(/\/+$/, '');
224
+ if (pk.endsWith('/index')) {
225
+ pk = pk.slice(0, -'/index'.length);
226
+ }
227
+ else if (pk === 'index') {
228
+ return 'index';
229
+ }
230
+ return pk === '' || pk === '.' ? 'index' : pk;
231
+ }
232
+ function escapeRe(s) {
233
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
234
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @param {string} info
3
+ * @returns {{ lang: string, run: string | null, source: string | null, playground: boolean }}
4
+ */
5
+ export declare function parseFenceInfo(info: any): {
6
+ lang: string;
7
+ run: any;
8
+ source: any;
9
+ playground: boolean;
10
+ };
11
+ /**
12
+ * @param {string} href
13
+ * @returns {string | null} symbol query (chunkId or name)
14
+ */
15
+ export declare function parseApiHref(href: any): string;
16
+ /**
17
+ * @param {string} projectRoot
18
+ * @returns {Array<{ chunkId: string, name: string, path: string, capabilities: string[], programPath: string }>}
19
+ */
20
+ export declare function loadProgramApiIndex(projectRoot: any): any[];
21
+ /**
22
+ * Resolve API symbol against Program Graph index.
23
+ * @param {ReturnType<typeof loadProgramApiIndex>} index
24
+ * @param {string} query
25
+ */
26
+ export declare function resolveApiSymbol(index: any, query: any): {
27
+ status: string;
28
+ matches: any;
29
+ };
30
+ /**
31
+ * @param {import('./document-schema.js').DocumentManifest} manifest
32
+ * @param {{
33
+ * analyzeMarkdown: Function,
34
+ * projectRoot: string,
35
+ * createWorkspace?: Function,
36
+ * ensureProgramGraph?: boolean,
37
+ * }} ctx
38
+ */
39
+ export declare function enrichDocumentEvidence(manifest: any, ctx: any): Promise<{
40
+ diagnostics: any[];
41
+ evidence: {
42
+ schema: string;
43
+ fences: any[];
44
+ apiRefs: any[];
45
+ testSelections: any[];
46
+ status: string;
47
+ };
48
+ }>;
49
+ /** Lazy note: callers pass `createWorkspace` from `./index.js` (see document-cmd / document-build). */