@chartcoach/catalog 0.1.7 → 0.2.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/README.md +324 -38
- package/dist/duckdb-wasm.d.ts +8 -0
- package/dist/duckdb-wasm.js +22 -0
- package/dist/duckdb.d.ts +8 -0
- package/dist/duckdb.js +9 -0
- package/dist/index-cache-Cx1gIK6N.js +280 -0
- package/dist/index.d.ts +40 -8
- package/dist/index.js +22 -7
- package/dist/json-CBWjz9b4.js +29 -0
- package/dist/model-De0MF-zg.d.ts +255 -0
- package/dist/node.d.ts +15 -0
- package/dist/node.js +218 -0
- package/dist/open-Nnzkr_bE.d.ts +15 -0
- package/dist/open-Uu3-r8kp.js +1244 -0
- package/dist/registration-CgQGMUIH.d.ts +6 -0
- package/dist/registration-DXhS7vgr.js +41 -0
- package/dist/tables-DcgKnXwF.js +389 -0
- package/package.json +47 -18
- package/src/catalog/artifacts.ts +309 -100
- package/src/catalog/description.ts +198 -0
- package/src/catalog/errors.ts +26 -1
- package/src/catalog/identity.ts +69 -0
- package/src/catalog/json.ts +32 -0
- package/src/catalog/labels.ts +8 -6
- package/src/catalog/manifest.ts +81 -20
- package/src/catalog/markdown.ts +2 -2
- package/src/catalog/model.ts +222 -37
- package/src/catalog/open.ts +428 -0
- package/src/catalog/parquet.ts +164 -0
- package/src/catalog/profile-layout.ts +86 -0
- package/src/catalog/profile.ts +345 -0
- package/src/catalog/query.ts +125 -0
- package/src/catalog/read.ts +101 -0
- package/src/catalog/references.ts +336 -0
- package/src/catalog/registration.ts +74 -0
- package/src/catalog/tables.ts +250 -0
- package/src/catalog/wire.ts +38 -65
- package/src/duckdb-wasm.ts +33 -0
- package/src/duckdb.ts +18 -0
- package/src/index.ts +51 -20
- package/src/node/index-cache.ts +432 -0
- package/src/node.ts +347 -0
- package/dist/catalog/artifacts.d.ts +0 -28
- package/dist/catalog/artifacts.d.ts.map +0 -1
- package/dist/catalog/artifacts.js +0 -89
- package/dist/catalog/chartcoach-defaults.d.ts +0 -17
- package/dist/catalog/chartcoach-defaults.d.ts.map +0 -1
- package/dist/catalog/chartcoach-defaults.js +0 -8
- package/dist/catalog/errors.d.ts +0 -4
- package/dist/catalog/errors.d.ts.map +0 -1
- package/dist/catalog/errors.js +0 -6
- package/dist/catalog/labels.d.ts +0 -9
- package/dist/catalog/labels.d.ts.map +0 -1
- package/dist/catalog/labels.js +0 -23
- package/dist/catalog/load-parquet-core.d.ts +0 -15
- package/dist/catalog/load-parquet-core.d.ts.map +0 -1
- package/dist/catalog/load-parquet-core.js +0 -45
- package/dist/catalog/manifest.d.ts +0 -15
- package/dist/catalog/manifest.d.ts.map +0 -1
- package/dist/catalog/manifest.js +0 -143
- package/dist/catalog/markdown.d.ts +0 -3
- package/dist/catalog/markdown.d.ts.map +0 -1
- package/dist/catalog/markdown.js +0 -13
- package/dist/catalog/model.d.ts +0 -33
- package/dist/catalog/model.d.ts.map +0 -1
- package/dist/catalog/model.js +0 -67
- package/dist/catalog/wire.d.ts +0 -18
- package/dist/catalog/wire.d.ts.map +0 -1
- package/dist/catalog/wire.js +0 -78
- package/dist/index.d.ts.map +0 -1
- package/src/catalog/chartcoach-defaults.ts +0 -17
- package/src/catalog/load-parquet-core.ts +0 -78
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CatalogError } from "./errors";
|
|
2
|
+
import { isJsonObject, type JsonObject, type JsonValue } from "./json";
|
|
3
|
+
import type { Guideline } from "./model";
|
|
4
|
+
|
|
5
|
+
export async function catalogEntriesDigest(guidelines: readonly Guideline[]): Promise<string> {
|
|
6
|
+
const rows = [...guidelines]
|
|
7
|
+
.sort((left, right) => compareUnicode(left.id, right.id))
|
|
8
|
+
.map((guideline): JsonObject => ({
|
|
9
|
+
id: guideline.id,
|
|
10
|
+
title: guideline.title,
|
|
11
|
+
description: guideline.description,
|
|
12
|
+
labels: guideline.labels,
|
|
13
|
+
sections: guideline.sections.map((section) => ({
|
|
14
|
+
role: section.role,
|
|
15
|
+
title: section.title,
|
|
16
|
+
content: section.content,
|
|
17
|
+
})),
|
|
18
|
+
references: guideline.references,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
return sha256Text(rows.map(canonicalJson).join(""));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function manifestDigest(markdown: string): Promise<string> {
|
|
25
|
+
return sha256Text(markdown);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function canonicalJson(value: JsonValue): string {
|
|
29
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
30
|
+
|
|
31
|
+
if (!isJsonObject(value)) return JSON.stringify(value) ?? "null";
|
|
32
|
+
|
|
33
|
+
return `{${Object.keys(value)
|
|
34
|
+
.sort(compareUnicode)
|
|
35
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key]!)}`)
|
|
36
|
+
.join(",")}}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function sha256Bytes(value: Uint8Array): Promise<string> {
|
|
40
|
+
const subtle = globalThis.crypto?.subtle;
|
|
41
|
+
|
|
42
|
+
if (!subtle) {
|
|
43
|
+
throw new CatalogError("SHA-256 digest support is unavailable in this runtime.", {
|
|
44
|
+
code: "unavailable_capability",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const digest = await subtle.digest("SHA-256", value.slice().buffer);
|
|
49
|
+
|
|
50
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function compareUnicode(left: string, right: string): number {
|
|
54
|
+
const leftPoints = Array.from(left, (character) => character.codePointAt(0)!);
|
|
55
|
+
const rightPoints = Array.from(right, (character) => character.codePointAt(0)!);
|
|
56
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < length; index += 1) {
|
|
59
|
+
const difference = leftPoints[index]! - rightPoints[index]!;
|
|
60
|
+
|
|
61
|
+
if (difference !== 0) return difference;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return leftPoints.length - rightPoints.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function sha256Text(value: string): Promise<string> {
|
|
68
|
+
return sha256Bytes(new TextEncoder().encode(value));
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type JsonScalar = boolean | null | number | string;
|
|
2
|
+
|
|
3
|
+
export interface JsonObject {
|
|
4
|
+
[key: string]: JsonValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type JsonValue = JsonScalar | JsonObject | readonly JsonValue[];
|
|
8
|
+
|
|
9
|
+
export function parseJson(text: string): JsonValue {
|
|
10
|
+
return JSON.parse(text);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
|
|
14
|
+
return (
|
|
15
|
+
value !== null &&
|
|
16
|
+
value !== undefined &&
|
|
17
|
+
!Array.isArray(value) &&
|
|
18
|
+
Object.prototype.toString.call(value) === "[object Object]"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isJsonNumber(value: JsonValue | undefined): value is number {
|
|
23
|
+
return (
|
|
24
|
+
Object.prototype.toString.call(value) === "[object Number]" &&
|
|
25
|
+
value === value?.valueOf() &&
|
|
26
|
+
Number.isFinite(value)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isJsonString(value: JsonValue | undefined): value is string {
|
|
31
|
+
return Object.prototype.toString.call(value) === "[object String]" && value === value?.valueOf();
|
|
32
|
+
}
|
package/src/catalog/labels.ts
CHANGED
|
@@ -7,20 +7,22 @@ export type CatalogLabel = {
|
|
|
7
7
|
modifier?: string;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export function parseLabel(value:
|
|
11
|
-
if (typeof value !== "string") {
|
|
12
|
-
throw new CatalogError(`${context} must be a string.`);
|
|
13
|
-
}
|
|
10
|
+
export function parseLabel(value: string, context = "label"): CatalogLabel {
|
|
14
11
|
const parts = value
|
|
15
12
|
.trim()
|
|
16
13
|
.split(":")
|
|
17
14
|
.map((part) => part.trim());
|
|
15
|
+
|
|
18
16
|
if ((parts.length !== 2 && parts.length !== 3) || parts.some((part) => part.length === 0)) {
|
|
19
17
|
throw new CatalogError(
|
|
20
18
|
`${context} must use <family>:<category> or <family>:<category>:<modifier>.`,
|
|
21
19
|
);
|
|
22
20
|
}
|
|
23
|
-
|
|
21
|
+
|
|
22
|
+
const family = parts[0]!;
|
|
23
|
+
const category = parts[1]!;
|
|
24
|
+
const modifier = parts[2];
|
|
25
|
+
|
|
24
26
|
return {
|
|
25
27
|
value: modifier === undefined ? `${family}:${category}` : `${family}:${category}:${modifier}`,
|
|
26
28
|
family,
|
|
@@ -29,6 +31,6 @@ export function parseLabel(value: unknown, context = "label"): CatalogLabel {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export function normalizeLabel(value:
|
|
34
|
+
export function normalizeLabel(value: string, context = "label"): string {
|
|
33
35
|
return parseLabel(value, context).value;
|
|
34
36
|
}
|
package/src/catalog/manifest.ts
CHANGED
|
@@ -2,52 +2,68 @@ import { CatalogError } from "./errors";
|
|
|
2
2
|
import { parseLabel } from "./labels";
|
|
3
3
|
import type { Guideline } from "./model";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const REQUIRED_MANIFEST_HEADINGS = ["Section Roles", "Label Families"] as const;
|
|
6
6
|
|
|
7
7
|
export type ManifestDefinition = {
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
examples: string[];
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly description: string;
|
|
10
|
+
readonly examples: readonly string[];
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export type CatalogManifest = {
|
|
14
|
-
markdown: string;
|
|
15
|
-
sectionRoles: Record<string, ManifestDefinition
|
|
16
|
-
labelFamilies: Record<string, ManifestDefinition
|
|
14
|
+
readonly markdown: string;
|
|
15
|
+
readonly sectionRoles: Readonly<Record<string, ManifestDefinition>>;
|
|
16
|
+
readonly labelFamilies: Readonly<Record<string, ManifestDefinition>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ManifestDefinitions = {
|
|
20
|
+
"Section Roles": Record<string, ManifestDefinition>;
|
|
21
|
+
"Label Families": Record<string, ManifestDefinition>;
|
|
17
22
|
};
|
|
18
23
|
|
|
19
24
|
const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
25
|
+
|
|
20
26
|
const codeSpanPattern = /`([^`\n]+)`/g;
|
|
21
27
|
|
|
28
|
+
function manifestDefinitions(): Record<string, ManifestDefinition> {
|
|
29
|
+
const definitions: Record<string, ManifestDefinition> = Object.create(null);
|
|
30
|
+
|
|
31
|
+
return definitions;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
export function parseCatalogManifest(markdown: string): CatalogManifest {
|
|
23
35
|
const requiredSeen = new Set<string>();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"Section Roles": {},
|
|
29
|
-
"Label Families": {},
|
|
36
|
+
|
|
37
|
+
const definitions: ManifestDefinitions = {
|
|
38
|
+
"Section Roles": manifestDefinitions(),
|
|
39
|
+
"Label Families": manifestDefinitions(),
|
|
30
40
|
};
|
|
31
|
-
|
|
41
|
+
|
|
42
|
+
let currentHeading: string | undefined;
|
|
32
43
|
let currentName: string | undefined;
|
|
33
44
|
let currentLines: string[] = [];
|
|
34
45
|
|
|
35
46
|
function flushDefinition() {
|
|
36
47
|
if (currentHeading !== "Section Roles" && currentHeading !== "Label Families") {
|
|
37
48
|
currentLines = [];
|
|
49
|
+
|
|
38
50
|
return;
|
|
39
51
|
}
|
|
52
|
+
|
|
40
53
|
if (currentName === undefined) {
|
|
41
54
|
currentLines = [];
|
|
55
|
+
|
|
42
56
|
return;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
const description = currentLines.join("\n").trim();
|
|
60
|
+
|
|
46
61
|
if (!description) {
|
|
47
62
|
throw new CatalogError(
|
|
48
63
|
`Manifest definition ${currentHeading}/${currentName} must include prose.`,
|
|
49
64
|
);
|
|
50
65
|
}
|
|
66
|
+
|
|
51
67
|
definitions[currentHeading][currentName] = {
|
|
52
68
|
name: currentName,
|
|
53
69
|
description,
|
|
@@ -59,6 +75,7 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
|
|
|
59
75
|
|
|
60
76
|
for (const line of markdown.split("\n")) {
|
|
61
77
|
const match = headingPattern.exec(line);
|
|
78
|
+
|
|
62
79
|
if (!match) {
|
|
63
80
|
if (currentName !== undefined) currentLines.push(line);
|
|
64
81
|
continue;
|
|
@@ -72,9 +89,11 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
|
|
|
72
89
|
currentHeading = title;
|
|
73
90
|
currentName = undefined;
|
|
74
91
|
currentLines = [];
|
|
92
|
+
|
|
75
93
|
if (title === "Section Roles" || title === "Label Families") {
|
|
76
94
|
requiredSeen.add(title);
|
|
77
95
|
}
|
|
96
|
+
|
|
78
97
|
continue;
|
|
79
98
|
}
|
|
80
99
|
|
|
@@ -83,9 +102,11 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
|
|
|
83
102
|
(currentHeading === "Section Roles" || currentHeading === "Label Families")
|
|
84
103
|
) {
|
|
85
104
|
flushDefinition();
|
|
105
|
+
|
|
86
106
|
if (!title) {
|
|
87
107
|
throw new CatalogError(`Manifest heading ${currentHeading} contains an empty subheading.`);
|
|
88
108
|
}
|
|
109
|
+
|
|
89
110
|
currentName = title;
|
|
90
111
|
currentLines = [];
|
|
91
112
|
continue;
|
|
@@ -97,21 +118,32 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
|
|
|
97
118
|
flushDefinition();
|
|
98
119
|
|
|
99
120
|
const missing = REQUIRED_MANIFEST_HEADINGS.filter((heading) => !requiredSeen.has(heading));
|
|
121
|
+
|
|
100
122
|
if (missing.length > 0) {
|
|
101
123
|
throw new CatalogError(`MANIFEST.md is missing required heading(s): ${missing.join(", ")}.`);
|
|
102
124
|
}
|
|
125
|
+
|
|
103
126
|
for (const heading of REQUIRED_MANIFEST_HEADINGS) {
|
|
104
127
|
if (Object.keys(definitions[heading]).length === 0) {
|
|
105
|
-
throw new CatalogError(`Manifest heading ${heading} must
|
|
128
|
+
throw new CatalogError(`Manifest heading ${heading} must contain definitions.`);
|
|
106
129
|
}
|
|
107
130
|
}
|
|
131
|
+
|
|
108
132
|
validateLabelFamilyExamples(Object.values(definitions["Label Families"]));
|
|
109
133
|
|
|
110
|
-
return {
|
|
134
|
+
return copyCatalogManifest({
|
|
111
135
|
markdown: markdown.endsWith("\n") ? markdown : `${markdown}\n`,
|
|
112
136
|
sectionRoles: definitions["Section Roles"],
|
|
113
137
|
labelFamilies: definitions["Label Families"],
|
|
114
|
-
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function copyCatalogManifest(manifest: CatalogManifest): CatalogManifest {
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
markdown: manifest.markdown,
|
|
144
|
+
sectionRoles: copyDefinitions(manifest.sectionRoles),
|
|
145
|
+
labelFamilies: copyDefinitions(manifest.labelFamilies),
|
|
146
|
+
});
|
|
115
147
|
}
|
|
116
148
|
|
|
117
149
|
export function validateManifestCoverage(
|
|
@@ -124,57 +156,70 @@ export function validateManifestCoverage(
|
|
|
124
156
|
for (const guideline of guidelines) {
|
|
125
157
|
for (const section of guideline.sections) {
|
|
126
158
|
const role = section.role.trim();
|
|
159
|
+
|
|
127
160
|
if (!role) {
|
|
128
161
|
throw new CatalogError("Section role values must not be empty.");
|
|
129
162
|
}
|
|
163
|
+
|
|
164
|
+
if (role === "__dangling__") continue;
|
|
130
165
|
usedRoles.add(role);
|
|
131
166
|
}
|
|
167
|
+
|
|
132
168
|
for (const label of guideline.labels) {
|
|
133
169
|
usedFamilies.add(parseLabel(label, `label ${JSON.stringify(label)}`).family);
|
|
134
170
|
}
|
|
135
171
|
}
|
|
136
172
|
|
|
137
173
|
const missingRoles = Array.from(usedRoles)
|
|
138
|
-
.filter((role) => manifest.sectionRoles
|
|
174
|
+
.filter((role) => !Object.hasOwn(manifest.sectionRoles, role))
|
|
139
175
|
.sort();
|
|
176
|
+
|
|
140
177
|
const missingFamilies = Array.from(usedFamilies)
|
|
141
|
-
.filter((family) => manifest.labelFamilies
|
|
178
|
+
.filter((family) => !Object.hasOwn(manifest.labelFamilies, family))
|
|
142
179
|
.sort();
|
|
143
180
|
|
|
144
181
|
const errors: string[] = [];
|
|
182
|
+
|
|
145
183
|
if (missingRoles.length > 0) {
|
|
146
184
|
errors.push(`undefined section role(s): ${missingRoles.join(", ")}`);
|
|
147
185
|
}
|
|
186
|
+
|
|
148
187
|
if (missingFamilies.length > 0) {
|
|
149
188
|
errors.push(`undefined label family/families: ${missingFamilies.join(", ")}`);
|
|
150
189
|
}
|
|
190
|
+
|
|
151
191
|
if (errors.length > 0) {
|
|
152
192
|
throw new CatalogError(`Catalog manifest validation failed: ${errors.join("; ")}.`);
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
195
|
|
|
156
|
-
function validateLabelFamilyExamples(definitions: ManifestDefinition[]) {
|
|
196
|
+
function validateLabelFamilyExamples(definitions: readonly ManifestDefinition[]) {
|
|
157
197
|
for (const definition of definitions) {
|
|
158
198
|
const familyExamples: string[] = [];
|
|
159
199
|
const invalidExamples: string[] = [];
|
|
200
|
+
|
|
160
201
|
for (const example of definition.examples) {
|
|
161
202
|
let parsed;
|
|
203
|
+
|
|
162
204
|
try {
|
|
163
205
|
parsed = parseLabel(example, `manifest label example ${JSON.stringify(example)}`);
|
|
164
206
|
} catch {
|
|
165
207
|
continue;
|
|
166
208
|
}
|
|
209
|
+
|
|
167
210
|
if (parsed.family === definition.name) {
|
|
168
211
|
familyExamples.push(parsed.value);
|
|
169
212
|
} else if (example.includes(":")) {
|
|
170
213
|
invalidExamples.push(example);
|
|
171
214
|
}
|
|
172
215
|
}
|
|
216
|
+
|
|
173
217
|
if (invalidExamples.length > 0) {
|
|
174
218
|
throw new CatalogError(
|
|
175
219
|
`Label family ${definition.name} has example(s) from another family: ${invalidExamples.join(", ")}.`,
|
|
176
220
|
);
|
|
177
221
|
}
|
|
222
|
+
|
|
178
223
|
if (familyExamples.length === 0) {
|
|
179
224
|
throw new CatalogError(
|
|
180
225
|
`Label family ${definition.name} must include at least one label example.`,
|
|
@@ -182,3 +227,19 @@ function validateLabelFamilyExamples(definitions: ManifestDefinition[]) {
|
|
|
182
227
|
}
|
|
183
228
|
}
|
|
184
229
|
}
|
|
230
|
+
|
|
231
|
+
function copyDefinitions(
|
|
232
|
+
definitions: Readonly<Record<string, ManifestDefinition>>,
|
|
233
|
+
): Readonly<Record<string, ManifestDefinition>> {
|
|
234
|
+
const owned = manifestDefinitions();
|
|
235
|
+
|
|
236
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
237
|
+
owned[name] = Object.freeze({
|
|
238
|
+
name: definition.name,
|
|
239
|
+
description: definition.description,
|
|
240
|
+
examples: Object.freeze([...definition.examples]),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return Object.freeze(owned);
|
|
245
|
+
}
|
package/src/catalog/markdown.ts
CHANGED
|
@@ -3,15 +3,15 @@ import { stringify as stringifyYaml } from "yaml";
|
|
|
3
3
|
import type { Guideline } from "./model";
|
|
4
4
|
|
|
5
5
|
export function toMarkdown(
|
|
6
|
-
guideline: Pick<Guideline, "id" | "title" | "
|
|
6
|
+
guideline: Pick<Guideline, "id" | "title" | "description" | "labels" | "body">,
|
|
7
7
|
): string {
|
|
8
8
|
const frontmatter = {
|
|
9
9
|
id: guideline.id,
|
|
10
10
|
title: guideline.title,
|
|
11
|
-
...(guideline.bibliography ? { bibliography: guideline.bibliography } : {}),
|
|
12
11
|
description: guideline.description,
|
|
13
12
|
labels: [...guideline.labels],
|
|
14
13
|
};
|
|
14
|
+
|
|
15
15
|
const frontmatterYaml = stringifyYaml(frontmatter, { sortMapEntries: false }).trim();
|
|
16
16
|
const body = guideline.body.trim();
|
|
17
17
|
|