@specglass/core 0.0.4 → 0.0.6

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.
@@ -54,13 +54,16 @@ export function defineDocsCollection(options) {
54
54
  export function getSlugFromFilePath(filePath) {
55
55
  // Normalize path separators
56
56
  let slug = filePath.replace(/\\/g, "/");
57
- // Strip leading content/ prefix
57
+ // Strip leading content/ and/or docs/ prefixes.
58
+ // In monorepo context, entry.id is "content/docs/getting-started.mdx".
59
+ // When consumed via npm, the glob loader produces "docs/getting-started.mdx".
58
60
  slug = slug.replace(/^content\//, "");
61
+ slug = slug.replace(/^docs\//, "");
59
62
  // Strip file extension (.mdx or .md)
60
63
  slug = slug.replace(/\.(mdx|md)$/, "");
61
64
  // Handle index files — map to parent directory
62
65
  slug = slug.replace(/\/index$/, "");
63
- // Handle root index (just "index" after stripping content/)
66
+ // Handle root index (just "index" after stripping prefixes)
64
67
  if (slug === "index") {
65
68
  return "";
66
69
  }
package/dist/index.d.ts CHANGED
@@ -19,10 +19,14 @@ export type { NavigationWatcherOptions } from "./navigation/watcher.js";
19
19
  export { SpecglassError } from "./errors/specglass-error.js";
20
20
  export { parseOpenApiSpec } from "./openapi/parser.js";
21
21
  export { transformSpec } from "./openapi/transformer.js";
22
- export { buildEndpointSlug, buildEndpointId } from "./openapi/utils.js";
22
+ export { buildEndpointSlug, buildErrorEndpointSlug, buildEndpointId } from "./openapi/utils.js";
23
23
  export type { ApiEndpoint, ApiEndpointError, ApiExample, ApiMediaType, ApiParameter, ApiRequestBody, ApiResponse, ApiSchema, ApiSecurityRequirement, ParsedOpenApiSpec, } from "./openapi/types.js";
24
24
  export { loadOpenApiContent, } from "./content/openapi-loader.js";
25
25
  export type { OpenApiLoaderConfig, OpenApiContentEntry, OpenApiLoaderResult, } from "./content/openapi-loader.js";
26
26
  export { generateExampleValue, extractMediaTypeExample, } from "./openapi/example-generator.js";
27
27
  export { generateCurlExample, generatePythonExample, generateNodeExample, buildRequestUrl, } from "./openapi/code-generator.js";
28
28
  export type { CodeExampleOptions } from "./openapi/code-generator.js";
29
+ export { runValidators, toSpecglassError, type ValidationResult, type ValidationSeverity, type ValidatorContext, type Validator, } from "./validation/index.js";
30
+ export { validateLinks } from "./validation/link-validator.js";
31
+ export { validateFrontmatter } from "./validation/frontmatter-validator.js";
32
+ export { validateSpecDrift } from "./validation/spec-drift-validator.js";
package/dist/index.js CHANGED
@@ -21,8 +21,13 @@ export { SpecglassError } from "./errors/specglass-error.js";
21
21
  // OpenAPI
22
22
  export { parseOpenApiSpec } from "./openapi/parser.js";
23
23
  export { transformSpec } from "./openapi/transformer.js";
24
- export { buildEndpointSlug, buildEndpointId } from "./openapi/utils.js";
24
+ export { buildEndpointSlug, buildErrorEndpointSlug, buildEndpointId } from "./openapi/utils.js";
25
25
  export { loadOpenApiContent, } from "./content/openapi-loader.js";
26
26
  // OpenAPI example generation
27
27
  export { generateExampleValue, extractMediaTypeExample, } from "./openapi/example-generator.js";
28
28
  export { generateCurlExample, generatePythonExample, generateNodeExample, buildRequestUrl, } from "./openapi/code-generator.js";
29
+ // Validation
30
+ export { runValidators, toSpecglassError, } from "./validation/index.js";
31
+ export { validateLinks } from "./validation/link-validator.js";
32
+ export { validateFrontmatter } from "./validation/frontmatter-validator.js";
33
+ export { validateSpecDrift } from "./validation/spec-drift-validator.js";
@@ -194,6 +194,10 @@ export const specglassIntegration = defineIntegration({
194
194
  rehypePlugins: [rehypeCodeBlocks],
195
195
  },
196
196
  });
197
+ params.injectRoute({
198
+ pattern: "/",
199
+ entrypoint: "@specglass/core/pages/index.astro",
200
+ });
197
201
  params.injectRoute({
198
202
  pattern: "/[...slug]",
199
203
  entrypoint: "@specglass/core/pages/[...slug].astro",
@@ -7,6 +7,19 @@ import type { NavigationTree } from "../types/navigation.js";
7
7
  * "api-reference" → "Api Reference"
8
8
  */
9
9
  export declare function toTitleCase(kebab: string): string;
10
+ /**
11
+ * Normalize a directory or file name into a URL-safe slug segment.
12
+ *
13
+ * Matches Astro glob loader behavior:
14
+ * - lowercases the name
15
+ * - replaces whitespace runs with a single hyphen
16
+ *
17
+ * Examples:
18
+ * "Prompt Optimization" → "prompt-optimization"
19
+ * "Introduction" → "introduction"
20
+ * "getting-started" → "getting-started"
21
+ */
22
+ export declare function slugify(name: string): string;
10
23
  /**
11
24
  * Build a navigation tree by recursively walking a content directory.
12
25
  *
@@ -14,6 +14,21 @@ export function toTitleCase(kebab) {
14
14
  .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
15
15
  .join(" ");
16
16
  }
17
+ /**
18
+ * Normalize a directory or file name into a URL-safe slug segment.
19
+ *
20
+ * Matches Astro glob loader behavior:
21
+ * - lowercases the name
22
+ * - replaces whitespace runs with a single hyphen
23
+ *
24
+ * Examples:
25
+ * "Prompt Optimization" → "prompt-optimization"
26
+ * "Introduction" → "introduction"
27
+ * "getting-started" → "getting-started"
28
+ */
29
+ export function slugify(name) {
30
+ return name.toLowerCase().replace(/\s+/g, "-");
31
+ }
17
32
  /**
18
33
  * Build a navigation tree by recursively walking a content directory.
19
34
  *
@@ -87,10 +102,12 @@ async function buildItems(dirPath, slugPrefix) {
87
102
  }
88
103
  // Check if entry matches a directory
89
104
  if (dirs.includes(entry.key)) {
90
- const children = await buildItems(join(dirPath, entry.key), slugPrefix ? `${slugPrefix}/${entry.key}` : entry.key);
105
+ const dirSlug = slugify(entry.key);
106
+ const fullSlug = slugPrefix ? `${slugPrefix}/${dirSlug}` : dirSlug;
107
+ const children = await buildItems(join(dirPath, entry.key), fullSlug);
91
108
  result.push({
92
109
  title: entry.title,
93
- slug: slugPrefix ? `${slugPrefix}/${entry.key}` : entry.key,
110
+ slug: fullSlug,
94
111
  type: "section",
95
112
  children,
96
113
  ...(entry.collapsed !== undefined && { collapsed: entry.collapsed }),
@@ -101,10 +118,10 @@ async function buildItems(dirPath, slugPrefix) {
101
118
  // Check if entry matches a file
102
119
  const matchingFile = files.find((f) => parse(f).name === entry.key);
103
120
  if (matchingFile) {
104
- const baseName = parse(matchingFile).name;
121
+ const fileSlug = slugify(parse(matchingFile).name);
105
122
  result.push({
106
123
  title: entry.title,
107
- slug: slugPrefix ? `${slugPrefix}/${baseName}` : baseName,
124
+ slug: slugPrefix ? `${slugPrefix}/${fileSlug}` : fileSlug,
108
125
  type: "page",
109
126
  ...(entry.icon !== undefined && { icon: entry.icon }),
110
127
  });
@@ -117,19 +134,22 @@ async function buildItems(dirPath, slugPrefix) {
117
134
  .sort();
118
135
  for (const file of unlistedFiles) {
119
136
  const baseName = parse(file).name;
137
+ const fileSlug = slugify(baseName);
120
138
  result.push({
121
139
  title: toTitleCase(baseName),
122
- slug: slugPrefix ? `${slugPrefix}/${baseName}` : baseName,
140
+ slug: slugPrefix ? `${slugPrefix}/${fileSlug}` : fileSlug,
123
141
  type: "page",
124
142
  });
125
143
  }
126
144
  // Append unlisted directories alphabetically
127
145
  const unlistedDirs = dirs.filter((d) => !listedKeys.has(d)).sort();
128
146
  for (const dir of unlistedDirs) {
129
- const children = await buildItems(join(dirPath, dir), slugPrefix ? `${slugPrefix}/${dir}` : dir);
147
+ const dirSlug = slugify(dir);
148
+ const fullSlug = slugPrefix ? `${slugPrefix}/${dirSlug}` : dirSlug;
149
+ const children = await buildItems(join(dirPath, dir), fullSlug);
130
150
  result.push({
131
151
  title: toTitleCase(dir),
132
- slug: slugPrefix ? `${slugPrefix}/${dir}` : dir,
152
+ slug: fullSlug,
133
153
  type: "section",
134
154
  children,
135
155
  });
@@ -146,7 +166,8 @@ async function buildAlphabetical(files, dirs, dirPath, slugPrefix) {
146
166
  ].sort((a, b) => a.name.localeCompare(b.name));
147
167
  const result = [];
148
168
  for (const entry of allEntries) {
149
- const slug = slugPrefix ? `${slugPrefix}/${entry.name}` : entry.name;
169
+ const entrySlug = slugify(entry.name);
170
+ const slug = slugPrefix ? `${slugPrefix}/${entrySlug}` : entrySlug;
150
171
  if (entry.isDir) {
151
172
  const children = await buildItems(join(dirPath, entry.name), slug);
152
173
  result.push({
@@ -190,10 +190,12 @@ export function generatePythonExample(endpoint, options = DEFAULT_OPTIONS) {
190
190
  * Convert JSON string to Python dict syntax (True/False/None).
191
191
  */
192
192
  function pythonifyJson(json) {
193
+ // Only replace JSON values (not strings containing these words).
194
+ // Match `: true`, `: false`, `: null` when followed by comma, newline, or end-of-object/array.
193
195
  return json
194
- .replace(/: true/g, ": True")
195
- .replace(/: false/g, ": False")
196
- .replace(/: null/g, ": None");
196
+ .replace(/: true(?=[,\n\r\s}\]]|$)/g, ": True")
197
+ .replace(/: false(?=[,\n\r\s}\]]|$)/g, ": False")
198
+ .replace(/: null(?=[,\n\r\s}\]]|$)/g, ": None");
197
199
  }
198
200
  /* ------------------------------------------------------------------ */
199
201
  /* Node.js Generator */
@@ -21,7 +21,14 @@ export function generateExampleValue(schema, depth = 0) {
21
21
  if (depth > MAX_DEPTH) {
22
22
  return getTypeDefault(schema.type);
23
23
  }
24
- // 1. Prefer enum first value
24
+ // 1. Prefer spec-provided example values (AC6)
25
+ if (schema.example !== undefined) {
26
+ return schema.example;
27
+ }
28
+ if (schema.examples && Array.isArray(schema.examples) && schema.examples.length > 0) {
29
+ return schema.examples[0];
30
+ }
31
+ // 2. Prefer enum first value
25
32
  if (schema.enum && schema.enum.length > 0) {
26
33
  return schema.enum[0];
27
34
  }
@@ -16,6 +16,10 @@ export interface ApiSchema {
16
16
  required?: string[];
17
17
  enum?: unknown[];
18
18
  description?: string;
19
+ /** Spec-provided example value for this schema */
20
+ example?: unknown;
21
+ /** Spec-provided array of example values for this schema */
22
+ examples?: unknown[];
19
23
  /** Original $ref path, preserved for display (e.g., "#/components/schemas/Pet") */
20
24
  ref?: string;
21
25
  allOf?: ApiSchema[];
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import type { ApiEndpoint } from "./types.js";
9
+ import type { ApiEndpoint, ApiEndpointError } from "./types.js";
10
10
  /**
11
11
  * Build a URL-friendly slug for an API endpoint.
12
12
  *
@@ -17,6 +17,18 @@ import type { ApiEndpoint } from "./types.js";
17
17
  * // → "pets/get-pets-petId"
18
18
  */
19
19
  export declare function buildEndpointSlug(endpoint: ApiEndpoint): string;
20
+ /**
21
+ * Build a URL-friendly slug for an errored API endpoint.
22
+ *
23
+ * Uses `_unsupported/` prefix to avoid collisions with valid endpoint slugs.
24
+ *
25
+ * Format: `_unsupported/{method}-{sanitized-path}`
26
+ *
27
+ * @example
28
+ * buildErrorEndpointSlug({ method: "get", path: "/pets/{petId}", reason: "...", rawSpec: {} })
29
+ * // → "_unsupported/get-pets-petId"
30
+ */
31
+ export declare function buildErrorEndpointSlug(error: ApiEndpointError): string;
20
32
  /**
21
33
  * Build a unique identifier for an endpoint (used for active-state matching).
22
34
  *
@@ -23,6 +23,24 @@ export function buildEndpointSlug(endpoint) {
23
23
  .replace(/[{}]/g, "");
24
24
  return `${tag}/${endpoint.method}-${sanitizedPath}`;
25
25
  }
26
+ /**
27
+ * Build a URL-friendly slug for an errored API endpoint.
28
+ *
29
+ * Uses `_unsupported/` prefix to avoid collisions with valid endpoint slugs.
30
+ *
31
+ * Format: `_unsupported/{method}-{sanitized-path}`
32
+ *
33
+ * @example
34
+ * buildErrorEndpointSlug({ method: "get", path: "/pets/{petId}", reason: "...", rawSpec: {} })
35
+ * // → "_unsupported/get-pets-petId"
36
+ */
37
+ export function buildErrorEndpointSlug(error) {
38
+ const sanitizedPath = error.path
39
+ .replace(/^\//, "")
40
+ .replace(/\//g, "-")
41
+ .replace(/[{}]/g, "");
42
+ return `_unsupported/${error.method}-${sanitizedPath}`;
43
+ }
26
44
  /**
27
45
  * Build a unique identifier for an endpoint (used for active-state matching).
28
46
  *
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Frontmatter validator — validates MDX/MD file frontmatter against the content schema.
3
+ *
4
+ * Composable validator following the pattern established in link-validator.ts.
5
+ * Reuses the existing `frontmatterSchema` Zod schema from content/frontmatter-schema.ts.
6
+ *
7
+ * Error codes:
8
+ * FRONTMATTER_MISSING_FIELD — Required field not present
9
+ * FRONTMATTER_TYPE_MISMATCH — Field present but wrong type
10
+ * FRONTMATTER_UNKNOWN_FIELD — Field not in schema (warning)
11
+ * FRONTMATTER_PARSE_ERROR — YAML block is malformed
12
+ * FRONTMATTER_INVALID — Catch-all for other Zod issues
13
+ */
14
+ import type { ValidationResult, ValidatorContext } from "./types.js";
15
+ /**
16
+ * Extract raw frontmatter string from file content.
17
+ *
18
+ * Frontmatter is the YAML block between the first pair of `---` delimiters
19
+ * at the top of the file.
20
+ *
21
+ * @returns The raw YAML string, or null if no frontmatter block found.
22
+ */
23
+ export declare function extractFrontmatterRaw(content: string): string | null;
24
+ /**
25
+ * Parse a raw YAML string into a plain object.
26
+ *
27
+ * Uses js-yaml (available as transitive dependency) for YAML parsing.
28
+ *
29
+ * @throws Error with a descriptive message if YAML is malformed.
30
+ */
31
+ export declare function parseFrontmatterYaml(raw: string): Record<string, unknown>;
32
+ /**
33
+ * Detect unknown frontmatter fields not in the schema.
34
+ *
35
+ * Fields starting with `_` (Astro internals) are ignored.
36
+ * Unknown fields are reported as warnings, not errors.
37
+ */
38
+ export declare function detectUnknownFields(frontmatter: Record<string, unknown>, filePath: string): ValidationResult[];
39
+ /**
40
+ * Validate a single file's frontmatter against the schema.
41
+ *
42
+ * Returns an array of ValidationResult for all issues found.
43
+ */
44
+ export declare function validateSingleFile(filePath: string): Promise<ValidationResult[]>;
45
+ /**
46
+ * Composable frontmatter validator.
47
+ *
48
+ * Discovers all MDX/MD files in the content directory and validates
49
+ * each file's frontmatter against the Zod content schema.
50
+ *
51
+ * @param context - Validator context with contentDir
52
+ * @returns Array of ValidationResult for all frontmatter issues
53
+ */
54
+ export declare function validateFrontmatter(context: ValidatorContext): Promise<ValidationResult[]>;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Frontmatter validator — validates MDX/MD file frontmatter against the content schema.
3
+ *
4
+ * Composable validator following the pattern established in link-validator.ts.
5
+ * Reuses the existing `frontmatterSchema` Zod schema from content/frontmatter-schema.ts.
6
+ *
7
+ * Error codes:
8
+ * FRONTMATTER_MISSING_FIELD — Required field not present
9
+ * FRONTMATTER_TYPE_MISMATCH — Field present but wrong type
10
+ * FRONTMATTER_UNKNOWN_FIELD — Field not in schema (warning)
11
+ * FRONTMATTER_PARSE_ERROR — YAML block is malformed
12
+ * FRONTMATTER_INVALID — Catch-all for other Zod issues
13
+ */
14
+ import { readFile } from "node:fs/promises";
15
+ import yaml from "js-yaml";
16
+ import { frontmatterSchema } from "../content/frontmatter-schema.js";
17
+ import { discoverContentFiles } from "./link-validator.js";
18
+ /**
19
+ * Known frontmatter schema keys, derived from the Zod schema shape.
20
+ * Keys starting with `_` (Astro internals like `_id`, `_collection`) are always allowed.
21
+ */
22
+ const KNOWN_KEYS = new Set(Object.keys(frontmatterSchema.shape));
23
+ /**
24
+ * Extract raw frontmatter string from file content.
25
+ *
26
+ * Frontmatter is the YAML block between the first pair of `---` delimiters
27
+ * at the top of the file.
28
+ *
29
+ * @returns The raw YAML string, or null if no frontmatter block found.
30
+ */
31
+ export function extractFrontmatterRaw(content) {
32
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
33
+ return match ? match[1] : null;
34
+ }
35
+ /**
36
+ * Parse a raw YAML string into a plain object.
37
+ *
38
+ * Uses js-yaml (available as transitive dependency) for YAML parsing.
39
+ *
40
+ * @throws Error with a descriptive message if YAML is malformed.
41
+ */
42
+ export function parseFrontmatterYaml(raw) {
43
+ const parsed = yaml.load(raw);
44
+ if (parsed === null || parsed === undefined) {
45
+ return {};
46
+ }
47
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
48
+ throw new Error("Frontmatter must be a YAML mapping, not a scalar or list");
49
+ }
50
+ return parsed;
51
+ }
52
+ /**
53
+ * Map a single Zod issue to a ValidationResult.
54
+ */
55
+ function zodIssueToResult(issue, filePath) {
56
+ const fieldPath = issue.path.join(".") || "unknown";
57
+ // Missing required field
58
+ if (issue.code === "invalid_type" && issue.received === "undefined") {
59
+ const hint = fieldPath === "title"
60
+ ? 'Field \'title\' is required — add `title: "Page Title"` to your frontmatter'
61
+ : `Field '${fieldPath}' is required`;
62
+ return {
63
+ code: "FRONTMATTER_MISSING_FIELD",
64
+ message: `Missing required field '${fieldPath}' in ${filePath}`,
65
+ filePath,
66
+ line: 1,
67
+ severity: "error",
68
+ hint,
69
+ };
70
+ }
71
+ // Type mismatch
72
+ if (issue.code === "invalid_type") {
73
+ return {
74
+ code: "FRONTMATTER_TYPE_MISMATCH",
75
+ message: `Type mismatch for field '${fieldPath}' in ${filePath}: expected ${issue.expected}, got ${issue.received}`,
76
+ filePath,
77
+ line: 1,
78
+ severity: "error",
79
+ hint: `Field '${fieldPath}' expected type '${issue.expected}', got '${issue.received}'`,
80
+ };
81
+ }
82
+ // Catch-all for other Zod issues
83
+ return {
84
+ code: "FRONTMATTER_INVALID",
85
+ message: `Invalid field '${fieldPath}' in ${filePath}: ${issue.message}`,
86
+ filePath,
87
+ line: 1,
88
+ severity: "error",
89
+ hint: `Field '${fieldPath}': ${issue.message}`,
90
+ };
91
+ }
92
+ /**
93
+ * Detect unknown frontmatter fields not in the schema.
94
+ *
95
+ * Fields starting with `_` (Astro internals) are ignored.
96
+ * Unknown fields are reported as warnings, not errors.
97
+ */
98
+ export function detectUnknownFields(frontmatter, filePath) {
99
+ const results = [];
100
+ for (const key of Object.keys(frontmatter)) {
101
+ if (key.startsWith("_"))
102
+ continue;
103
+ if (!KNOWN_KEYS.has(key)) {
104
+ results.push({
105
+ code: "FRONTMATTER_UNKNOWN_FIELD",
106
+ message: `Unknown frontmatter field '${key}' in ${filePath}`,
107
+ filePath,
108
+ line: 1,
109
+ severity: "warning",
110
+ hint: `Consider moving to \`custom.${key}\` for forward compatibility`,
111
+ });
112
+ }
113
+ }
114
+ return results;
115
+ }
116
+ /**
117
+ * Validate a single file's frontmatter against the schema.
118
+ *
119
+ * Returns an array of ValidationResult for all issues found.
120
+ */
121
+ export async function validateSingleFile(filePath) {
122
+ const results = [];
123
+ let content;
124
+ try {
125
+ content = await readFile(filePath, "utf-8");
126
+ }
127
+ catch {
128
+ results.push({
129
+ code: "FRONTMATTER_PARSE_ERROR",
130
+ message: `Cannot read file: ${filePath}`,
131
+ filePath,
132
+ line: 1,
133
+ severity: "error",
134
+ hint: "Verify the file exists and is readable",
135
+ });
136
+ return results;
137
+ }
138
+ // Extract frontmatter block
139
+ const raw = extractFrontmatterRaw(content);
140
+ if (raw === null) {
141
+ // No frontmatter block — report missing required title
142
+ results.push({
143
+ code: "FRONTMATTER_MISSING_FIELD",
144
+ message: `No frontmatter block found in ${filePath}`,
145
+ filePath,
146
+ line: 1,
147
+ severity: "error",
148
+ hint: 'Add a frontmatter block with at least `title`: `---\ntitle: "Page Title"\n---`',
149
+ });
150
+ return results;
151
+ }
152
+ // Parse YAML
153
+ let parsed;
154
+ try {
155
+ parsed = parseFrontmatterYaml(raw);
156
+ }
157
+ catch (err) {
158
+ const message = err instanceof Error ? err.message : "Unknown YAML parse error";
159
+ results.push({
160
+ code: "FRONTMATTER_PARSE_ERROR",
161
+ message: `Malformed frontmatter in ${filePath}: ${message}`,
162
+ filePath,
163
+ line: 1,
164
+ severity: "error",
165
+ hint: "Fix the YAML syntax in the frontmatter block",
166
+ });
167
+ return results;
168
+ }
169
+ // Validate against Zod schema
170
+ const zodResult = frontmatterSchema.safeParse(parsed);
171
+ if (!zodResult.success) {
172
+ for (const issue of zodResult.error.issues) {
173
+ results.push(zodIssueToResult(issue, filePath));
174
+ }
175
+ }
176
+ // Detect unknown fields (even if Zod validation passed due to .passthrough())
177
+ results.push(...detectUnknownFields(parsed, filePath));
178
+ return results;
179
+ }
180
+ /**
181
+ * Composable frontmatter validator.
182
+ *
183
+ * Discovers all MDX/MD files in the content directory and validates
184
+ * each file's frontmatter against the Zod content schema.
185
+ *
186
+ * @param context - Validator context with contentDir
187
+ * @returns Array of ValidationResult for all frontmatter issues
188
+ */
189
+ export async function validateFrontmatter(context) {
190
+ const files = await discoverContentFiles(context.contentDir);
191
+ const results = [];
192
+ for (const filePath of files) {
193
+ const fileResults = await validateSingleFile(filePath);
194
+ results.push(...fileResults);
195
+ }
196
+ return results;
197
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Validation runner — collects results from composable validators.
3
+ *
4
+ * Usage:
5
+ * const results = await runValidators([validateLinks, validateFrontmatter], context);
6
+ */
7
+ import type { ValidationResult, ValidatorContext, Validator } from "./types.js";
8
+ export { type ValidationResult, type ValidationSeverity, type ValidatorContext, type Validator, toSpecglassError } from "./types.js";
9
+ export { validateFrontmatter } from "./frontmatter-validator.js";
10
+ export { validateSpecDrift } from "./spec-drift-validator.js";
11
+ /**
12
+ * Run multiple validators and collect all results.
13
+ *
14
+ * Each validator runs independently and returns its findings.
15
+ * Results are concatenated in validator order.
16
+ */
17
+ export declare function runValidators(validators: Validator[], context: ValidatorContext): Promise<ValidationResult[]>;
@@ -0,0 +1,17 @@
1
+ export { toSpecglassError } from "./types.js";
2
+ export { validateFrontmatter } from "./frontmatter-validator.js";
3
+ export { validateSpecDrift } from "./spec-drift-validator.js";
4
+ /**
5
+ * Run multiple validators and collect all results.
6
+ *
7
+ * Each validator runs independently and returns its findings.
8
+ * Results are concatenated in validator order.
9
+ */
10
+ export async function runValidators(validators, context) {
11
+ const results = [];
12
+ for (const validator of validators) {
13
+ const findings = await validator(context);
14
+ results.push(...findings);
15
+ }
16
+ return results;
17
+ }
@@ -0,0 +1,84 @@
1
+ import type { ValidationResult, ValidatorContext } from "./types.js";
2
+ export declare const LINK_INTERNAL_BROKEN = "LINK_INTERNAL_BROKEN";
3
+ export declare const LINK_INTERNAL_ANCHOR_BROKEN = "LINK_INTERNAL_ANCHOR_BROKEN";
4
+ export declare const LINK_EXTERNAL_UNREACHABLE = "LINK_EXTERNAL_UNREACHABLE";
5
+ /** A link extracted from source content. */
6
+ export interface ExtractedLink {
7
+ /** The raw href/URL from the link. */
8
+ target: string;
9
+ /** 1-indexed line number in the source file. */
10
+ line: number;
11
+ /** Whether this is an external (http/https) link. */
12
+ isExternal: boolean;
13
+ }
14
+ /**
15
+ * Extract all links from MDX/MD content string.
16
+ *
17
+ * Finds both markdown links `[text](url)` and HTML `<a href="url">` links.
18
+ * Returns line numbers for each extracted link.
19
+ */
20
+ export declare function extractLinks(content: string): ExtractedLink[];
21
+ /** Check if a URL is external (http:// or https://). */
22
+ export declare function isExternalLink(target: string): boolean;
23
+ /**
24
+ * Resolve and validate all internal links in a single file.
25
+ */
26
+ export declare function resolveInternalLinks(links: ExtractedLink[], sourceFilePath: string, contentDir: string, knownPages: string[]): Promise<ValidationResult[]>;
27
+ /** Resolve a link path to an absolute file path. */
28
+ export declare function resolvePagePath(linkPath: string, sourceFilePath: string, contentDir: string): string;
29
+ /**
30
+ * Check if a resolved path corresponds to an existing content file.
31
+ * Handles extension normalization (.mdx/.md), index files, and trailing slashes.
32
+ */
33
+ export declare function checkFileExists(resolvedPath: string, contentDir: string, _knownPages: string[]): Promise<{
34
+ exists: boolean;
35
+ resolvedPath?: string;
36
+ }>;
37
+ /**
38
+ * Check if a heading anchor exists in a markdown file.
39
+ * Extracts headings and converts them to slug IDs.
40
+ */
41
+ export declare function checkAnchorInFile(anchor: string, filePath: string, sourceLine: number, sourceFilePath?: string): Promise<ValidationResult | null>;
42
+ /**
43
+ * Extract heading IDs from markdown content.
44
+ * Converts headings to GitHub-style slug IDs.
45
+ * Filters out headings that appear inside fenced code blocks.
46
+ */
47
+ export declare function extractHeadingIds(content: string): string[];
48
+ /**
49
+ * Convert a heading text to a GitHub-style anchor slug.
50
+ * e.g., "Getting Started" → "getting-started"
51
+ */
52
+ export declare function headingToSlug(text: string): string;
53
+ /**
54
+ * Check external links for reachability via HTTP HEAD requests.
55
+ * Failures are reported as warnings, not errors.
56
+ */
57
+ export declare function checkExternalLinks(links: ExtractedLink[], sourceFilePath: string): Promise<ValidationResult[]>;
58
+ /**
59
+ * Check a single external link via HTTP HEAD request.
60
+ * Returns a ValidationResult for unreachable links, null for valid ones.
61
+ */
62
+ export declare function checkSingleExternalLink(link: ExtractedLink, sourceFilePath: string): Promise<ValidationResult | null>;
63
+ /**
64
+ * Find a suggested page for a broken internal link.
65
+ * Simple string distance comparison against known pages.
66
+ */
67
+ export declare function findSuggestion(brokenPath: string, knownPages: string[]): string | null;
68
+ /** Simple Levenshtein distance implementation. */
69
+ export declare function levenshtein(a: string, b: string): number;
70
+ /**
71
+ * Recursively discover all .md/.mdx files in a directory.
72
+ */
73
+ export declare function discoverContentFiles(dir: string): Promise<string[]>;
74
+ /**
75
+ * Build the list of known page slugs from the content directory.
76
+ */
77
+ export declare function buildKnownPages(contentFiles: string[], contentDir: string): string[];
78
+ /**
79
+ * Link validator — the composable validator function for link checking.
80
+ *
81
+ * @param context - Validator context with contentDir and options
82
+ * @returns Array of validation results (errors for internal, warnings for external)
83
+ */
84
+ export declare function validateLinks(context: ValidatorContext): Promise<ValidationResult[]>;