@jasonbelmonti/markdown-engine 2.0.0 → 3.1.0
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/CHANGELOG.md +45 -1
- package/README.md +132 -57
- package/dist/api/contracts.d.ts +2 -1
- package/dist/api/contracts.d.ts.map +1 -1
- package/dist/api/contracts.js +1 -0
- package/dist/api/contracts.js.map +1 -1
- package/dist/api/declarative-validation.d.ts +2 -2
- package/dist/api/declarative-validation.d.ts.map +1 -1
- package/dist/api/declarative-validation.js +53 -31
- package/dist/api/declarative-validation.js.map +1 -1
- package/dist/api/document-set-validation-types.d.ts +32 -0
- package/dist/api/document-set-validation-types.d.ts.map +1 -0
- package/dist/api/document-set-validation-types.js +2 -0
- package/dist/api/document-set-validation-types.js.map +1 -0
- package/dist/api/document-set-validation.d.ts +4 -0
- package/dist/api/document-set-validation.d.ts.map +1 -0
- package/dist/api/document-set-validation.js +60 -0
- package/dist/api/document-set-validation.js.map +1 -0
- package/dist/api/serialize.d.ts +2 -1
- package/dist/api/serialize.d.ts.map +1 -1
- package/dist/api/serialize.js.map +1 -1
- package/dist/cli/declarative-validation.d.ts.map +1 -1
- package/dist/cli/declarative-validation.js +7 -12
- package/dist/cli/declarative-validation.js.map +1 -1
- package/dist/declarative-validation/applicability/classifier.d.ts +18 -0
- package/dist/declarative-validation/applicability/classifier.d.ts.map +1 -0
- package/dist/declarative-validation/applicability/classifier.js +37 -0
- package/dist/declarative-validation/applicability/classifier.js.map +1 -0
- package/dist/declarative-validation/applicability/index.d.ts +2 -0
- package/dist/declarative-validation/applicability/index.d.ts.map +1 -0
- package/dist/declarative-validation/applicability/index.js +2 -0
- package/dist/declarative-validation/applicability/index.js.map +1 -0
- package/dist/declarative-validation/assertions/context.d.ts +2 -2
- package/dist/declarative-validation/assertions/context.d.ts.map +1 -1
- package/dist/declarative-validation/assertions/diagnostics.d.ts +7 -4
- package/dist/declarative-validation/assertions/diagnostics.d.ts.map +1 -1
- package/dist/declarative-validation/assertions/diagnostics.js +26 -1
- package/dist/declarative-validation/assertions/diagnostics.js.map +1 -1
- package/dist/declarative-validation/assertions/evaluator.d.ts +2 -2
- package/dist/declarative-validation/assertions/evaluator.d.ts.map +1 -1
- package/dist/declarative-validation/assertions/evaluator.js +9 -0
- package/dist/declarative-validation/assertions/evaluator.js.map +1 -1
- package/dist/declarative-validation/assertions/frontmatter-shape.d.ts +9 -0
- package/dist/declarative-validation/assertions/frontmatter-shape.d.ts.map +1 -0
- package/dist/declarative-validation/assertions/frontmatter-shape.js +105 -0
- package/dist/declarative-validation/assertions/frontmatter-shape.js.map +1 -0
- package/dist/declarative-validation/assertions/group-evaluator.d.ts +6 -0
- package/dist/declarative-validation/assertions/group-evaluator.d.ts.map +1 -0
- package/dist/declarative-validation/assertions/group-evaluator.js +57 -0
- package/dist/declarative-validation/assertions/group-evaluator.js.map +1 -0
- package/dist/declarative-validation/assertions/id-targets.d.ts +15 -0
- package/dist/declarative-validation/assertions/id-targets.d.ts.map +1 -1
- package/dist/declarative-validation/assertions/id-targets.js +18 -55
- package/dist/declarative-validation/assertions/id-targets.js.map +1 -1
- package/dist/declarative-validation/assertions/ids.d.ts.map +1 -1
- package/dist/declarative-validation/assertions/ids.js +100 -14
- package/dist/declarative-validation/assertions/ids.js.map +1 -1
- package/dist/declarative-validation/assertions/index.d.ts +1 -0
- package/dist/declarative-validation/assertions/index.d.ts.map +1 -1
- package/dist/declarative-validation/assertions/index.js +1 -0
- package/dist/declarative-validation/assertions/index.js.map +1 -1
- package/dist/declarative-validation/assertions/table-column-coverage.d.ts +9 -0
- package/dist/declarative-validation/assertions/table-column-coverage.d.ts.map +1 -0
- package/dist/declarative-validation/assertions/table-column-coverage.js +108 -0
- package/dist/declarative-validation/assertions/table-column-coverage.js.map +1 -0
- package/dist/declarative-validation/assertions/text-format.d.ts +9 -0
- package/dist/declarative-validation/assertions/text-format.d.ts.map +1 -0
- package/dist/declarative-validation/assertions/text-format.js +75 -0
- package/dist/declarative-validation/assertions/text-format.js.map +1 -0
- package/dist/declarative-validation/compiler/applicability-plan.d.ts +5 -0
- package/dist/declarative-validation/compiler/applicability-plan.d.ts.map +1 -0
- package/dist/declarative-validation/compiler/applicability-plan.js +22 -0
- package/dist/declarative-validation/compiler/applicability-plan.js.map +1 -0
- package/dist/declarative-validation/compiler/assertion-builders.d.ts +2 -1
- package/dist/declarative-validation/compiler/assertion-builders.d.ts.map +1 -1
- package/dist/declarative-validation/compiler/assertion-builders.js +88 -13
- package/dist/declarative-validation/compiler/assertion-builders.js.map +1 -1
- package/dist/declarative-validation/compiler/assertion-shapes.d.ts +5 -1
- package/dist/declarative-validation/compiler/assertion-shapes.d.ts.map +1 -1
- package/dist/declarative-validation/compiler/assertion-shapes.js +120 -0
- package/dist/declarative-validation/compiler/assertion-shapes.js.map +1 -1
- package/dist/declarative-validation/compiler/assertions.d.ts +2 -1
- package/dist/declarative-validation/compiler/assertions.d.ts.map +1 -1
- package/dist/declarative-validation/compiler/assertions.js +16 -4
- package/dist/declarative-validation/compiler/assertions.js.map +1 -1
- package/dist/declarative-validation/compiler/compatibility.d.ts.map +1 -1
- package/dist/declarative-validation/compiler/compatibility.js +3 -0
- package/dist/declarative-validation/compiler/compatibility.js.map +1 -1
- package/dist/declarative-validation/compiler/group-plans.d.ts +6 -0
- package/dist/declarative-validation/compiler/group-plans.d.ts.map +1 -0
- package/dist/declarative-validation/compiler/group-plans.js +67 -0
- package/dist/declarative-validation/compiler/group-plans.js.map +1 -0
- package/dist/declarative-validation/compiler/index.d.ts +1 -1
- package/dist/declarative-validation/compiler/index.d.ts.map +1 -1
- package/dist/declarative-validation/compiler/index.js +70 -35
- package/dist/declarative-validation/compiler/index.js.map +1 -1
- package/dist/declarative-validation/compiler/plan.d.ts +80 -5
- package/dist/declarative-validation/compiler/plan.d.ts.map +1 -1
- package/dist/declarative-validation/compiler/rule-fields.d.ts +6 -0
- package/dist/declarative-validation/compiler/rule-fields.d.ts.map +1 -0
- package/dist/declarative-validation/compiler/rule-fields.js +40 -0
- package/dist/declarative-validation/compiler/rule-fields.js.map +1 -0
- package/dist/declarative-validation/diagnostics/profile-config-diagnostics.d.ts +0 -1
- package/dist/declarative-validation/diagnostics/profile-config-diagnostics.d.ts.map +1 -1
- package/dist/declarative-validation/diagnostics/profile-config-diagnostics.js +8 -2
- package/dist/declarative-validation/diagnostics/profile-config-diagnostics.js.map +1 -1
- package/dist/declarative-validation/evidence/index.d.ts +3 -3
- package/dist/declarative-validation/evidence/index.d.ts.map +1 -1
- package/dist/declarative-validation/evidence/index.js +6 -6
- package/dist/declarative-validation/evidence/index.js.map +1 -1
- package/dist/declarative-validation/profile/applicability-schema.d.ts +4 -0
- package/dist/declarative-validation/profile/applicability-schema.d.ts.map +1 -0
- package/dist/declarative-validation/profile/applicability-schema.js +24 -0
- package/dist/declarative-validation/profile/applicability-schema.js.map +1 -0
- package/dist/declarative-validation/profile/assertion-schema.d.ts +2 -1
- package/dist/declarative-validation/profile/assertion-schema.d.ts.map +1 -1
- package/dist/declarative-validation/profile/assertion-schema.js +194 -14
- package/dist/declarative-validation/profile/assertion-schema.js.map +1 -1
- package/dist/declarative-validation/profile/direct-profile-diagnostics.d.ts +3 -1
- package/dist/declarative-validation/profile/direct-profile-diagnostics.d.ts.map +1 -1
- package/dist/declarative-validation/profile/direct-profile-diagnostics.js +10 -2
- package/dist/declarative-validation/profile/direct-profile-diagnostics.js.map +1 -1
- package/dist/declarative-validation/profile/frontmatter-shape-schema.d.ts +8 -0
- package/dist/declarative-validation/profile/frontmatter-shape-schema.d.ts.map +1 -0
- package/dist/declarative-validation/profile/frontmatter-shape-schema.js +178 -0
- package/dist/declarative-validation/profile/frontmatter-shape-schema.js.map +1 -0
- package/dist/declarative-validation/profile/group-schema.d.ts +5 -0
- package/dist/declarative-validation/profile/group-schema.d.ts.map +1 -0
- package/dist/declarative-validation/profile/group-schema.js +55 -0
- package/dist/declarative-validation/profile/group-schema.js.map +1 -0
- package/dist/declarative-validation/profile/ids-assertion-contract.d.ts +12 -0
- package/dist/declarative-validation/profile/ids-assertion-contract.d.ts.map +1 -0
- package/dist/declarative-validation/profile/ids-assertion-contract.js +32 -0
- package/dist/declarative-validation/profile/ids-assertion-contract.js.map +1 -0
- package/dist/declarative-validation/profile/index.d.ts +63 -2
- package/dist/declarative-validation/profile/index.d.ts.map +1 -1
- package/dist/declarative-validation/profile/index.js.map +1 -1
- package/dist/declarative-validation/profile/materialization.d.ts.map +1 -1
- package/dist/declarative-validation/profile/materialization.js +13 -10
- package/dist/declarative-validation/profile/materialization.js.map +1 -1
- package/dist/declarative-validation/profile/schema.d.ts.map +1 -1
- package/dist/declarative-validation/profile/schema.js +73 -12
- package/dist/declarative-validation/profile/schema.js.map +1 -1
- package/dist/declarative-validation/profile/syntax-version.d.ts +7 -0
- package/dist/declarative-validation/profile/syntax-version.d.ts.map +1 -0
- package/dist/declarative-validation/profile/syntax-version.js +11 -0
- package/dist/declarative-validation/profile/syntax-version.js.map +1 -0
- package/dist/declarative-validation/profile/text-format-contract.d.ts +4 -0
- package/dist/declarative-validation/profile/text-format-contract.d.ts.map +1 -0
- package/dist/declarative-validation/profile/text-format-contract.js +6 -0
- package/dist/declarative-validation/profile/text-format-contract.js.map +1 -0
- package/dist/declarative-validation/results/clone-rule-result.d.ts +6 -0
- package/dist/declarative-validation/results/clone-rule-result.d.ts.map +1 -0
- package/dist/declarative-validation/results/clone-rule-result.js +125 -0
- package/dist/declarative-validation/results/clone-rule-result.js.map +1 -0
- package/dist/declarative-validation/results/create-result.d.ts +15 -0
- package/dist/declarative-validation/results/create-result.d.ts.map +1 -0
- package/dist/declarative-validation/results/create-result.js +58 -0
- package/dist/declarative-validation/results/create-result.js.map +1 -0
- package/dist/declarative-validation/results/index.d.ts +3 -25
- package/dist/declarative-validation/results/index.d.ts.map +1 -1
- package/dist/declarative-validation/results/index.js +2 -1
- package/dist/declarative-validation/results/index.js.map +1 -1
- package/dist/declarative-validation/results/skipped-rule-result.d.ts +10 -0
- package/dist/declarative-validation/results/skipped-rule-result.d.ts.map +1 -0
- package/dist/declarative-validation/results/skipped-rule-result.js +18 -0
- package/dist/declarative-validation/results/skipped-rule-result.js.map +1 -0
- package/dist/declarative-validation/results/types.d.ts +77 -0
- package/dist/declarative-validation/results/types.d.ts.map +1 -0
- package/dist/declarative-validation/results/types.js +2 -0
- package/dist/declarative-validation/results/types.js.map +1 -0
- package/dist/declarative-validation/selectors/table-targets.d.ts +23 -0
- package/dist/declarative-validation/selectors/table-targets.d.ts.map +1 -1
- package/dist/declarative-validation/selectors/table-targets.js +52 -9
- package/dist/declarative-validation/selectors/table-targets.js.map +1 -1
- package/dist-bundled/markdown-engine-cli.mjs +27373 -0
- package/docs/contracts/api.md +79 -15
- package/docs/contracts/declarative-validation.md +462 -40
- package/fixtures/declarative-validation/examples/okf-v0.1/fail/invalid-log-date/log.md +5 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/fail/missing-concept-type/concepts/customer-metric.md +8 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/fail/non-root-index-frontmatter/datasets/index.md +7 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/pass/datasets/index.md +3 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/pass/datasets/sales.md +16 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/pass/index.md +8 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/pass/log.md +9 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/pass/playbooks/incident-response.md +17 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/profiles/concept.yaml +14 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/profiles/log.yaml +10 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/profiles/non-root-index.yaml +9 -0
- package/fixtures/declarative-validation/examples/okf-v0.1/profiles/root-index.yaml +12 -0
- package/package.json +9 -3
- package/scripts/install-markdown-engine-cli.sh +113 -0
- package/skills/profile-backed-markdown/SKILL.md +54 -0
- package/skills/profile-backed-markdown/assets/profiles/operational-spec.yaml +98 -0
- package/skills/profile-backed-markdown/references/repair-brief.md +14 -0
- package/skills/profile-backed-markdown/scripts/validate-profile-backed-markdown.mjs +590 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
5
|
+
import {
|
|
6
|
+
dirname,
|
|
7
|
+
isAbsolute,
|
|
8
|
+
join,
|
|
9
|
+
relative,
|
|
10
|
+
resolve,
|
|
11
|
+
sep,
|
|
12
|
+
} from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
16
|
+
const scriptDir = dirname(scriptPath);
|
|
17
|
+
const skillRoot = resolve(scriptDir, "..");
|
|
18
|
+
const defaultProfileRoot = join(skillRoot, "assets", "profiles");
|
|
19
|
+
const profileFileExtensions = [".yaml", ".yml", ".json"];
|
|
20
|
+
const defaultBundledCliPath = resolve(
|
|
21
|
+
skillRoot,
|
|
22
|
+
"..",
|
|
23
|
+
"..",
|
|
24
|
+
"dist-bundled",
|
|
25
|
+
"markdown-engine-cli.mjs",
|
|
26
|
+
);
|
|
27
|
+
const usage = `Usage: validate-profile-backed-markdown --file <markdown-file> [--profile-root <profiles-dir>] [--repair-brief]
|
|
28
|
+
|
|
29
|
+
Routes frontmatter validationProfile to a local profile asset and runs the bundled markdown-engine validator.
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--file <markdown-file> Markdown file to validate.
|
|
33
|
+
--profile-root <profiles-dir> Local directory for validation profiles. Defaults to skill assets/profiles.
|
|
34
|
+
--repair-brief Emit compact repair guidance on stderr while preserving JSON on stdout.
|
|
35
|
+
-h, --help Show this help message.
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
main(process.argv.slice(2)).then((exitCode) => {
|
|
39
|
+
process.exitCode = exitCode;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function main(args) {
|
|
43
|
+
const parsedArgs = parseArgs(args);
|
|
44
|
+
|
|
45
|
+
if (parsedArgs.kind === "help") {
|
|
46
|
+
process.stdout.write(usage);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (parsedArgs.kind === "error") {
|
|
51
|
+
process.stderr.write(`${parsedArgs.message}\n\n${usage}`);
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const profileRefResult = await readValidationProfileRef(parsedArgs.filePath);
|
|
56
|
+
|
|
57
|
+
if (profileRefResult.kind === "error") {
|
|
58
|
+
process.stderr.write(`${profileRefResult.message}\n`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const profileResult = await resolveProfilePath(
|
|
63
|
+
profileRefResult.validationProfile,
|
|
64
|
+
parsedArgs.profileRoot,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (profileResult.kind === "error") {
|
|
68
|
+
process.stderr.write(`${profileResult.message}\n`);
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const cliPathResult = await resolveBundledCliPath();
|
|
73
|
+
|
|
74
|
+
if (cliPathResult.kind === "error") {
|
|
75
|
+
process.stderr.write(`${cliPathResult.message}\n`);
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const validation = await runBundledValidator({
|
|
80
|
+
cliPath: cliPathResult.cliPath,
|
|
81
|
+
filePath: parsedArgs.filePath,
|
|
82
|
+
profilePath: profileResult.profilePath,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
process.stdout.write(validation.stdout);
|
|
86
|
+
process.stderr.write(validation.stderr);
|
|
87
|
+
|
|
88
|
+
if (parsedArgs.repairBrief) {
|
|
89
|
+
process.stderr.write(renderRepairBrief(validation.stdout));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return validation.exitCode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseArgs(args) {
|
|
96
|
+
const filePaths = [];
|
|
97
|
+
const profileRoots = [];
|
|
98
|
+
let profileRoot = defaultProfileRoot;
|
|
99
|
+
let repairBrief = false;
|
|
100
|
+
|
|
101
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
102
|
+
const arg = args[index];
|
|
103
|
+
|
|
104
|
+
if (arg === "-h" || arg === "--help") {
|
|
105
|
+
return { kind: "help" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (arg === "--repair-brief") {
|
|
109
|
+
repairBrief = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (arg === "--file") {
|
|
114
|
+
const value = readRequiredValue(arg, args, index);
|
|
115
|
+
|
|
116
|
+
if (value.kind === "error") {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
filePaths.push(value.value);
|
|
121
|
+
index += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (arg?.startsWith("--file=") === true) {
|
|
126
|
+
filePaths.push(arg.slice("--file=".length));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (arg === "--profile-root") {
|
|
131
|
+
const value = readRequiredValue(arg, args, index);
|
|
132
|
+
|
|
133
|
+
if (value.kind === "error") {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
profileRoots.push(value.value);
|
|
138
|
+
index += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (arg?.startsWith("--profile-root=") === true) {
|
|
143
|
+
profileRoots.push(arg.slice("--profile-root=".length));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { kind: "error", message: `Unknown argument: ${arg ?? ""}` };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (filePaths.length === 0) {
|
|
151
|
+
return { kind: "error", message: "Expected exactly one --file target." };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (filePaths.length > 1) {
|
|
155
|
+
return {
|
|
156
|
+
kind: "error",
|
|
157
|
+
message: "Expected one Markdown file target, received multiple.",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (profileRoots.length > 1) {
|
|
162
|
+
return {
|
|
163
|
+
kind: "error",
|
|
164
|
+
message: "Expected at most one --profile-root selector.",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const [filePath] = filePaths;
|
|
169
|
+
const [selectedProfileRoot] = profileRoots;
|
|
170
|
+
|
|
171
|
+
if (filePath === undefined || filePath.trim() === "") {
|
|
172
|
+
return { kind: "error", message: "File path cannot be empty." };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (selectedProfileRoot !== undefined) {
|
|
176
|
+
profileRoot = selectedProfileRoot;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
kind: "ok",
|
|
181
|
+
filePath,
|
|
182
|
+
profileRoot: resolve(process.cwd(), profileRoot),
|
|
183
|
+
repairBrief,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readRequiredValue(flag, args, index) {
|
|
188
|
+
const value = args[index + 1];
|
|
189
|
+
|
|
190
|
+
if (value === undefined || value.startsWith("-")) {
|
|
191
|
+
return { kind: "error", message: `Missing value for ${flag}.` };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { kind: "ok", value };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function readValidationProfileRef(filePath) {
|
|
198
|
+
let markdown;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
markdown = await readFile(resolve(process.cwd(), filePath), "utf8");
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
kind: "error",
|
|
205
|
+
message: `Unable to read "${filePath}": ${errorMessage(error)}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const frontmatter = extractFrontmatter(markdown);
|
|
210
|
+
|
|
211
|
+
if (frontmatter.kind === "error") {
|
|
212
|
+
return frontmatter;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return parseValidationProfileField(frontmatter.raw);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function extractFrontmatter(markdown) {
|
|
219
|
+
const content = markdown.startsWith("\uFEFF") ? markdown.slice(1) : markdown;
|
|
220
|
+
const lines = content.split(/\r\n|\n|\r/);
|
|
221
|
+
|
|
222
|
+
if (lines[0] !== "---") {
|
|
223
|
+
return {
|
|
224
|
+
kind: "error",
|
|
225
|
+
message: "Expected YAML frontmatter with validationProfile at the start of the Markdown file.",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const closingIndex = lines.findIndex(
|
|
230
|
+
(line, index) => index > 0 && line === "---",
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (closingIndex === -1) {
|
|
234
|
+
return {
|
|
235
|
+
kind: "error",
|
|
236
|
+
message: "Expected closing YAML frontmatter delimiter before validation.",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
kind: "ok",
|
|
242
|
+
raw: lines.slice(1, closingIndex).join("\n"),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseValidationProfileField(frontmatter) {
|
|
247
|
+
let validationProfile;
|
|
248
|
+
|
|
249
|
+
for (const [index, line] of frontmatter.split("\n").entries()) {
|
|
250
|
+
const field = /^validationProfile\s*:(.*)$/.exec(line);
|
|
251
|
+
|
|
252
|
+
if (field === null) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (validationProfile !== undefined) {
|
|
257
|
+
return {
|
|
258
|
+
kind: "error",
|
|
259
|
+
message: "Expected exactly one frontmatter validationProfile field.",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const value = parseProfileScalar(field[1]?.trim() ?? "");
|
|
264
|
+
|
|
265
|
+
if (value.kind === "error") {
|
|
266
|
+
return {
|
|
267
|
+
kind: "error",
|
|
268
|
+
message: `Invalid validationProfile on frontmatter line ${index + 2}: ${value.message}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
validationProfile = value.value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (validationProfile === undefined) {
|
|
276
|
+
return {
|
|
277
|
+
kind: "error",
|
|
278
|
+
message: "Expected frontmatter validationProfile to select a local profile asset.",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
kind: "ok",
|
|
284
|
+
validationProfile,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseProfileScalar(rawValue) {
|
|
289
|
+
if (rawValue === "") {
|
|
290
|
+
return { kind: "error", message: "expected a non-empty string scalar." };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const value = unquoteScalar(stripLineComment(rawValue));
|
|
294
|
+
|
|
295
|
+
if (value.kind === "error") {
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(value.value)) {
|
|
300
|
+
return {
|
|
301
|
+
kind: "error",
|
|
302
|
+
message: "expected a local profile id using letters, numbers, dots, dashes, underscores, or slashes.",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function stripLineComment(value) {
|
|
310
|
+
let quote;
|
|
311
|
+
let escaped = false;
|
|
312
|
+
|
|
313
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
314
|
+
const character = value[index];
|
|
315
|
+
const previous = value[index - 1];
|
|
316
|
+
|
|
317
|
+
if (quote === "\"") {
|
|
318
|
+
if (escaped) {
|
|
319
|
+
escaped = false;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (character === "\\") {
|
|
324
|
+
escaped = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (character === quote) {
|
|
329
|
+
quote = undefined;
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (quote === "'") {
|
|
335
|
+
if (character === "'" && value[index + 1] === "'") {
|
|
336
|
+
index += 1;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (character === quote) {
|
|
341
|
+
quote = undefined;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (character === "\"" || character === "'") {
|
|
347
|
+
quote = character;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (
|
|
352
|
+
character === "#" &&
|
|
353
|
+
(index === 0 || previous === " " || previous === "\t")
|
|
354
|
+
) {
|
|
355
|
+
return value.slice(0, index).trimEnd();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return value.trim();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function unquoteScalar(value) {
|
|
363
|
+
if (value.startsWith("\"")) {
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(value);
|
|
366
|
+
|
|
367
|
+
return typeof parsed === "string"
|
|
368
|
+
? { kind: "ok", value: parsed }
|
|
369
|
+
: { kind: "error", message: "expected a string scalar." };
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return { kind: "error", message: errorMessage(error) };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (value.startsWith("'")) {
|
|
376
|
+
if (!value.endsWith("'") || value.length < 2) {
|
|
377
|
+
return { kind: "error", message: "unterminated single-quoted string." };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { kind: "ok", value: value.slice(1, -1).replaceAll("''", "'") };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { kind: "ok", value };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function resolveProfilePath(validationProfile, profileRoot) {
|
|
387
|
+
if (isAbsolute(validationProfile)) {
|
|
388
|
+
return {
|
|
389
|
+
kind: "error",
|
|
390
|
+
message: "validationProfile must resolve to a local profile asset, not an absolute path.",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const normalizedRef = validationProfile.replaceAll("\\", "/");
|
|
395
|
+
const profileSegments = normalizedRef.split("/");
|
|
396
|
+
|
|
397
|
+
if (
|
|
398
|
+
profileSegments.some(
|
|
399
|
+
(segment) => segment === "" || segment === "." || segment === "..",
|
|
400
|
+
)
|
|
401
|
+
) {
|
|
402
|
+
return {
|
|
403
|
+
kind: "error",
|
|
404
|
+
message: "validationProfile must not contain empty, current-directory, or parent-directory segments.",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const candidates = hasProfileFileExtension(normalizedRef)
|
|
409
|
+
? [normalizedRef]
|
|
410
|
+
: [
|
|
411
|
+
...profileFileExtensions.map((extension) => `${normalizedRef}${extension}`),
|
|
412
|
+
normalizedRef,
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
for (const candidate of candidates) {
|
|
416
|
+
const candidatePath = resolve(profileRoot, candidate);
|
|
417
|
+
|
|
418
|
+
if (!isWithinDirectory(profileRoot, candidatePath)) {
|
|
419
|
+
return {
|
|
420
|
+
kind: "error",
|
|
421
|
+
message: "validationProfile resolved outside the local profile root.",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (await isReadableFile(candidatePath)) {
|
|
426
|
+
return { kind: "ok", profilePath: candidatePath };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
kind: "error",
|
|
432
|
+
message: `Unable to resolve validationProfile "${validationProfile}" under ${profileRoot}.`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function hasProfileFileExtension(profileRef) {
|
|
437
|
+
return profileFileExtensions.some((extension) =>
|
|
438
|
+
profileRef.toLowerCase().endsWith(extension),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function isWithinDirectory(root, target) {
|
|
443
|
+
const relativePath = relative(root, target);
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
relativePath === "" ||
|
|
447
|
+
(!relativePath.startsWith("..") && !relativePath.includes(`..${sep}`))
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function resolveBundledCliPath() {
|
|
452
|
+
const candidates = [
|
|
453
|
+
process.env.MARKDOWN_ENGINE_CLI,
|
|
454
|
+
join(scriptDir, "markdown-engine-cli.mjs"),
|
|
455
|
+
defaultBundledCliPath,
|
|
456
|
+
].filter((candidate) => candidate !== undefined && candidate !== "");
|
|
457
|
+
|
|
458
|
+
for (const candidate of candidates) {
|
|
459
|
+
const cliPath = resolve(process.cwd(), candidate);
|
|
460
|
+
|
|
461
|
+
if (await isReadableFile(cliPath)) {
|
|
462
|
+
return { kind: "ok", cliPath };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
kind: "error",
|
|
468
|
+
message: [
|
|
469
|
+
"Unable to locate bundled markdown-engine CLI.",
|
|
470
|
+
"Set MARKDOWN_ENGINE_CLI, copy markdown-engine-cli.mjs into this skill's scripts directory,",
|
|
471
|
+
"or run `npm run build:cli:bundled` from the package root.",
|
|
472
|
+
].join(" "),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function isReadableFile(path) {
|
|
477
|
+
try {
|
|
478
|
+
const candidate = await stat(path);
|
|
479
|
+
if (!candidate.isFile()) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
await access(path, constants.R_OK);
|
|
484
|
+
return true;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function runBundledValidator({ cliPath, filePath, profilePath }) {
|
|
491
|
+
return new Promise((resolvePromise) => {
|
|
492
|
+
const child = spawn(
|
|
493
|
+
process.execPath,
|
|
494
|
+
[
|
|
495
|
+
cliPath,
|
|
496
|
+
"validate",
|
|
497
|
+
"--file",
|
|
498
|
+
filePath,
|
|
499
|
+
"--profile",
|
|
500
|
+
profilePath,
|
|
501
|
+
"--format",
|
|
502
|
+
"json",
|
|
503
|
+
],
|
|
504
|
+
{
|
|
505
|
+
cwd: process.cwd(),
|
|
506
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
507
|
+
},
|
|
508
|
+
);
|
|
509
|
+
const stdout = [];
|
|
510
|
+
const stderr = [];
|
|
511
|
+
|
|
512
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
513
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
514
|
+
|
|
515
|
+
child.on("error", (error) => {
|
|
516
|
+
resolvePromise({
|
|
517
|
+
exitCode: 2,
|
|
518
|
+
stderr: `${errorMessage(error)}\n`,
|
|
519
|
+
stdout: "",
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
child.on("close", (code) => {
|
|
524
|
+
resolvePromise({
|
|
525
|
+
exitCode: typeof code === "number" ? code : 2,
|
|
526
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
527
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function renderRepairBrief(jsonText) {
|
|
534
|
+
let result;
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
result = JSON.parse(jsonText);
|
|
538
|
+
} catch {
|
|
539
|
+
return "\nRepair brief unavailable: validator output was not JSON.\n";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (result?.valid === true) {
|
|
543
|
+
return "\nRepair brief:\n- Validation passed. No repair actions.\n";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const diagnostics = Array.isArray(result?.diagnostics) ? result.diagnostics : [];
|
|
547
|
+
|
|
548
|
+
if (diagnostics.length === 0) {
|
|
549
|
+
return "\nRepair brief:\n- Validation failed without diagnostics in validator JSON.\n";
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return [
|
|
553
|
+
"",
|
|
554
|
+
"Repair brief:",
|
|
555
|
+
...diagnostics.map((diagnostic) => formatDiagnostic(diagnostic)),
|
|
556
|
+
"",
|
|
557
|
+
].join("\n");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function formatDiagnostic(diagnostic) {
|
|
561
|
+
const ruleId = typeof diagnostic.ruleId === "string" ? diagnostic.ruleId : "(none)";
|
|
562
|
+
const message = typeof diagnostic.message === "string" ? diagnostic.message : "(no message)";
|
|
563
|
+
const sourceRange = formatSourceRange(diagnostic.sourceRange);
|
|
564
|
+
|
|
565
|
+
return [
|
|
566
|
+
`- ruleId: ${ruleId}`,
|
|
567
|
+
` message: ${message}`,
|
|
568
|
+
` sourceRange: ${sourceRange}`,
|
|
569
|
+
].join("\n");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function formatSourceRange(sourceRange) {
|
|
573
|
+
const start = sourceRange?.start;
|
|
574
|
+
const end = sourceRange?.end;
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
typeof start?.line !== "number" ||
|
|
578
|
+
typeof start?.column !== "number" ||
|
|
579
|
+
typeof end?.line !== "number" ||
|
|
580
|
+
typeof end?.column !== "number"
|
|
581
|
+
) {
|
|
582
|
+
return "unavailable";
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return `${start.line}:${start.column}-${end.line}:${end.column}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function errorMessage(error) {
|
|
589
|
+
return error instanceof Error ? error.message : String(error);
|
|
590
|
+
}
|