@vibe-agent-toolkit/resources 0.1.37 → 0.1.39-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.
Files changed (87) hide show
  1. package/README.md +35 -26
  2. package/dist/ajv-factory.d.ts +33 -0
  3. package/dist/ajv-factory.d.ts.map +1 -0
  4. package/dist/ajv-factory.js +51 -0
  5. package/dist/ajv-factory.js.map +1 -0
  6. package/dist/config-parser.d.ts +0 -18
  7. package/dist/config-parser.d.ts.map +1 -1
  8. package/dist/config-parser.js +5 -46
  9. package/dist/config-parser.js.map +1 -1
  10. package/dist/frontmatter-editor.d.ts +45 -0
  11. package/dist/frontmatter-editor.d.ts.map +1 -0
  12. package/dist/frontmatter-editor.js +161 -0
  13. package/dist/frontmatter-editor.js.map +1 -0
  14. package/dist/frontmatter-link-validator.d.ts +5 -5
  15. package/dist/frontmatter-link-validator.d.ts.map +1 -1
  16. package/dist/frontmatter-link-validator.js +25 -24
  17. package/dist/frontmatter-link-validator.js.map +1 -1
  18. package/dist/frontmatter-validator.d.ts +3 -2
  19. package/dist/frontmatter-validator.d.ts.map +1 -1
  20. package/dist/frontmatter-validator.js +19 -20
  21. package/dist/frontmatter-validator.js.map +1 -1
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/json-pointer-path.d.ts +13 -0
  27. package/dist/json-pointer-path.d.ts.map +1 -0
  28. package/dist/json-pointer-path.js +30 -0
  29. package/dist/json-pointer-path.js.map +1 -0
  30. package/dist/link-parser.js +14 -16
  31. package/dist/link-parser.js.map +1 -1
  32. package/dist/link-validator.d.ts +23 -1
  33. package/dist/link-validator.d.ts.map +1 -1
  34. package/dist/link-validator.js +107 -104
  35. package/dist/link-validator.js.map +1 -1
  36. package/dist/multi-schema-validator.d.ts.map +1 -1
  37. package/dist/multi-schema-validator.js +6 -8
  38. package/dist/multi-schema-validator.js.map +1 -1
  39. package/dist/resource-registry.d.ts +10 -2
  40. package/dist/resource-registry.d.ts.map +1 -1
  41. package/dist/resource-registry.js +25 -32
  42. package/dist/resource-registry.js.map +1 -1
  43. package/dist/rewriter-helpers.d.ts +49 -0
  44. package/dist/rewriter-helpers.d.ts.map +1 -0
  45. package/dist/rewriter-helpers.js +142 -0
  46. package/dist/rewriter-helpers.js.map +1 -0
  47. package/dist/schemas/project-config.d.ts +219 -171
  48. package/dist/schemas/project-config.d.ts.map +1 -1
  49. package/dist/schemas/project-config.js +2 -0
  50. package/dist/schemas/project-config.js.map +1 -1
  51. package/dist/schemas/validation-result.d.ts +36 -57
  52. package/dist/schemas/validation-result.d.ts.map +1 -1
  53. package/dist/schemas/validation-result.js +5 -27
  54. package/dist/schemas/validation-result.js.map +1 -1
  55. package/dist/types/resource-parser.d.ts.map +1 -1
  56. package/dist/types/resource-parser.js +2 -3
  57. package/dist/types/resource-parser.js.map +1 -1
  58. package/dist/types/resources.d.ts +1 -1
  59. package/dist/types/resources.d.ts.map +1 -1
  60. package/dist/types/resources.js.map +1 -1
  61. package/dist/types.d.ts +1 -1
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/dist/utils.d.ts +50 -11
  66. package/dist/utils.d.ts.map +1 -1
  67. package/dist/utils.js +53 -13
  68. package/dist/utils.js.map +1 -1
  69. package/package.json +5 -5
  70. package/src/ajv-factory.ts +56 -0
  71. package/src/config-parser.ts +5 -51
  72. package/src/frontmatter-editor.ts +214 -0
  73. package/src/frontmatter-link-validator.ts +23 -25
  74. package/src/frontmatter-validator.ts +29 -22
  75. package/src/index.ts +21 -2
  76. package/src/json-pointer-path.ts +29 -0
  77. package/src/link-parser.ts +27 -20
  78. package/src/link-validator.ts +194 -119
  79. package/src/multi-schema-validator.ts +10 -8
  80. package/src/resource-registry.ts +48 -33
  81. package/src/rewriter-helpers.ts +166 -0
  82. package/src/schemas/project-config.ts +2 -0
  83. package/src/schemas/validation-result.ts +5 -29
  84. package/src/types/resource-parser.ts +2 -3
  85. package/src/types/resources.ts +2 -1
  86. package/src/types.ts +0 -1
  87. package/src/utils.ts +72 -14
package/dist/utils.js CHANGED
@@ -6,6 +6,20 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { toForwardSlash, safePath } from '@vibe-agent-toolkit/utils';
8
8
  import picomatch from 'picomatch';
9
+ /**
10
+ * Compute a `ValidationIssue.location`: the (absolute) source file path made
11
+ * relative to the project root. When no project root is known, fall back to the
12
+ * source path forward-slashed so the location is still useful and stable.
13
+ *
14
+ * @param sourceFilePath - Absolute path to the file the issue was found in.
15
+ * @param projectRoot - Project root, or undefined when none could be determined.
16
+ * @returns Forward-slashed relative location (or the forward-slashed absolute path).
17
+ */
18
+ export function issueLocation(sourceFilePath, projectRoot) {
19
+ return projectRoot === undefined
20
+ ? toForwardSlash(sourceFilePath)
21
+ : safePath.relative(projectRoot, sourceFilePath);
22
+ }
9
23
  /**
10
24
  * Check if a file path matches a glob pattern.
11
25
  *
@@ -64,32 +78,37 @@ export function splitHrefAnchor(href) {
64
78
  return [filePath, anchor];
65
79
  }
66
80
  /**
67
- * Resolve a markdown link href to an absolute filesystem path.
81
+ * Resolve a markdown link href to a filesystem path or a typed failure.
68
82
  *
69
83
  * Performs the standard href → path conversion used by both audit and validate:
70
84
  * 1. Strips anchor fragment (`#section`)
71
85
  * 2. Decodes URL-encoded characters (`%20` → space, `%26` → `&`)
72
- * 3. Resolves the path relative to the source file's directory
73
- *
74
- * Returns `null` for anchor-only links (e.g., `#heading`).
86
+ * 3. Resolves the path:
87
+ * - Leading `/` (RFC 3986 §4.2 absolute-path reference) → resolve against
88
+ * `projectRoot`. Requires a `projectRoot`; must not escape it.
89
+ * - Otherwise → resolve relative to the source file's directory.
75
90
  *
76
91
  * @param href - Raw href from a markdown link
77
92
  * @param sourceFilePath - Absolute path of the file containing the link
78
- * @returns Resolved path info, or null for anchor-only links
93
+ * @param projectRoot - Optional project root for absolute-path references.
94
+ * @returns A {@link ResolveLocalHrefResult} discriminating success vs failure modes.
79
95
  *
80
96
  * @example
81
97
  * ```typescript
82
98
  * resolveLocalHref('My%20Folder/doc.md#intro', '/project/README.md')
83
- * // { resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
99
+ * // { kind: 'resolved', resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
84
100
  *
85
101
  * resolveLocalHref('#heading', '/project/README.md')
86
- * // null
102
+ * // { kind: 'anchor_only' }
103
+ *
104
+ * resolveLocalHref('/docs/foo.md', '/project/docs/sub/page.md', '/project')
105
+ * // { kind: 'resolved', resolvedPath: '/project/docs/foo.md', anchor: undefined }
87
106
  * ```
88
107
  */
89
- export function resolveLocalHref(href, sourceFilePath) {
108
+ export function resolveLocalHref(href, sourceFilePath, projectRoot) {
90
109
  const [fileHref, anchor] = splitHrefAnchor(href);
91
110
  if (fileHref === '') {
92
- return null;
111
+ return { kind: 'anchor_only' };
93
112
  }
94
113
  let decodedHref;
95
114
  try {
@@ -98,9 +117,21 @@ export function resolveLocalHref(href, sourceFilePath) {
98
117
  catch {
99
118
  decodedHref = fileHref;
100
119
  }
120
+ // RFC 3986 §4.2 absolute-path reference — resolve against projectRoot.
121
+ if (decodedHref.startsWith('/')) {
122
+ if (!projectRoot) {
123
+ return { kind: 'absolute_no_root', href: fileHref, anchor };
124
+ }
125
+ const candidate = safePath.resolve(projectRoot, decodedHref.slice(1));
126
+ if (!isWithinProject(candidate, projectRoot)) {
127
+ return { kind: 'absolute_escapes_root', href: fileHref, anchor };
128
+ }
129
+ return { kind: 'resolved', resolvedPath: candidate, anchor };
130
+ }
131
+ // Relative reference — resolve against the source file's directory.
101
132
  const sourceDir = path.dirname(sourceFilePath);
102
133
  const resolvedPath = safePath.resolve(sourceDir, decodedHref);
103
- return { resolvedPath, anchor };
134
+ return { kind: 'resolved', resolvedPath, anchor };
104
135
  }
105
136
  /**
106
137
  * Check if a file path is within a project directory.
@@ -120,7 +151,9 @@ export function resolveLocalHref(href, sourceFilePath) {
120
151
  * ```
121
152
  */
122
153
  export function isWithinProject(filePath, projectRoot) {
123
- // Resolve symlinks to get real paths
154
+ // Canonicalize both sides symmetrically. Asymmetric handling (realpath one
155
+ // side, resolve the other) false-flags legitimate matches when projectRoot
156
+ // traverses a symlink — e.g. macOS /tmp → /private/tmp, bind mounts.
124
157
  let resolvedFilePath;
125
158
  try {
126
159
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- filePath is validated path parameter
@@ -130,7 +163,14 @@ export function isWithinProject(filePath, projectRoot) {
130
163
  // If realpath fails, file doesn't exist - use original path
131
164
  resolvedFilePath = safePath.resolve(filePath);
132
165
  }
133
- const resolvedProjectRoot = safePath.resolve(projectRoot);
166
+ let resolvedProjectRoot;
167
+ try {
168
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- projectRoot is validated path parameter
169
+ resolvedProjectRoot = fs.realpathSync(projectRoot);
170
+ }
171
+ catch {
172
+ resolvedProjectRoot = safePath.resolve(projectRoot);
173
+ }
134
174
  // Normalize to forward slashes for cross-platform comparison
135
175
  const normalizedFile = toForwardSlash(resolvedFilePath);
136
176
  const normalizedRoot = toForwardSlash(resolvedProjectRoot);
@@ -179,7 +219,7 @@ export function formatJsonPointerAsDotted(pointer) {
179
219
  }
180
220
  return out;
181
221
  }
182
- function isCanonicalArrayIndex(s) {
222
+ export function isCanonicalArrayIndex(s) {
183
223
  // Canonical integer per RFC 6901 §4 + JSON canonical form: no leading zeros
184
224
  // except for "0" itself.
185
225
  if (s === '')
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-agent-toolkit/resources",
3
- "version": "0.1.37",
3
+ "version": "0.1.39-rc.1",
4
4
  "description": "Markdown resource parsing, validation, and link integrity checking",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,11 +33,11 @@
33
33
  "author": "Jeff Dutton",
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@vibe-agent-toolkit/agent-schema": "0.1.37",
37
- "@vibe-agent-toolkit/utils": "0.1.37",
36
+ "@vibe-agent-toolkit/agent-schema": "0.1.39-rc.1",
37
+ "@vibe-agent-toolkit/utils": "0.1.39-rc.1",
38
38
  "ajv": "^8.17.1",
39
+ "ajv-formats": "^3.0.1",
39
40
  "github-slugger": "^2.0.0",
40
- "js-yaml": "^4.1.1",
41
41
  "markdown-link-check": "^3.14.2",
42
42
  "picomatch": "^4.0.3",
43
43
  "remark-frontmatter": "^5.0.0",
@@ -46,10 +46,10 @@
46
46
  "semver": "^7.7.3",
47
47
  "unified": "^11.0.5",
48
48
  "unist-util-visit": "^5.0.0",
49
+ "yaml": "^2.6.1",
49
50
  "zod": "^3.24.1"
50
51
  },
51
52
  "devDependencies": {
52
- "@types/js-yaml": "^4.0.9",
53
53
  "@types/mdast": "^4.0.4",
54
54
  "@types/node": "^22.10.5",
55
55
  "@types/picomatch": "^4.0.2",
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Ajv factory for adopters consuming VAT-generated schemas.
3
+ *
4
+ * VAT's frontmatter walker treats `format: "uri-reference"` (plus `uri`,
5
+ * `iri`, `iri-reference`) as first-class URI families and validates the
6
+ * referenced files via {@link import('./utils.js').resolveLocalHref}, not via
7
+ * Ajv. But adopters consuming the same schemas with vanilla
8
+ * `new Ajv(...)` hit Ajv's default strict mode, which upgrades
9
+ * `unknown format "uri-reference" ignored` from a warning to a thrown error.
10
+ *
11
+ * This helper returns an Ajv instance with the standard JSON Schema format
12
+ * vocabulary registered (via `ajv-formats`) plus no-op shims for
13
+ * `iri` / `iri-reference` (which `ajv-formats` does not ship). All
14
+ * URI-family schemas compile cleanly under strict mode.
15
+ */
16
+
17
+ import { Ajv, type Options as AjvOptions } from 'ajv';
18
+ // ajv-formats is a CJS module published with `module.exports = formatsPlugin`
19
+ // plus an `exports.default` alias. Under NodeNext module resolution the
20
+ // default import is typed as the namespace object (not callable), even
21
+ // though the runtime value IS the plugin function. The `.default ??
22
+ // namespace` pattern below resolves both at type level and runtime.
23
+ import * as ajvFormatsModule from 'ajv-formats';
24
+
25
+ type AddFormatsFn = (ajv: Ajv) => Ajv;
26
+
27
+ const addFormats: AddFormatsFn =
28
+ (ajvFormatsModule as unknown as { default?: AddFormatsFn }).default ??
29
+ (ajvFormatsModule as unknown as AddFormatsFn);
30
+
31
+ /**
32
+ * Construct an Ajv instance pre-registered with the URI-family formats VAT
33
+ * schemas use. Use this anywhere downstream code compiles a schema that may
34
+ * reference `format: "uri-reference"` (or `uri`, `iri`, `iri-reference`).
35
+ *
36
+ * @param options - Ajv options. Passed through unchanged — caller controls
37
+ * `allErrors`, `strict`, `allowUnionTypes`, `verbose`, etc.
38
+ *
39
+ * @example
40
+ * import { createAjvWithUriFormats } from '@vibe-agent-toolkit/resources';
41
+ *
42
+ * const ajv = createAjvWithUriFormats({ allErrors: true });
43
+ * const validate = ajv.compile(mySchemaWithUriReference);
44
+ * if (!validate(data)) console.error(validate.errors);
45
+ */
46
+ export function createAjvWithUriFormats(options: AjvOptions = {}): Ajv {
47
+ const ajv = new Ajv(options);
48
+ addFormats(ajv);
49
+ // ajv-formats does not register `iri` / `iri-reference`. Adopters whose
50
+ // schemas declare those would still hit "unknown format" under strict
51
+ // mode. Register no-op validators — Ajv accepts the format token, and
52
+ // semantic validation happens through resolveLocalHref / equivalent.
53
+ ajv.addFormat('iri', true);
54
+ ajv.addFormat('iri-reference', true);
55
+ return ajv;
56
+ }
@@ -5,58 +5,12 @@
5
5
  */
6
6
 
7
7
  import { readFile } from 'node:fs/promises';
8
- import path from 'node:path';
9
8
 
10
- import { safePath } from '@vibe-agent-toolkit/utils';
11
- import { CORE_SCHEMA, load as loadYaml } from 'js-yaml';
9
+ import { findConfigFile } from '@vibe-agent-toolkit/utils';
10
+ import { parse as parseYaml } from 'yaml';
12
11
 
13
12
  import { ProjectConfigSchema, type ProjectConfig } from './schemas/project-config.js';
14
13
 
15
- const CONFIG_FILENAME = 'vibe-agent-toolkit.config.yaml';
16
-
17
- /**
18
- * Find the config file by walking up the directory tree.
19
- *
20
- * Starts from the current directory and walks up until the config file is found
21
- * or the root directory is reached.
22
- *
23
- * @param startDir - Directory to start searching from (default: process.cwd())
24
- * @returns Absolute path to config file, or undefined if not found
25
- *
26
- * @example
27
- * ```typescript
28
- * const configPath = await findConfigFile();
29
- * if (configPath) {
30
- * console.log(`Found config: ${configPath}`);
31
- * }
32
- * ```
33
- */
34
- export async function findConfigFile(startDir: string = process.cwd()): Promise<string | undefined> {
35
- let currentDir = safePath.resolve(startDir);
36
- const { root } = path.parse(currentDir);
37
-
38
- while (true) {
39
- const configPath = safePath.join(currentDir, CONFIG_FILENAME);
40
-
41
- try {
42
- // Check if file exists by attempting to read metadata
43
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- constructing path during tree walk
44
- await readFile(configPath, 'utf-8');
45
- return configPath;
46
- } catch {
47
- // File doesn't exist, continue walking up
48
- }
49
-
50
- // Check if we've reached the root
51
- if (currentDir === root) {
52
- return undefined;
53
- }
54
-
55
- // Move up one directory
56
- currentDir = path.dirname(currentDir);
57
- }
58
- }
59
-
60
14
  /**
61
15
  * Parse a project configuration file.
62
16
  *
@@ -81,8 +35,7 @@ export async function parseConfigFile(configPath: string): Promise<ProjectConfig
81
35
  // Parse YAML
82
36
  let parsed: unknown;
83
37
  try {
84
- // CORE_SCHEMA: YAML 1.2 spec — see link-parser.ts for rationale.
85
- parsed = loadYaml(content, { schema: CORE_SCHEMA });
38
+ parsed = parseYaml(content);
86
39
  } catch (error) {
87
40
  throw new Error(`Invalid YAML in config file: ${error instanceof Error ? error.message : String(error)}`);
88
41
  }
@@ -118,7 +71,8 @@ export async function parseConfigFile(configPath: string): Promise<ProjectConfig
118
71
  * ```
119
72
  */
120
73
  export async function loadConfig(startDir: string = process.cwd()): Promise<ProjectConfig | undefined> {
121
- const configPath = await findConfigFile(startDir);
74
+ // findConfigFile from utils is synchronous; awaiting a non-promise is a no-op.
75
+ const configPath = findConfigFile(startDir);
122
76
  if (!configPath) {
123
77
  return undefined;
124
78
  }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * FrontmatterEditor — round-trip-safe primitive for editing YAML frontmatter
3
+ * in markdown files.
4
+ *
5
+ * Public surface: openFrontmatter(markdown) → FrontmatterEditor.
6
+ *
7
+ * Round-trip identity contract: openFrontmatter(x).toString() === x for any
8
+ * well-formed input, byte-for-byte. Mutations preserve comments, blank lines,
9
+ * key ordering, quoting style, and detected EOL.
10
+ *
11
+ * See docs/superpowers/specs/2026-05-17-frontmatter-editor-and-yaml-consolidation-design.md
12
+ * §5 for the full contract.
13
+ */
14
+
15
+ import { Document, parseDocument } from 'yaml';
16
+
17
+ export class FrontmatterParseError extends Error {
18
+ public override readonly cause: unknown;
19
+
20
+ constructor(message: string, cause: unknown) {
21
+ super(message);
22
+ this.name = 'FrontmatterParseError';
23
+ this.cause = cause;
24
+ }
25
+ }
26
+
27
+ /** Path to a value in the parsed frontmatter document. */
28
+ export type FrontmatterPath = string | readonly (string | number)[];
29
+
30
+ /** Scalar value type accepted by mutation methods. */
31
+ export type FrontmatterScalar = string | number | boolean | null;
32
+
33
+ export interface FrontmatterEditor {
34
+ body: string;
35
+ get(path: FrontmatterPath): unknown;
36
+ set(path: FrontmatterPath, value: FrontmatterScalar): void;
37
+ setArrayItem(path: FrontmatterPath, index: number, value: FrontmatterScalar): void;
38
+ appendArrayItem(path: FrontmatterPath, value: FrontmatterScalar): void;
39
+ delete(path: FrontmatterPath): void;
40
+ toString(): string;
41
+ /**
42
+ * Returns true if any mutating method has been called or `body` was
43
+ * reassigned to a different string. Use to gate writeFileSync and avoid
44
+ * the no-op-rewrite churn described in §"What's preserved, what isn't"
45
+ * of the markdown-rewriting skill.
46
+ *
47
+ * **Caveat:** any call to `set` / `setArrayItem` / `appendArrayItem` /
48
+ * `delete` flips the flag, even if the underlying value didn't change
49
+ * (e.g. `set('foo', sameValue)`). For strict byte-level dirty detection
50
+ * compare `editor.toString() !== originalText` instead. `body =` is the
51
+ * one exception — it only flips dirty on actual string change.
52
+ */
53
+ isDirty(): boolean;
54
+ }
55
+
56
+ interface FrontmatterSplit {
57
+ hasFrontmatter: boolean;
58
+ frontmatterText: string;
59
+ body: string;
60
+ eol: '\n' | '\r\n';
61
+ }
62
+
63
+ const OPENING_FENCE = /^---\r?\n/;
64
+ // Closing fence: either immediately after opening (empty frontmatter), or
65
+ // preceded by a newline. Trailing variants accept newline or EOF.
66
+ const EMPTY_CLOSING_FENCE = /^---(?:\r?\n|$)/;
67
+ const CLOSING_FENCE = /(?:\r?\n---\r?\n|\r?\n---$)/;
68
+
69
+ function detectEol(input: string): '\n' | '\r\n' {
70
+ const firstBreak = input.indexOf('\n');
71
+ if (firstBreak === -1) return '\n';
72
+ return firstBreak > 0 && input.charAt(firstBreak - 1) === '\r' ? '\r\n' : '\n';
73
+ }
74
+
75
+ function splitFrontmatter(input: string): FrontmatterSplit {
76
+ const eol = detectEol(input);
77
+ const openingMatch = OPENING_FENCE.exec(input);
78
+ if (!openingMatch) {
79
+ return { hasFrontmatter: false, frontmatterText: '', body: input, eol };
80
+ }
81
+ const afterOpening = input.slice(openingMatch[0].length);
82
+ // Handle empty frontmatter (closing fence immediately follows opening fence)
83
+ const emptyMatch = EMPTY_CLOSING_FENCE.exec(afterOpening);
84
+ if (emptyMatch) {
85
+ const bodyStart = emptyMatch[0].length;
86
+ return {
87
+ hasFrontmatter: true,
88
+ frontmatterText: '',
89
+ body: afterOpening.slice(bodyStart),
90
+ eol,
91
+ };
92
+ }
93
+ const closingMatch = CLOSING_FENCE.exec(afterOpening);
94
+ if (!closingMatch) {
95
+ return { hasFrontmatter: false, frontmatterText: '', body: input, eol };
96
+ }
97
+ const frontmatterText = afterOpening.slice(0, closingMatch.index);
98
+ const bodyStart = closingMatch.index + closingMatch[0].length;
99
+ const body = afterOpening.slice(bodyStart);
100
+ return { hasFrontmatter: true, frontmatterText, body, eol };
101
+ }
102
+
103
+ class FrontmatterEditorImpl implements FrontmatterEditor {
104
+ private readonly doc: Document.Parsed | Document;
105
+ private readonly hasFrontmatter: boolean;
106
+ private readonly eol: '\n' | '\r\n';
107
+ private _body: string;
108
+ private _dirty = false;
109
+
110
+ get body(): string {
111
+ return this._body;
112
+ }
113
+
114
+ set body(value: string) {
115
+ if (value !== this._body) {
116
+ this._body = value;
117
+ this._dirty = true;
118
+ }
119
+ }
120
+
121
+ isDirty(): boolean {
122
+ return this._dirty;
123
+ }
124
+
125
+ constructor(input: string) {
126
+ const split = splitFrontmatter(input);
127
+ this.hasFrontmatter = split.hasFrontmatter;
128
+ this.eol = split.eol;
129
+ this._body = split.body;
130
+ if (!split.hasFrontmatter) {
131
+ this.doc = new Document({});
132
+ return;
133
+ }
134
+ try {
135
+ this.doc = parseDocument(split.frontmatterText, { prettyErrors: true });
136
+ if (this.doc.errors.length > 0) {
137
+ throw new FrontmatterParseError(
138
+ `Invalid YAML frontmatter: ${this.doc.errors[0]?.message ?? 'unknown error'}`,
139
+ this.doc.errors[0],
140
+ );
141
+ }
142
+ } catch (error) {
143
+ if (error instanceof FrontmatterParseError) throw error;
144
+ throw new FrontmatterParseError(
145
+ `Failed to parse frontmatter: ${error instanceof Error ? error.message : String(error)}`,
146
+ error,
147
+ );
148
+ }
149
+ }
150
+
151
+ private toPath(path: FrontmatterPath): readonly (string | number)[] {
152
+ if (Array.isArray(path)) return path;
153
+ if (typeof path === 'string') return [path];
154
+ return path as readonly (string | number)[];
155
+ }
156
+
157
+ get(path: FrontmatterPath): unknown {
158
+ const segments = this.toPath(path);
159
+ if (segments.length === 0) return this.doc.toJS();
160
+ return this.doc.getIn(segments as Iterable<unknown>, false);
161
+ }
162
+
163
+ set(path: FrontmatterPath, value: FrontmatterScalar): void {
164
+ const segments = this.toPath(path);
165
+ this.doc.setIn(segments as Iterable<unknown>, value);
166
+ this._dirty = true;
167
+ }
168
+
169
+ setArrayItem(path: FrontmatterPath, index: number, value: FrontmatterScalar): void {
170
+ const segments = [...this.toPath(path), index];
171
+ this.doc.setIn(segments as Iterable<unknown>, value);
172
+ this._dirty = true;
173
+ }
174
+
175
+ appendArrayItem(path: FrontmatterPath, value: FrontmatterScalar): void {
176
+ const segments = this.toPath(path);
177
+ this.doc.addIn(segments as Iterable<unknown>, value);
178
+ this._dirty = true;
179
+ }
180
+
181
+ delete(path: FrontmatterPath): void {
182
+ const segments = this.toPath(path);
183
+ this.doc.deleteIn(segments as Iterable<unknown>);
184
+ this._dirty = true;
185
+ }
186
+
187
+ toString(): string {
188
+ // No frontmatter originally, and nothing was added → return body unchanged.
189
+ if (!this.hasFrontmatter && this.isDocEffectivelyEmpty()) {
190
+ return this.body;
191
+ }
192
+ // Empty frontmatter (e.g. `---\n---\n`) where the doc remained empty —
193
+ // preserve the empty fence block without injecting `null` or `{}` between.
194
+ if (this.hasFrontmatter && this.isDocEffectivelyEmpty()) {
195
+ return `---${this.eol}---${this.eol}${this.body}`;
196
+ }
197
+ const fmText = this.doc.toString();
198
+ const normalized = this.eol === '\r\n' ? fmText.replaceAll('\n', '\r\n') : fmText;
199
+ return `---${this.eol}${normalized}---${this.eol}${this.body}`;
200
+ }
201
+
202
+ private isDocEffectivelyEmpty(): boolean {
203
+ const contents = this.doc.contents;
204
+ if (contents === null) return true;
205
+ // yaml.YAMLMap and YAMLSeq expose `items`; an empty map/seq counts as empty.
206
+ const maybeItems = (contents as { items?: unknown[] }).items;
207
+ if (Array.isArray(maybeItems) && maybeItems.length === 0) return true;
208
+ return false;
209
+ }
210
+ }
211
+
212
+ export function openFrontmatter(markdown: string): FrontmatterEditor {
213
+ return new FrontmatterEditorImpl(markdown);
214
+ }
@@ -7,22 +7,32 @@
7
7
  * frontmatter-specific type codes plus a list of external URLs the registry
8
8
  * can fold into its existing external URL collection.
9
9
  *
10
- * Type mapping:
11
- * broken_file -> frontmatter_link_broken
12
- * broken_anchor -> frontmatter_anchor_missing
13
- * link_to_gitignored -> frontmatter_link_to_gitignored
14
- * unknown_link -> frontmatter_unknown_link
10
+ * Code mapping:
11
+ * LINK_BROKEN_FILE -> FRONTMATTER_LINK_BROKEN
12
+ * LINK_BROKEN_ANCHOR -> FRONTMATTER_ANCHOR_MISSING
13
+ * LINK_TO_GITIGNORED -> FRONTMATTER_LINK_TO_GITIGNORED
14
+ * LINK_UNKNOWN -> FRONTMATTER_UNKNOWN_LINK
15
15
  *
16
16
  * Skipped (no issue, no external):
17
17
  * email (mailto:)
18
18
  * anchor-only (validated as anchor in current file via validateLink)
19
19
  */
20
20
 
21
+ import { createRegistryIssue, type IssueCode } from '@vibe-agent-toolkit/agent-schema';
22
+
21
23
  import { classifyLink } from './link-parser.js';
22
24
  import { validateLink, type ValidateLinkOptions } from './link-validator.js';
23
25
  import { walkFrontmatterUriReferences } from './schema-uri-walker.js';
24
26
  import type { HeadingNode, ResourceLink, ValidationIssue } from './types.js';
25
27
 
28
+ /** Map the link-level code emitted by validateLink to its frontmatter-scoped code. */
29
+ const LINK_CODE_TO_FRONTMATTER_CODE: Partial<Record<IssueCode, IssueCode>> = {
30
+ LINK_BROKEN_FILE: 'FRONTMATTER_LINK_BROKEN',
31
+ LINK_BROKEN_ANCHOR: 'FRONTMATTER_ANCHOR_MISSING',
32
+ LINK_TO_GITIGNORED: 'FRONTMATTER_LINK_TO_GITIGNORED',
33
+ LINK_UNKNOWN: 'FRONTMATTER_UNKNOWN_LINK',
34
+ };
35
+
26
36
  /** A frontmatter-sourced external URL captured for downstream health checking. */
27
37
  export interface FrontmatterExternalUrl {
28
38
  url: string;
@@ -89,24 +99,12 @@ export async function validateFrontmatterLinks(
89
99
  }
90
100
 
91
101
  function rewriteIssue(issue: ValidationIssue, dottedPath: string): ValidationIssue {
92
- return {
93
- ...issue,
94
- type: mapType(issue.type),
95
- message: `field \`${dottedPath}\`: ${issue.message}`,
96
- };
97
- }
98
-
99
- function mapType(originalType: string): string {
100
- switch (originalType) {
101
- case 'broken_file':
102
- return 'frontmatter_link_broken';
103
- case 'broken_anchor':
104
- return 'frontmatter_anchor_missing';
105
- case 'link_to_gitignored':
106
- return 'frontmatter_link_to_gitignored';
107
- case 'unknown_link':
108
- return 'frontmatter_unknown_link';
109
- default:
110
- return originalType;
111
- }
102
+ const mappedCode = LINK_CODE_TO_FRONTMATTER_CODE[issue.code as IssueCode] ?? (issue.code as IssueCode);
103
+ const message = `field \`${dottedPath}\`: ${issue.message}`;
104
+ const extras: Partial<Pick<ValidationIssue, 'location' | 'line' | 'link' | 'suggestion'>> = {};
105
+ if (issue.location !== undefined) extras.location = issue.location;
106
+ if (issue.line !== undefined) extras.line = issue.line;
107
+ if (issue.link !== undefined) extras.link = issue.link;
108
+ if (issue.suggestion !== undefined) extras.suggestion = issue.suggestion;
109
+ return createRegistryIssue(mappedCode, message, extras);
112
110
  }