@intentset/core 0.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.
Files changed (76) hide show
  1. package/dist/artifact.d.ts +28 -0
  2. package/dist/artifact.d.ts.map +1 -0
  3. package/dist/artifact.js +621 -0
  4. package/dist/artifact.js.map +1 -0
  5. package/dist/carrier.d.ts +21 -0
  6. package/dist/carrier.d.ts.map +1 -0
  7. package/dist/carrier.js +70 -0
  8. package/dist/carrier.js.map +1 -0
  9. package/dist/config.d.ts +17 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +74 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/context.d.ts +28 -0
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/context.js +223 -0
  16. package/dist/context.js.map +1 -0
  17. package/dist/diagnostics.d.ts +37 -0
  18. package/dist/diagnostics.d.ts.map +1 -0
  19. package/dist/diagnostics.js +22 -0
  20. package/dist/diagnostics.js.map +1 -0
  21. package/dist/export.d.ts +23 -0
  22. package/dist/export.d.ts.map +1 -0
  23. package/dist/export.js +144 -0
  24. package/dist/export.js.map +1 -0
  25. package/dist/graph.d.ts +17 -0
  26. package/dist/graph.d.ts.map +1 -0
  27. package/dist/graph.js +51 -0
  28. package/dist/graph.js.map +1 -0
  29. package/dist/hash.d.ts +16 -0
  30. package/dist/hash.d.ts.map +1 -0
  31. package/dist/hash.js +55 -0
  32. package/dist/hash.js.map +1 -0
  33. package/dist/impact.d.ts +6 -0
  34. package/dist/impact.d.ts.map +1 -0
  35. package/dist/impact.js +89 -0
  36. package/dist/impact.js.map +1 -0
  37. package/dist/index.d.ts +15 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +15 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/registries.d.ts +19 -0
  42. package/dist/registries.d.ts.map +1 -0
  43. package/dist/registries.js +137 -0
  44. package/dist/registries.js.map +1 -0
  45. package/dist/report.d.ts +34 -0
  46. package/dist/report.d.ts.map +1 -0
  47. package/dist/report.js +58 -0
  48. package/dist/report.js.map +1 -0
  49. package/dist/types.d.ts +266 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +90 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/validate.d.ts +6 -0
  54. package/dist/validate.d.ts.map +1 -0
  55. package/dist/validate.js +573 -0
  56. package/dist/validate.js.map +1 -0
  57. package/dist/yaml.d.ts +48 -0
  58. package/dist/yaml.d.ts.map +1 -0
  59. package/dist/yaml.js +413 -0
  60. package/dist/yaml.js.map +1 -0
  61. package/package.json +36 -0
  62. package/src/artifact.ts +835 -0
  63. package/src/carrier.ts +81 -0
  64. package/src/config.ts +96 -0
  65. package/src/context.ts +240 -0
  66. package/src/diagnostics.ts +60 -0
  67. package/src/export.ts +173 -0
  68. package/src/graph.ts +49 -0
  69. package/src/hash.ts +53 -0
  70. package/src/impact.ts +90 -0
  71. package/src/index.ts +14 -0
  72. package/src/registries.ts +171 -0
  73. package/src/report.ts +84 -0
  74. package/src/types.ts +357 -0
  75. package/src/validate.ts +655 -0
  76. package/src/yaml.ts +406 -0
@@ -0,0 +1,28 @@
1
+ import type { Diagnostic } from "./diagnostics.ts";
2
+ import { type Locate } from "./report.ts";
3
+ import { type Artifact, type DocumentInput } from "./types.ts";
4
+ /** Core §3's file-size limit for one record. A product record is prose; a megabyte is far beyond any real one. */
5
+ export declare const DOCUMENT_MAX_BYTES: number;
6
+ export interface ReadDocument {
7
+ artifact: Artifact | null;
8
+ diagnostics: Diagnostic[];
9
+ /** The ID the document declares when it is a well-formed string, even if the record could not be read. */
10
+ id: string | null;
11
+ /** File line of a frontmatter JSON pointer, when the key exists. */
12
+ locate: Locate;
13
+ }
14
+ /** Core §3, §4, §6 and §7 (ADR 0002): read one document into an Artifact, reporting CORE001 and its kin with origin "profile". */
15
+ export declare function readArtifact(input: DocumentInput): {
16
+ artifact: Artifact | null;
17
+ diagnostics: Diagnostic[];
18
+ };
19
+ /** Core §3, §4, §6 and §7: `readArtifact` plus the positions the validator needs to place its own diagnostics on lines. */
20
+ export declare function readDocument(input: DocumentInput): ReadDocument;
21
+ /**
22
+ * Core §5 and §11: the ID a frontmatter that the reader rejected appears to declare, an `id:`
23
+ * line inside the top-level `intentset:` block. It is used only so references
24
+ * to the record are not also reported as unresolved; it is never reported as
25
+ * the record's identity, since the record was not read.
26
+ */
27
+ export declare function declaredId(text: string): string | null;
28
+ //# sourceMappingURL=artifact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../src/artifact.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,kBAAkB,CAAC;AAE7D,OAAO,EAAE,KAAK,MAAM,EAA4E,MAAM,aAAa,CAAC;AACpH,OAAO,EAEL,KAAK,QAAQ,EAMb,KAAK,aAAa,EAWnB,MAAM,YAAY,CAAC;AAGpB,kHAAkH;AAClH,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,0GAA0G;IAC1G,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,kIAAkI;AAClI,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG;IAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAG3G;AAED,2HAA2H;AAC3H,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CAqH/D;AA6lBD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAStD"}
@@ -0,0 +1,621 @@
1
+ /**
2
+ * Reading one document into an Artifact (Core §3, §4, §6, §7; ADR 0002).
3
+ *
4
+ * `readArtifact` checks, in typed code, every constraint of
5
+ * spec/frontmatter.schema.json plus the required narrative sections. Each
6
+ * schema constraint is CORE001 with a JSON pointer, except two that the
7
+ * schema also states and Core assigns elsewhere: a missing navigation parent
8
+ * is CORE004 (Core §5, the chain does not reach a product) and a missing
9
+ * `availability` on a behavior or knowledge is CORE005 (Core §7). A
10
+ * duplicate ID inside one link array is CORE003 (Core §5, duplicate edge).
11
+ * A test asserts that the schema and this code agree on validity.
12
+ *
13
+ * The record is returned whenever its identity and required common fields
14
+ * are sound; an optional structure that fails its checks is reported and
15
+ * left out of the metadata, so one bad link does not remove an artifact from
16
+ * the graph and cascade into unresolved references elsewhere.
17
+ */
18
+ import { closingFenceIndex } from "./carrier.js";
19
+ import { sha256Hex } from "./hash.js";
20
+ import { NO_LOCATION, aType, capitalize, compareStrings, isRecord, makeDiagnostic } from "./report.js";
21
+ import { ARTIFACT_TYPES, CLAIM_KINDS, ID_PATTERN, LINK_KINDS, PARENT_TARGETS, REQUIRED_SECTIONS, SPEC_VERSION, STATUSES, VISIBILITIES, } from "./types.js";
22
+ import { parseYamlDetailed, pointerToken } from "./yaml.js";
23
+ /** Core §3's file-size limit for one record. A product record is prose; a megabyte is far beyond any real one. */
24
+ export const DOCUMENT_MAX_BYTES = 1024 * 1024;
25
+ const REQUIRED_COMMON = [
26
+ "spec",
27
+ "profile",
28
+ "id",
29
+ "type",
30
+ "title",
31
+ "status",
32
+ "owner",
33
+ "visibility",
34
+ "audiences",
35
+ ];
36
+ const OPTIONAL_COMMON = [
37
+ "parent",
38
+ "links",
39
+ "availability",
40
+ "revision",
41
+ "reviewedAt",
42
+ "reviewedBy",
43
+ "extensions",
44
+ "slice",
45
+ "verification",
46
+ ];
47
+ const KNOWN_KEYS = new Set([...REQUIRED_COMMON, ...OPTIONAL_COMMON]);
48
+ const LINK_FIELDS = LINK_KINDS.filter((kind) => kind !== "parent");
49
+ const AVAILABILITY_KEYS = ["products", "releases", "roles", "editions", "flags"];
50
+ const EXTENSION_KEY = /^[^/]+\/.+$/;
51
+ /** Core §3, §4, §6 and §7 (ADR 0002): read one document into an Artifact, reporting CORE001 and its kin with origin "profile". */
52
+ export function readArtifact(input) {
53
+ const { artifact, diagnostics } = readDocument(input);
54
+ return { artifact, diagnostics };
55
+ }
56
+ /** Core §3, §4, §6 and §7: `readArtifact` plus the positions the validator needs to place its own diagnostics on lines. */
57
+ export function readDocument(input) {
58
+ const diagnostics = [];
59
+ const report = (fields) => {
60
+ diagnostics.push(makeDiagnostic({
61
+ code: fields.code ?? "CORE001",
62
+ severity: fields.severity,
63
+ origin: "profile",
64
+ artifact: fields.artifact,
65
+ path: input.path,
66
+ location: fields.location,
67
+ field: fields.field,
68
+ message: fields.message,
69
+ remediation: fields.remediation,
70
+ }));
71
+ };
72
+ // Core §3: implementations MUST limit file size. Checked before anything is parsed.
73
+ const bytes = Buffer.byteLength(input.source, "utf8");
74
+ if (bytes > DOCUMENT_MAX_BYTES) {
75
+ report({
76
+ artifact: null,
77
+ message: `The document is ${bytes} bytes, more than the ${DOCUMENT_MAX_BYTES / 1024 / 1024} MiB a record may be.`,
78
+ remediation: "Split the record, or move large material such as data or images out of the document and link it.",
79
+ });
80
+ return { artifact: null, diagnostics, id: null, locate: NO_LOCATION };
81
+ }
82
+ const frontmatter = input.frontmatter;
83
+ if (frontmatter === null) {
84
+ report({
85
+ artifact: null,
86
+ location: { line: 1 },
87
+ message: "The document has no frontmatter block between `---` lines at the top of the file.",
88
+ remediation: "Start the file with `---`, the `markset: 0` and `intentset:` keys, and a closing `---` line.",
89
+ });
90
+ return { artifact: null, diagnostics, id: null, locate: NO_LOCATION };
91
+ }
92
+ const parsed = parseYamlDetailed(frontmatter.text);
93
+ if (parsed.error) {
94
+ report({
95
+ artifact: null,
96
+ location: { line: frontmatter.line + parsed.error.line - 1 },
97
+ message: `The frontmatter is not accepted: ${parsed.error.message}`,
98
+ remediation: "Write the frontmatter in the strict subset: block mappings, sequences and quoted or plain scalars.",
99
+ });
100
+ return { artifact: null, diagnostics, id: declaredId(frontmatter.text), locate: NO_LOCATION };
101
+ }
102
+ const root = parsed.value;
103
+ const locate = (pointer) => {
104
+ const line = parsed.lines.get(pointer);
105
+ return line === undefined ? undefined : { line: frontmatter.line + line - 1 };
106
+ };
107
+ const section = root.intentset;
108
+ const id = isRecord(section) && typeof section.id === "string" && ID_PATTERN.test(section.id) ? section.id : null;
109
+ const problem = (field, message, remediation, code = "CORE001") => report({ code, artifact: id, location: locate(field), field, message, remediation });
110
+ if (!Object.hasOwn(root, "markset")) {
111
+ problem("/markset", "The frontmatter has no `markset` key.", "Add `markset: 0`, the Markset version the document is written against.");
112
+ }
113
+ else if (root.markset !== 0) {
114
+ problem("/markset", "`markset` must be the number 0.", "Write `markset: 0`, unquoted: it is a version, not a flag.");
115
+ }
116
+ if (!Object.hasOwn(root, "intentset")) {
117
+ problem("/intentset", "The frontmatter has no `intentset` block.", "Add an `intentset:` mapping with the common fields of Core §4.");
118
+ return { artifact: null, diagnostics, id, locate };
119
+ }
120
+ if (!isRecord(section)) {
121
+ problem("/intentset", "`intentset` must be a mapping.", "Write the common fields as keys under `intentset:`.");
122
+ return { artifact: null, diagnostics, id, locate };
123
+ }
124
+ const meta = readMeta(section, problem);
125
+ const usable = meta !== null;
126
+ if (usable)
127
+ checkSections(input, meta, id, report);
128
+ if (!usable)
129
+ return { artifact: null, diagnostics, id, locate };
130
+ const foreign = {};
131
+ for (const key of Object.keys(root).sort(compareStrings)) {
132
+ if (key !== "markset" && key !== "intentset")
133
+ foreign[key] = root[key];
134
+ }
135
+ return {
136
+ artifact: {
137
+ meta,
138
+ path: input.path,
139
+ sourceHash: sha256Hex(input.source),
140
+ body: bodyOf(input.source),
141
+ headings: input.headings,
142
+ foreign,
143
+ },
144
+ diagnostics,
145
+ id,
146
+ locate,
147
+ };
148
+ }
149
+ /** The `intentset` block against Core §4 and the schema; null when identity or a required common field is unusable. */
150
+ function readMeta(s, problem) {
151
+ let usable = true;
152
+ const base = "/intentset";
153
+ for (const key of Object.keys(s).sort(compareStrings)) {
154
+ if (!KNOWN_KEYS.has(key)) {
155
+ problem(`${base}/${pointerToken(key)}`, `Unknown key \`${key}\` under intentset.`, "Remove it, or move it under `extensions` with a namespaced key such as `org.example/change`.");
156
+ }
157
+ }
158
+ for (const key of [...REQUIRED_COMMON, ...OPTIONAL_COMMON]) {
159
+ if (Object.hasOwn(s, key) && s[key] === null) {
160
+ problem(`${base}/${key}`, `\`${key}\` is present with no value.`, `Give \`${key}\` a value, or remove the key.`);
161
+ }
162
+ }
163
+ for (const key of REQUIRED_COMMON) {
164
+ if (!Object.hasOwn(s, key)) {
165
+ problem(`${base}/${key}`, `Required field \`${key}\` is missing.`, `Add \`${key}\` under intentset (Core §4).`);
166
+ }
167
+ if (!Object.hasOwn(s, key) || s[key] === null)
168
+ usable = false;
169
+ }
170
+ if (Object.hasOwn(s, "spec") && s.spec !== null && s.spec !== SPEC_VERSION) {
171
+ const hint = typeof s.spec === "number" ? " Quote it: unquoted, it reads as a number." : "";
172
+ problem(`${base}/spec`, `\`spec\` must be the string "${SPEC_VERSION}".${hint}`, `Write \`spec: '${SPEC_VERSION}'\`.`);
173
+ usable = false;
174
+ }
175
+ const idValue = s.id;
176
+ if (idValue !== undefined && idValue !== null && (typeof idValue !== "string" || !ID_PATTERN.test(idValue))) {
177
+ problem(`${base}/id`, `\`id\` must match ${ID_PATTERN.source}.`, "Use upper-case letters and digits in dash-separated groups, such as BEH-ASMT-SCHEDULE.");
178
+ usable = false;
179
+ }
180
+ const typeValue = s.type;
181
+ const type = typeof typeValue === "string" && ARTIFACT_TYPES.includes(typeValue)
182
+ ? typeValue
183
+ : null;
184
+ if (typeValue !== undefined && typeValue !== null && type === null) {
185
+ problem(`${base}/type`, `\`type\` must be one of ${ARTIFACT_TYPES.join(", ")}.`, "Choose the Core §2 type the artifact is.");
186
+ usable = false;
187
+ }
188
+ if (Object.hasOwn(s, "profile") && s.profile !== null) {
189
+ if (typeof s.profile !== "string") {
190
+ problem(`${base}/profile`, "`profile` must be a string.", "Write `profile: intentset/<type>/0.1`.");
191
+ usable = false;
192
+ }
193
+ else if (type !== null && s.profile !== `intentset/${type}/${SPEC_VERSION}`) {
194
+ problem(`${base}/profile`, `\`profile\` must equal intentset/${type}/${SPEC_VERSION} for ${aType(type)}.`, `Set profile to intentset/${type}/${SPEC_VERSION}, or change type.`);
195
+ usable = false;
196
+ }
197
+ }
198
+ if (Object.hasOwn(s, "title") && s.title !== null && (typeof s.title !== "string" || s.title === "")) {
199
+ problem(`${base}/title`, "`title` must be a non-empty string.", "Give the artifact a title; quote it if it would read as a number.");
200
+ usable = false;
201
+ }
202
+ if (Object.hasOwn(s, "status") && s.status !== null && !STATUSES.includes(s.status)) {
203
+ problem(`${base}/status`, `\`status\` must be one of ${STATUSES.join(", ")}.`, "Use the Core §7 lifecycle status.");
204
+ usable = false;
205
+ }
206
+ if (Object.hasOwn(s, "owner") && s.owner !== null && (typeof s.owner !== "string" || s.owner === "")) {
207
+ problem(`${base}/owner`, "`owner` must be a non-empty string.", "Name one accountable team or role from the owner registry.");
208
+ usable = false;
209
+ }
210
+ if (Object.hasOwn(s, "visibility") &&
211
+ s.visibility !== null &&
212
+ !VISIBILITIES.includes(s.visibility)) {
213
+ problem(`${base}/visibility`, `\`visibility\` must be one of ${VISIBILITIES.join(", ")}.`, "Choose the narrowest visibility that fits; publication fails closed.");
214
+ usable = false;
215
+ }
216
+ let audiences = null;
217
+ if (Object.hasOwn(s, "audiences") && s.audiences !== null) {
218
+ audiences = stringList(s.audiences, `${base}/audiences`, "audiences", problem, { minItems: 1 });
219
+ if (audiences === null)
220
+ usable = false;
221
+ }
222
+ let parent;
223
+ if (Object.hasOwn(s, "parent") && s.parent !== null) {
224
+ if (typeof s.parent === "string" && ID_PATTERN.test(s.parent))
225
+ parent = s.parent;
226
+ else
227
+ problem(`${base}/parent`, "`parent` must be an artifact ID.", "Name the navigation parent's ID, such as CAP-ASMT-ASSIGN.");
228
+ }
229
+ else if (!Object.hasOwn(s, "parent") && type !== null && PARENT_TARGETS[type] !== undefined) {
230
+ problem(`${base}/parent`, `${capitalize(aType(type))} has no \`parent\`, so its navigation chain does not reach a product.`, `Add \`parent\` naming the ${PARENT_TARGETS[type]?.join(" or ")} it belongs to (Core §5).`, "CORE004");
231
+ }
232
+ const links = readLinks(s.links, base, problem);
233
+ let availability;
234
+ if (Object.hasOwn(s, "availability") && s.availability !== null) {
235
+ availability = readAvailability(s.availability, `${base}/availability`, problem) ?? undefined;
236
+ }
237
+ else if (!Object.hasOwn(s, "availability") && (type === "behavior" || type === "knowledge")) {
238
+ problem(`${base}/availability`, `${capitalize(aType(type))} must declare \`availability\` with products, releases, roles, editions and flags.`, "Add the availability block; empty flags is `flags: []`, never an omission (Core §7).", "CORE005");
239
+ }
240
+ let revision;
241
+ if (Object.hasOwn(s, "revision") && s.revision !== null) {
242
+ if (typeof s.revision === "number" && Number.isInteger(s.revision) && s.revision >= 1)
243
+ revision = s.revision;
244
+ else
245
+ problem(`${base}/revision`, "`revision` must be a positive integer.", "Write an unquoted whole number from 1 upward.");
246
+ }
247
+ let reviewedAt;
248
+ if (Object.hasOwn(s, "reviewedAt") && s.reviewedAt !== null) {
249
+ if (typeof s.reviewedAt === "string")
250
+ reviewedAt = s.reviewedAt;
251
+ else
252
+ problem(`${base}/reviewedAt`, "`reviewedAt` must be a string.", "Quote the date, for example '2026-10-02'.");
253
+ }
254
+ let reviewedBy;
255
+ if (Object.hasOwn(s, "reviewedBy") && s.reviewedBy !== null) {
256
+ if (typeof s.reviewedBy === "string")
257
+ reviewedBy = s.reviewedBy;
258
+ else
259
+ problem(`${base}/reviewedBy`, "`reviewedBy` must be a string.", "Name the reviewer or review record.");
260
+ }
261
+ let extensions;
262
+ if (Object.hasOwn(s, "extensions") && s.extensions !== null) {
263
+ if (!isRecord(s.extensions)) {
264
+ problem(`${base}/extensions`, "`extensions` must be a mapping.", "Write namespaced keys under `extensions:`.");
265
+ }
266
+ else {
267
+ extensions = {};
268
+ for (const key of Object.keys(s.extensions).sort(compareStrings)) {
269
+ if (EXTENSION_KEY.test(key))
270
+ extensions[key] = s.extensions[key];
271
+ else
272
+ problem(`${base}/extensions/${pointerToken(key)}`, `Extension key \`${key}\` is not namespaced.`, "Prefix it with a namespace and a slash, such as `org.example/change`.");
273
+ }
274
+ }
275
+ }
276
+ let slice;
277
+ if (Object.hasOwn(s, "slice") && s.slice !== null) {
278
+ slice = readSlice(s.slice, `${base}/slice`, problem) ?? undefined;
279
+ }
280
+ else if (!Object.hasOwn(s, "slice") && type === "slice") {
281
+ problem(`${base}/slice`, "A slice must carry the `slice` metadata block.", "Add `slice:` with kind, domain, entrypoints, layers, claims and usesResources (VSA §3).");
282
+ }
283
+ let verification;
284
+ if (Object.hasOwn(s, "verification") && s.verification !== null) {
285
+ verification = readVerification(s.verification, `${base}/verification`, problem) ?? undefined;
286
+ }
287
+ else if (!Object.hasOwn(s, "verification") && type === "verification") {
288
+ problem(`${base}/verification`, "A verification must carry the `verification` metadata block.", "Add `verification:` with method, locator and selector (Core §8).");
289
+ }
290
+ if (!usable || type === null || audiences === null)
291
+ return null;
292
+ const meta = {
293
+ spec: SPEC_VERSION,
294
+ profile: s.profile,
295
+ id: s.id,
296
+ type,
297
+ title: s.title,
298
+ status: s.status,
299
+ owner: s.owner,
300
+ visibility: s.visibility,
301
+ audiences,
302
+ links,
303
+ };
304
+ if (parent !== undefined)
305
+ meta.parent = parent;
306
+ if (availability !== undefined)
307
+ meta.availability = availability;
308
+ if (revision !== undefined)
309
+ meta.revision = revision;
310
+ if (reviewedAt !== undefined)
311
+ meta.reviewedAt = reviewedAt;
312
+ if (reviewedBy !== undefined)
313
+ meta.reviewedBy = reviewedBy;
314
+ if (extensions !== undefined)
315
+ meta.extensions = extensions;
316
+ if (slice !== undefined)
317
+ meta.slice = slice;
318
+ if (verification !== undefined)
319
+ meta.verification = verification;
320
+ return meta;
321
+ }
322
+ /**
323
+ * An array of non-empty unique strings. Bad and repeated entries are reported
324
+ * and left out, so one bad ID does not detach the others; null when the value
325
+ * is not an array or too few good entries remain.
326
+ */
327
+ function stringList(value, field, name, problem, options = {}) {
328
+ if (!Array.isArray(value)) {
329
+ problem(field, `\`${name}\` must be a list.`, `Write ${name} as a sequence, for example \`${name}: [a, b]\`.`);
330
+ return null;
331
+ }
332
+ const out = [];
333
+ value.forEach((item, i) => {
334
+ if (typeof item !== "string" || item === "") {
335
+ problem(`${field}/${i}`, `Entry ${i} of \`${name}\` must be a non-empty string.`, "Quote the value if it reads as a number or boolean.");
336
+ return;
337
+ }
338
+ if (options.pattern !== undefined && !options.pattern.test(item)) {
339
+ problem(`${field}/${i}`, `Entry ${i} of \`${name}\` is not an artifact ID.`, "Use an ID such as BEH-ASMT-SCHEDULE.");
340
+ return;
341
+ }
342
+ if (out.includes(item)) {
343
+ problem(`${field}/${i}`, `\`${item}\` appears twice in \`${name}\`.`, options.duplicateRemediation ?? "Keep one entry per ID.", options.duplicateCode ?? "CORE001");
344
+ return;
345
+ }
346
+ out.push(item);
347
+ });
348
+ if (options.minItems !== undefined && out.length < options.minItems) {
349
+ if (value.length < options.minItems) {
350
+ problem(field, `\`${name}\` must list at least ${options.minItems} entr${options.minItems === 1 ? "y" : "ies"}.`, `Add at least one value to ${name}.`);
351
+ }
352
+ return null;
353
+ }
354
+ return out;
355
+ }
356
+ function readLinks(value, base, problem) {
357
+ const links = {};
358
+ if (value === undefined || value === null)
359
+ return links;
360
+ if (!isRecord(value)) {
361
+ problem(`${base}/links`, "`links` must be a mapping of relationship names to ID lists.", "Write `links:` with keys from the Core §5 table.");
362
+ return links;
363
+ }
364
+ for (const key of Object.keys(value).sort(compareStrings)) {
365
+ if (!LINK_FIELDS.includes(key)) {
366
+ problem(`${base}/links/${pointerToken(key)}`, `Unknown relationship \`${key}\` under links.`, `Use one of ${LINK_FIELDS.join(", ")}; inverse edges are derived, never written (Core §5).`);
367
+ }
368
+ }
369
+ for (const kind of LINK_FIELDS) {
370
+ if (!Object.hasOwn(value, kind))
371
+ continue;
372
+ const list = stringList(value[kind], `${base}/links/${kind}`, kind, problem, {
373
+ pattern: ID_PATTERN,
374
+ duplicateCode: "CORE003",
375
+ });
376
+ if (list !== null)
377
+ links[kind] = list;
378
+ }
379
+ return links;
380
+ }
381
+ function readAvailability(value, field, problem) {
382
+ if (!isRecord(value)) {
383
+ problem(field, "`availability` must be a mapping with products, releases, roles, editions and flags.", "Write the five dimension lists under `availability:`.");
384
+ return null;
385
+ }
386
+ let ok = true;
387
+ for (const key of Object.keys(value).sort(compareStrings)) {
388
+ if (!AVAILABILITY_KEYS.includes(key)) {
389
+ problem(`${field}/${pointerToken(key)}`, `Unknown availability dimension \`${key}\`.`, `The dimensions are ${AVAILABILITY_KEYS.join(", ")}.`);
390
+ ok = false;
391
+ }
392
+ }
393
+ const out = {};
394
+ for (const key of AVAILABILITY_KEYS) {
395
+ if (!Object.hasOwn(value, key) || value[key] === null) {
396
+ problem(`${field}/${key}`, `Availability dimension \`${key}\` is missing.`, key === "flags"
397
+ ? "Write `flags: []` when no flag is required; no implicit wildcard exists."
398
+ : `List the ${key} the artifact applies to.`);
399
+ ok = false;
400
+ continue;
401
+ }
402
+ const list = stringList(value[key], `${field}/${key}`, key, problem, { minItems: key === "flags" ? 0 : 1 });
403
+ if (list === null)
404
+ ok = false;
405
+ else
406
+ out[key] = list;
407
+ }
408
+ return ok ? out : null;
409
+ }
410
+ function readSlice(value, field, problem) {
411
+ if (!isRecord(value)) {
412
+ problem(field, "`slice` must be a mapping.", "Write kind, domain, entrypoints, layers, claims and usesResources under `slice:`.");
413
+ return null;
414
+ }
415
+ let ok = true;
416
+ const known = ["kind", "rationale", "domain", "entrypoints", "layers", "claims", "usesResources"];
417
+ for (const key of Object.keys(value).sort(compareStrings)) {
418
+ if (!known.includes(key)) {
419
+ problem(`${field}/${pointerToken(key)}`, `Unknown slice key \`${key}\`.`, key === "entrypoint"
420
+ ? "A slice lists its public surfaces, one per package it spans: write `entrypoints: [path]` (VSA §3)."
421
+ : `The slice keys are ${known.join(", ")} (VSA §3).`);
422
+ ok = false;
423
+ }
424
+ }
425
+ for (const key of ["kind", "domain", "entrypoints", "layers", "claims", "usesResources"]) {
426
+ if (!Object.hasOwn(value, key) || value[key] === null) {
427
+ problem(`${field}/${key}`, `Slice field \`${key}\` is missing.`, `Add \`${key}\` under slice (VSA §3).`);
428
+ ok = false;
429
+ }
430
+ }
431
+ if (Object.hasOwn(value, "kind") && value.kind !== null && value.kind !== "product" && value.kind !== "technical") {
432
+ problem(`${field}/kind`, "`slice.kind` must be product or technical.", "A slice that owns behaviors is product; one that owns only mechanisms is technical.");
433
+ ok = false;
434
+ }
435
+ let rationale;
436
+ if (Object.hasOwn(value, "rationale")) {
437
+ if (typeof value.rationale === "string")
438
+ rationale = value.rationale;
439
+ else {
440
+ problem(`${field}/rationale`, "`slice.rationale` must be a string.", "Explain in one sentence why the slice is technical.");
441
+ ok = false;
442
+ }
443
+ }
444
+ if (Object.hasOwn(value, "domain") && value.domain !== null && typeof value.domain !== "string") {
445
+ problem(`${field}/domain`, "`slice.domain` must be a string.", "Name the product-oriented grouping the slice belongs to.");
446
+ ok = false;
447
+ }
448
+ let entrypoints = [];
449
+ if (Object.hasOwn(value, "entrypoints") && value.entrypoints !== null) {
450
+ const list = stringList(value.entrypoints, `${field}/entrypoints`, "entrypoints", problem, {
451
+ minItems: 1,
452
+ duplicateRemediation: "List each entrypoint once.",
453
+ });
454
+ if (list === null)
455
+ ok = false;
456
+ else
457
+ entrypoints = list;
458
+ }
459
+ const layers = {};
460
+ if (Object.hasOwn(value, "layers") && value.layers !== null) {
461
+ if (!isRecord(value.layers)) {
462
+ problem(`${field}/layers`, "`slice.layers` must be a mapping of layer names to pattern lists.", "Write each layer as `name: [pattern, ...]`; `{}` declares none.");
463
+ ok = false;
464
+ }
465
+ else {
466
+ for (const layer of Object.keys(value.layers).sort(compareStrings)) {
467
+ const list = stringList(value.layers[layer], `${field}/layers/${pointerToken(layer)}`, `layers.${layer}`, problem);
468
+ if (list === null)
469
+ ok = false;
470
+ else
471
+ layers[layer] = list;
472
+ }
473
+ }
474
+ }
475
+ const claims = [];
476
+ if (Object.hasOwn(value, "claims") && value.claims !== null) {
477
+ if (!Array.isArray(value.claims)) {
478
+ problem(`${field}/claims`, "`slice.claims` must be a list of `{ kind, path }` records.", "Write each claim as `- kind: source` and `path: ...`.");
479
+ ok = false;
480
+ }
481
+ else {
482
+ value.claims.forEach((claim, i) => {
483
+ const read = readClaim(claim, `${field}/claims/${i}`, problem);
484
+ if (read === null)
485
+ ok = false;
486
+ else
487
+ claims.push(read);
488
+ });
489
+ }
490
+ }
491
+ let usesResources = [];
492
+ if (Object.hasOwn(value, "usesResources") && value.usesResources !== null) {
493
+ const list = stringList(value.usesResources, `${field}/usesResources`, "usesResources", problem, {
494
+ pattern: ID_PATTERN,
495
+ });
496
+ if (list === null)
497
+ ok = false;
498
+ else
499
+ usesResources = list;
500
+ }
501
+ if (!ok)
502
+ return null;
503
+ const slice = {
504
+ kind: value.kind,
505
+ domain: value.domain,
506
+ entrypoints,
507
+ layers,
508
+ claims,
509
+ usesResources,
510
+ };
511
+ if (rationale !== undefined)
512
+ slice.rationale = rationale;
513
+ return slice;
514
+ }
515
+ function readClaim(value, field, problem) {
516
+ if (!isRecord(value)) {
517
+ problem(field, "A claim must be a mapping with kind and path.", "Write `- kind: source` and `path: src/...`.");
518
+ return null;
519
+ }
520
+ let ok = true;
521
+ for (const key of Object.keys(value).sort(compareStrings)) {
522
+ if (key !== "kind" && key !== "path") {
523
+ problem(`${field}/${pointerToken(key)}`, `Unknown claim key \`${key}\`.`, "A claim has kind and path only.");
524
+ ok = false;
525
+ }
526
+ }
527
+ if (!CLAIM_KINDS.includes(value.kind)) {
528
+ problem(`${field}/kind`, `\`claims.kind\` must be one of ${CLAIM_KINDS.join(", ")}.`, "Choose the kind of files the pattern claims.");
529
+ ok = false;
530
+ }
531
+ if (typeof value.path !== "string" || value.path === "") {
532
+ problem(`${field}/path`, "`claims.path` must be a non-empty pattern.", "Give a repository-relative POSIX pattern such as src/features/x/**.");
533
+ ok = false;
534
+ }
535
+ return ok ? { kind: value.kind, path: value.path } : null;
536
+ }
537
+ function readVerification(value, field, problem) {
538
+ if (!isRecord(value)) {
539
+ problem(field, "`verification` must be a mapping with method, locator and selector.", "Write the three keys under `verification:` (Core §8).");
540
+ return null;
541
+ }
542
+ let ok = true;
543
+ for (const key of Object.keys(value).sort(compareStrings)) {
544
+ if (!["method", "locator", "selector"].includes(key)) {
545
+ problem(`${field}/${pointerToken(key)}`, `Unknown verification key \`${key}\`.`, "The keys are method, locator and selector.");
546
+ ok = false;
547
+ }
548
+ }
549
+ if (value.method !== "automated" && value.method !== "manual") {
550
+ problem(`${field}/method`, "`verification.method` must be automated or manual.", "Say whether a tool or a reviewer performs the check.");
551
+ ok = false;
552
+ }
553
+ for (const key of ["locator", "selector"]) {
554
+ if (typeof value[key] !== "string" || value[key] === "") {
555
+ problem(`${field}/${key}`, `\`verification.${key}\` must be a non-empty string.`, key === "locator"
556
+ ? "Give the repository-relative path of the check."
557
+ : "Give the stable test or review-case identifier.");
558
+ ok = false;
559
+ }
560
+ }
561
+ if (!ok)
562
+ return null;
563
+ return {
564
+ method: value.method,
565
+ locator: value.locator,
566
+ selector: value.selector,
567
+ };
568
+ }
569
+ /** Core §6: a level-one heading equal to the title and the type's level-two sections, outside code fences. */
570
+ function checkSections(input, meta, id, report) {
571
+ const title = meta.title.replace(/\s+/g, " ").trim();
572
+ const level1 = input.headings.filter((h) => h.depth === 1);
573
+ if (!level1.some((h) => h.text === title)) {
574
+ const first = level1[0];
575
+ report({
576
+ artifact: id,
577
+ location: first === undefined ? undefined : { line: first.line },
578
+ message: first === undefined
579
+ ? `The document has no level-one heading; Core §6 requires one equal to the title "${title}".`
580
+ : `The level-one heading "${first.text}" does not match the title "${title}".`,
581
+ remediation: `Add or correct the heading \`# ${title}\` so it equals intentset.title.`,
582
+ });
583
+ }
584
+ const level2 = new Set(input.headings.filter((h) => h.depth === 2).map((h) => h.text));
585
+ for (const section of REQUIRED_SECTIONS[meta.type]) {
586
+ if (level2.has(section))
587
+ continue;
588
+ report({
589
+ artifact: id,
590
+ message: `Required section "${section}" is missing from ${aType(meta.type)} document.`,
591
+ remediation: `Add a level-two heading \`## ${section}\` with substantive prose (Core §6).`,
592
+ });
593
+ }
594
+ }
595
+ /**
596
+ * Core §5 and §11: the ID a frontmatter that the reader rejected appears to declare, an `id:`
597
+ * line inside the top-level `intentset:` block. It is used only so references
598
+ * to the record are not also reported as unresolved; it is never reported as
599
+ * the record's identity, since the record was not read.
600
+ */
601
+ export function declaredId(text) {
602
+ let inside = false;
603
+ for (const line of text.split(/\r?\n/)) {
604
+ if (/^\S/.test(line))
605
+ inside = /^intentset:\s*(?:#.*)?$/.test(line);
606
+ if (!inside)
607
+ continue;
608
+ const match = /^ +id: *(['"]?)([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\1 *(?:#.*)?$/.exec(line);
609
+ if (match !== null)
610
+ return match[2];
611
+ }
612
+ return null;
613
+ }
614
+ /** Everything after the closing frontmatter fence, line endings kept. */
615
+ function bodyOf(source) {
616
+ const text = source.startsWith("") ? source.slice(1) : source;
617
+ const lines = text.split("\n");
618
+ const close = closingFenceIndex(lines);
619
+ return close === -1 ? text : lines.slice(close + 1).join("\n");
620
+ }
621
+ //# sourceMappingURL=artifact.js.map