@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.
- package/README.md +35 -26
- package/dist/ajv-factory.d.ts +33 -0
- package/dist/ajv-factory.d.ts.map +1 -0
- package/dist/ajv-factory.js +51 -0
- package/dist/ajv-factory.js.map +1 -0
- package/dist/config-parser.d.ts +0 -18
- package/dist/config-parser.d.ts.map +1 -1
- package/dist/config-parser.js +5 -46
- package/dist/config-parser.js.map +1 -1
- package/dist/frontmatter-editor.d.ts +45 -0
- package/dist/frontmatter-editor.d.ts.map +1 -0
- package/dist/frontmatter-editor.js +161 -0
- package/dist/frontmatter-editor.js.map +1 -0
- package/dist/frontmatter-link-validator.d.ts +5 -5
- package/dist/frontmatter-link-validator.d.ts.map +1 -1
- package/dist/frontmatter-link-validator.js +25 -24
- package/dist/frontmatter-link-validator.js.map +1 -1
- package/dist/frontmatter-validator.d.ts +3 -2
- package/dist/frontmatter-validator.d.ts.map +1 -1
- package/dist/frontmatter-validator.js +19 -20
- package/dist/frontmatter-validator.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/json-pointer-path.d.ts +13 -0
- package/dist/json-pointer-path.d.ts.map +1 -0
- package/dist/json-pointer-path.js +30 -0
- package/dist/json-pointer-path.js.map +1 -0
- package/dist/link-parser.js +14 -16
- package/dist/link-parser.js.map +1 -1
- package/dist/link-validator.d.ts +23 -1
- package/dist/link-validator.d.ts.map +1 -1
- package/dist/link-validator.js +107 -104
- package/dist/link-validator.js.map +1 -1
- package/dist/multi-schema-validator.d.ts.map +1 -1
- package/dist/multi-schema-validator.js +6 -8
- package/dist/multi-schema-validator.js.map +1 -1
- package/dist/resource-registry.d.ts +10 -2
- package/dist/resource-registry.d.ts.map +1 -1
- package/dist/resource-registry.js +25 -32
- package/dist/resource-registry.js.map +1 -1
- package/dist/rewriter-helpers.d.ts +49 -0
- package/dist/rewriter-helpers.d.ts.map +1 -0
- package/dist/rewriter-helpers.js +142 -0
- package/dist/rewriter-helpers.js.map +1 -0
- package/dist/schemas/project-config.d.ts +219 -171
- package/dist/schemas/project-config.d.ts.map +1 -1
- package/dist/schemas/project-config.js +2 -0
- package/dist/schemas/project-config.js.map +1 -1
- package/dist/schemas/validation-result.d.ts +36 -57
- package/dist/schemas/validation-result.d.ts.map +1 -1
- package/dist/schemas/validation-result.js +5 -27
- package/dist/schemas/validation-result.js.map +1 -1
- package/dist/types/resource-parser.d.ts.map +1 -1
- package/dist/types/resource-parser.js +2 -3
- package/dist/types/resource-parser.js.map +1 -1
- package/dist/types/resources.d.ts +1 -1
- package/dist/types/resources.d.ts.map +1 -1
- package/dist/types/resources.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +50 -11
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +53 -13
- package/dist/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/ajv-factory.ts +56 -0
- package/src/config-parser.ts +5 -51
- package/src/frontmatter-editor.ts +214 -0
- package/src/frontmatter-link-validator.ts +23 -25
- package/src/frontmatter-validator.ts +29 -22
- package/src/index.ts +21 -2
- package/src/json-pointer-path.ts +29 -0
- package/src/link-parser.ts +27 -20
- package/src/link-validator.ts +194 -119
- package/src/multi-schema-validator.ts +10 -8
- package/src/resource-registry.ts +48 -33
- package/src/rewriter-helpers.ts +166 -0
- package/src/schemas/project-config.ts +2 -0
- package/src/schemas/validation-result.ts +5 -29
- package/src/types/resource-parser.ts +2 -3
- package/src/types/resources.ts +2 -1
- package/src/types.ts +0 -1
- 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
|
|
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
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
* //
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,OAAe;IAClE,MAAM,eAAe,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAE1C,8DAA8D;IAC9D,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,yEAAyE;IACzE,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACvD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,IAAI,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;IAC3C,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5B,CAAC;
|
|
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.
|
|
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
|
-
"@vibe-agent-toolkit/utils": "0.1.
|
|
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
|
+
}
|
package/src/config-parser.ts
CHANGED
|
@@ -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 {
|
|
11
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
}
|