@yukyu30/fluorite 0.1.0 → 0.1.2

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.
@@ -1,9 +1,11 @@
1
1
  // src/parse.ts
2
2
  import matter from "gray-matter";
3
+ import yaml from "js-yaml";
4
+ var keepDatesAsStrings = (input) => yaml.load(input, { schema: yaml.CORE_SCHEMA }) ?? {};
3
5
  function parseFrontmatter(source) {
4
6
  const hasFrontmatter = /^?\s*---\r?\n/.test(source);
5
7
  try {
6
- const parsed = matter(source);
8
+ const parsed = matter(source, { engines: { yaml: keepDatesAsStrings } });
7
9
  const data = parsed.data ?? {};
8
10
  return {
9
11
  data,
@@ -25,6 +27,7 @@ var MISSING = /* @__PURE__ */ Symbol("missing");
25
27
  function valueType(value) {
26
28
  if (value === MISSING) return "undefined";
27
29
  if (value === null) return "null";
30
+ if (value instanceof Date) return "date";
28
31
  if (Array.isArray(value)) return "array";
29
32
  const t = typeof value;
30
33
  if (t === "string" || t === "number" || t === "boolean" || t === "object") {
@@ -181,6 +184,27 @@ var KeyAssertion = class {
181
184
  pattern.source
182
185
  );
183
186
  }
187
+ /**
188
+ * Assert the value is a calendar date written as `YYYY-MM-DD`.
189
+ *
190
+ * fluorite keeps unquoted YAML dates as strings (see {@link parseFrontmatter}),
191
+ * so this checks the written form exactly: a full timestamp
192
+ * (`2026-06-07 10:30:00`), a loosely-padded date (`2026-6-7`), or an
193
+ * impossible date (`2026-02-30`) all fail.
194
+ *
195
+ * ```ts
196
+ * fm.key("date").required().isoDate();
197
+ * ```
198
+ */
199
+ isoDate() {
200
+ const v = this.resolved;
201
+ const pass = typeof v === "string" && isCalendarDate(v);
202
+ return this.record(
203
+ "isoDate",
204
+ pass,
205
+ (neg) => neg ? `should not be a YYYY-MM-DD date` : `should be a YYYY-MM-DD date (was ${typeof v === "string" ? display(v) : valueType(v)})`
206
+ );
207
+ }
184
208
  /** Assert an array contains `item`, or a string contains the substring. */
185
209
  has(item) {
186
210
  const v = this.resolved;
@@ -309,7 +333,24 @@ var EachAssertion = class {
309
333
  pattern.source
310
334
  );
311
335
  }
336
+ /** Every element must be a `YYYY-MM-DD` calendar date string. */
337
+ isoDate() {
338
+ return this.record(
339
+ "isoDate",
340
+ (el) => typeof el === "string" && isCalendarDate(el),
341
+ (neg, invalid, isArray) => !isArray ? `should be an array of YYYY-MM-DD dates` : neg ? `every item should not be a YYYY-MM-DD date` : `every item should be a YYYY-MM-DD date${invalid.length ? ` (invalid: ${display(invalid)})` : ""}`
342
+ );
343
+ }
312
344
  };
345
+ function isCalendarDate(value) {
346
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
347
+ if (!m) return false;
348
+ const year = Number(m[1]);
349
+ const month = Number(m[2]);
350
+ const day = Number(m[3]);
351
+ const dt = new Date(Date.UTC(year, month - 1, day));
352
+ return dt.getUTCFullYear() === year && dt.getUTCMonth() === month - 1 && dt.getUTCDate() === day;
353
+ }
313
354
  function contains(container, item) {
314
355
  if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));
315
356
  if (typeof container === "string" && typeof item === "string")
@@ -481,4 +522,4 @@ export {
481
522
  loadConfig,
482
523
  formatReports
483
524
  };
484
- //# sourceMappingURL=chunk-2QW4LSG5.js.map
525
+ //# sourceMappingURL=chunk-PTAYLHQM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parse.ts","../src/assertion.ts","../src/recorder.ts","../src/check.ts","../src/config.ts","../src/report.ts"],"sourcesContent":["import matter from \"gray-matter\";\nimport yaml from \"js-yaml\";\n\n/**\n * gray-matter's default YAML engine follows YAML 1.1 and coerces unquoted\n * dates (`date: 2026-06-07`) into JavaScript `Date` objects. That erases how\n * the value was written, so the format can no longer be linted — a clean\n * `YYYY-MM-DD`, a full timestamp, and an impossible date all collapse to a\n * `Date` (or get silently re-serialised).\n *\n * We swap in js-yaml's `CORE_SCHEMA`, which omits the `!!timestamp` type, so\n * every date stays a verbatim string. Matchers like `isoDate()` / `matches()`\n * can then validate the written `YYYY-MM-DD` form without requiring every\n * value to be quoted. Booleans, numbers and `null` are still parsed.\n */\nconst keepDatesAsStrings = (input: string): object =>\n (yaml.load(input, { schema: yaml.CORE_SCHEMA }) as object) ?? {};\n\n/** Outcome of parsing frontmatter out of a Markdown source string. */\nexport interface ParseResult {\n /** Parsed frontmatter data (empty object when none / on error). */\n data: Record<string, unknown>;\n /** The Markdown body following the frontmatter block. */\n content: string;\n /** Whether a frontmatter block was present at all. */\n hasFrontmatter: boolean;\n /** Parse error message, if the frontmatter (YAML) was malformed. */\n error?: string;\n}\n\n/**\n * Extract frontmatter from a Markdown source string.\n *\n * Never throws: malformed YAML is reported via the `error` field so callers\n * can record it as a failure rather than crash.\n */\nexport function parseFrontmatter(source: string): ParseResult {\n const hasFrontmatter = /^?\\s*---\\r?\\n/.test(source);\n try {\n const parsed = matter(source, { engines: { yaml: keepDatesAsStrings } });\n const data = (parsed.data ?? {}) as Record<string, unknown>;\n return {\n data,\n content: parsed.content,\n hasFrontmatter,\n };\n } catch (err) {\n return {\n data: {},\n content: source,\n hasFrontmatter,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n","import type { Recorder } from \"./recorder.js\";\nimport type { RuleResult, ValueType } from \"./types.js\";\n\n/** Sentinel meaning \"the key was not present in the frontmatter\". */\nconst MISSING = Symbol(\"missing\");\n\nfunction valueType(value: unknown): ValueType | \"undefined\" {\n if (value === MISSING) return \"undefined\";\n if (value === null) return \"null\";\n if (value instanceof Date) return \"date\";\n if (Array.isArray(value)) return \"array\";\n const t = typeof value;\n if (t === \"string\" || t === \"number\" || t === \"boolean\" || t === \"object\") {\n return t;\n }\n return \"undefined\";\n}\n\nfunction lengthOf(value: unknown): number | undefined {\n if (typeof value === \"string\" || Array.isArray(value)) return value.length;\n return undefined;\n}\n\nfunction display(value: unknown): string {\n if (value === MISSING) return \"undefined\";\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Fluent, chainable assertions for a single frontmatter key.\n *\n * Each terminal matcher records one {@link RuleResult} on the parent\n * {@link Recorder} and returns `this`, so matchers can be chained:\n *\n * ```ts\n * fm.key(\"title\").required().type(\"string\").lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * ```\n *\n * The `.not` modifier negates only the next matcher, then resets.\n */\nexport class KeyAssertion {\n #negated = false;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n private readonly present: boolean,\n ) {}\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n /** The raw value (resolved against the sentinel) used by matchers. */\n private get resolved(): unknown {\n return this.present ? this.value : MISSING;\n }\n\n /**\n * Record a result, applying the pending `.not` negation, then reset it.\n */\n private record(\n rule: string,\n rawPass: boolean,\n describe: (negated: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const ok = negated ? !rawPass : rawPass;\n const result: RuleResult = {\n key: this.key,\n rule,\n ok,\n negated,\n message: describe(negated),\n value: this.present ? this.value : undefined,\n expected,\n };\n this.recorder.push(result);\n return this;\n }\n\n /** Assert the key exists in the frontmatter. */\n required(): this {\n return this.record(\n \"required\",\n this.present,\n (neg) => (neg ? `should not exist` : `is required`),\n );\n }\n\n /** Alias of {@link required}. */\n exists(): this {\n return this.record(\n \"exists\",\n this.present,\n (neg) => (neg ? `should not exist` : `should exist`),\n );\n }\n\n /** Assert the value is of the given type. */\n type(expected: ValueType): this {\n const actual = valueType(this.resolved);\n return this.record(\n \"type\",\n actual === expected,\n (neg) =>\n neg\n ? `should not be of type ${expected}`\n : `should be of type ${expected} (was ${actual})`,\n expected,\n );\n }\n\n /** Assert the value strictly equals `expected` (deep for arrays/objects). */\n eq(expected: unknown): this {\n return this.record(\n \"eq\",\n deepEqual(this.resolved, expected),\n (neg) =>\n neg\n ? `should not equal ${display(expected)}`\n : `should equal ${display(expected)}`,\n expected,\n );\n }\n\n /**\n * Assert the value is an array whose every element is in `allowed` (enum).\n *\n * Designed for catching tag notation drift: define the canonical set once\n * and any stray / mistyped value is reported.\n *\n * ```ts\n * fm.key(\"tags\").subsetOf([\"ok\", \"release\", \"blog\"]);\n * ```\n */\n subsetOf(allowed: readonly unknown[]): this {\n const v = this.resolved;\n const isArray = Array.isArray(v);\n const invalid = isArray\n ? v.filter((el) => !allowed.some((a) => deepEqual(el, a)))\n : [];\n return this.record(\n \"subsetOf\",\n isArray && invalid.length === 0,\n (neg) => {\n if (!isArray) return `should be an array of values from ${display(allowed)}`;\n if (neg) return `should contain values outside ${display(allowed)}`;\n return invalid.length\n ? `all items should be one of ${display(allowed)} (invalid: ${display(invalid)})`\n : `all items should be one of ${display(allowed)}`;\n },\n allowed,\n );\n }\n\n /** Alias of {@link subsetOf}. */\n only(allowed: readonly unknown[]): this {\n return this.subsetOf(allowed);\n }\n\n /**\n * Apply matchers to every element of an array value.\n *\n * ```ts\n * fm.key(\"tags\").each.oneOf([\"ok\", \"release\"]);\n * fm.key(\"tags\").each.matches(/^[a-z0-9-]+$/);\n * ```\n */\n get each(): EachAssertion {\n const negated = this.#negated;\n this.#negated = false;\n return new EachAssertion(this.recorder, this.key, this.resolved, negated);\n }\n\n /** Assert the value is one of `allowed` (enum). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n allowed.some((a) => deepEqual(this.resolved, a)),\n (neg) =>\n neg\n ? `should not be one of ${display(allowed)}`\n : `should be one of ${display(allowed)}`,\n allowed,\n );\n }\n\n /** Assert a string value matches the given regular expression. */\n matches(pattern: RegExp): this {\n const v = this.resolved;\n const pass = typeof v === \"string\" && pattern.test(v);\n return this.record(\n \"matches\",\n pass,\n (neg) =>\n neg\n ? `should not match ${pattern}`\n : `should match ${pattern}`,\n pattern.source,\n );\n }\n\n /**\n * Assert the value is a calendar date written as `YYYY-MM-DD`.\n *\n * fluorite keeps unquoted YAML dates as strings (see {@link parseFrontmatter}),\n * so this checks the written form exactly: a full timestamp\n * (`2026-06-07 10:30:00`), a loosely-padded date (`2026-6-7`), or an\n * impossible date (`2026-02-30`) all fail.\n *\n * ```ts\n * fm.key(\"date\").required().isoDate();\n * ```\n */\n isoDate(): this {\n const v = this.resolved;\n const pass = typeof v === \"string\" && isCalendarDate(v);\n return this.record(\n \"isoDate\",\n pass,\n (neg) =>\n neg\n ? `should not be a YYYY-MM-DD date`\n : `should be a YYYY-MM-DD date (was ${\n typeof v === \"string\" ? display(v) : valueType(v)\n })`,\n );\n }\n\n /** Assert an array contains `item`, or a string contains the substring. */\n has(item: unknown): this {\n const v = this.resolved;\n let pass = false;\n if (Array.isArray(v)) pass = v.some((el) => deepEqual(el, item));\n else if (typeof v === \"string\" && typeof item === \"string\")\n pass = v.includes(item);\n return this.record(\n \"has\",\n pass,\n (neg) =>\n neg ? `should not have ${display(item)}` : `should have ${display(item)}`,\n item,\n );\n }\n\n /** Assert an array/string contains all of `items`. */\n hasAll(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.every((item) => contains(v, item));\n return this.record(\n \"hasAll\",\n pass,\n (neg) =>\n neg\n ? `should not have all of ${display(items)}`\n : `should have all of ${display(items)}`,\n items,\n );\n }\n\n /** Assert an array/string contains at least one of `items`. */\n hasAny(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.some((item) => contains(v, item));\n return this.record(\n \"hasAny\",\n pass,\n (neg) =>\n neg\n ? `should not have any of ${display(items)}`\n : `should have any of ${display(items)}`,\n items,\n );\n }\n\n /** Assert the string/array length equals `n`. */\n length(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"length\",\n len === n,\n (neg) =>\n neg\n ? `length should not be ${n} (was ${len ?? \"n/a\"})`\n : `length should be ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at least `n`. */\n lengthMin(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMin\",\n len !== undefined && len >= n,\n (neg) =>\n neg\n ? `length should be < ${n} (was ${len ?? \"n/a\"})`\n : `length should be >= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at most `n`. */\n lengthMax(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMax\",\n len !== undefined && len <= n,\n (neg) =>\n neg\n ? `length should be > ${n} (was ${len ?? \"n/a\"})`\n : `length should be <= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n}\n\n/**\n * Applies matchers to every element of an array frontmatter value.\n *\n * Obtained via {@link KeyAssertion.each}. Each terminal matcher records a\n * single {@link RuleResult}: it passes only when the value is an array and\n * every element satisfies the matcher; failures list the offending elements.\n */\nexport class EachAssertion {\n #negated: boolean;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n negated: boolean,\n ) {\n this.#negated = negated;\n }\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n private record(\n rule: string,\n perElement: (el: unknown) => boolean,\n describe: (negated: boolean, invalid: unknown[], isArray: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const isArray = Array.isArray(this.value);\n const invalid = isArray\n ? (this.value as unknown[]).filter((el) => !perElement(el))\n : [];\n const rawPass = isArray && invalid.length === 0;\n // A non-array can never satisfy a per-element check, even when negated.\n const ok = isArray ? (negated ? !rawPass : rawPass) : false;\n this.recorder.push({\n key: this.key,\n rule: `each.${rule}`,\n ok,\n negated,\n message: describe(negated, invalid, isArray),\n value: this.value === MISSING ? undefined : this.value,\n expected,\n });\n return this;\n }\n\n /** Every element must be one of `allowed` (enum over array contents). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n (el) => allowed.some((a) => deepEqual(el, a)),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of values from ${display(allowed)}`\n : neg\n ? `every item should be outside ${display(allowed)}`\n : `every item should be one of ${display(allowed)}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n allowed,\n );\n }\n\n /** Every element must be of the given type. */\n type(expected: ValueType): this {\n return this.record(\n \"type\",\n (el) => valueType(el) === expected,\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of ${expected}`\n : neg\n ? `every item should not be of type ${expected}`\n : `every item should be of type ${expected}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n expected,\n );\n }\n\n /** Every (string) element must match the pattern. */\n matches(pattern: RegExp): this {\n return this.record(\n \"matches\",\n (el) => typeof el === \"string\" && pattern.test(el),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of strings matching ${pattern}`\n : neg\n ? `every item should not match ${pattern}`\n : `every item should match ${pattern}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n pattern.source,\n );\n }\n\n /** Every element must be a `YYYY-MM-DD` calendar date string. */\n isoDate(): this {\n return this.record(\n \"isoDate\",\n (el) => typeof el === \"string\" && isCalendarDate(el),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of YYYY-MM-DD dates`\n : neg\n ? `every item should not be a YYYY-MM-DD date`\n : `every item should be a YYYY-MM-DD date${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n );\n }\n}\n\n/**\n * True only for a string that is a real calendar date in `YYYY-MM-DD` form.\n *\n * The four-then-two-then-two digit shape is required (so `2026-6-7` fails), and\n * the round-trip through {@link Date.UTC} rejects impossible dates such as\n * `2026-02-30` or `2026-13-01` (which would otherwise roll over to a valid\n * day in the next month/year).\n */\nfunction isCalendarDate(value: string): boolean {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (!m) return false;\n const year = Number(m[1]);\n const month = Number(m[2]);\n const day = Number(m[3]);\n const dt = new Date(Date.UTC(year, month - 1, day));\n return (\n dt.getUTCFullYear() === year &&\n dt.getUTCMonth() === month - 1 &&\n dt.getUTCDate() === day\n );\n}\n\nfunction contains(container: unknown, item: unknown): boolean {\n if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));\n if (typeof container === \"string\" && typeof item === \"string\")\n return container.includes(item);\n return false;\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((el, i) => deepEqual(el, b[i]));\n }\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const ak = Object.keys(ao);\n const bk = Object.keys(bo);\n if (ak.length !== bk.length) return false;\n return ak.every((k) => deepEqual(ao[k], bo[k]));\n}\n","import { KeyAssertion } from \"./assertion.js\";\nimport type { RuleResult } from \"./types.js\";\n\n/**\n * The `fm` object passed to a rule set. Holds the parsed frontmatter data,\n * hands out {@link KeyAssertion} instances via {@link key}, and accumulates\n * every {@link RuleResult} the matchers record.\n */\nexport class Recorder {\n readonly results: RuleResult[] = [];\n\n constructor(readonly data: Record<string, unknown>) {}\n\n /** Begin a chain of assertions against the given frontmatter key. */\n key(name: string): KeyAssertion {\n const present = Object.prototype.hasOwnProperty.call(this.data, name);\n return new KeyAssertion(this, name, this.data[name], present);\n }\n\n /** Internal: append a recorded rule result. */\n push(result: RuleResult): void {\n this.results.push(result);\n }\n}\n","import { parseFrontmatter } from \"./parse.js\";\nimport { Recorder } from \"./recorder.js\";\nimport type { CheckResult, RulesFn, RuleResult } from \"./types.js\";\n\n/**\n * Run a rule set against the frontmatter of a Markdown source string.\n *\n * Never throws: malformed YAML or a missing frontmatter block is surfaced as a\n * failing {@link RuleResult} so results can always be collected and reported.\n *\n * ```ts\n * const result = check(markdown, (fm) => {\n * fm.key(\"title\").required().lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * });\n * if (!result.ok) console.error(result.failures);\n * ```\n */\nexport function check(source: string, rules: RulesFn): CheckResult {\n const parsed = parseFrontmatter(source);\n const recorder = new Recorder(parsed.data);\n\n if (parsed.error) {\n recorder.push(parseErrorResult(`invalid frontmatter: ${parsed.error}`));\n } else if (!parsed.hasFrontmatter) {\n recorder.push(parseErrorResult(\"no frontmatter block found\"));\n } else {\n rules(recorder);\n }\n\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return {\n ok: failures.length === 0,\n results,\n failures,\n data: parsed.data,\n };\n}\n\n/** Run a rule set against already-parsed frontmatter data. */\nexport function checkData(\n data: Record<string, unknown>,\n rules: RulesFn,\n): CheckResult {\n const recorder = new Recorder(data);\n rules(recorder);\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return { ok: failures.length === 0, results, failures, data };\n}\n\nfunction parseErrorResult(message: string): RuleResult {\n return {\n key: \"(frontmatter)\",\n rule: \"parse\",\n ok: false,\n negated: false,\n message,\n value: undefined,\n };\n}\n","import { pathToFileURL } from \"node:url\";\nimport { access } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { FluoriteConfig } from \"./types.js\";\n\n/**\n * Identity helper for authoring a config file with full type inference:\n *\n * ```ts\n * import { defineConfig } from \"fluorite\";\n * export default defineConfig({\n * include: [\"docs/**\\/*.md\"],\n * rules: (fm) => fm.key(\"title\").required(),\n * });\n * ```\n */\nexport function defineConfig(config: FluoriteConfig): FluoriteConfig {\n return config;\n}\n\nconst CONFIG_NAMES = [\n \"fluorite.config.js\",\n \"fluorite.config.mjs\",\n \"fluorite.config.cjs\",\n];\n\n/**\n * Locate a config file: an explicit path, or the first conventional name found\n * in `cwd`. Returns `undefined` when none is found.\n */\nexport async function resolveConfigPath(\n explicit: string | undefined,\n cwd = process.cwd(),\n): Promise<string | undefined> {\n if (explicit) return resolve(cwd, explicit);\n for (const name of CONFIG_NAMES) {\n const candidate = resolve(cwd, name);\n if (await fileExists(candidate)) return candidate;\n }\n return undefined;\n}\n\n/** Dynamically import a config file and return its default export. */\nexport async function loadConfig(path: string): Promise<FluoriteConfig> {\n const mod = await import(pathToFileURL(path).href);\n const config = (mod.default ?? mod) as FluoriteConfig;\n if (!config || typeof config.rules !== \"function\") {\n throw new Error(\n `Config at ${path} must export a default object with a \"rules\" function.`,\n );\n }\n return config;\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n","import pc from \"picocolors\";\nimport type { CheckResult } from \"./types.js\";\n\n/** A check result paired with the file it came from. */\nexport interface FileReport {\n file: string;\n result: CheckResult;\n}\n\nexport interface FormatOptions {\n /** Suppress per-file lines for passing files. */\n quiet?: boolean;\n}\n\nfunction displayValue(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/** Format file reports into a human-readable, colorized string. */\nexport function formatReports(\n reports: FileReport[],\n options: FormatOptions = {},\n): string {\n const lines: string[] = [];\n let passed = 0;\n let failed = 0;\n let ruleFailures = 0;\n\n for (const { file, result } of reports) {\n if (result.ok) {\n passed++;\n if (!options.quiet) lines.push(`${pc.green(\"✔\")} ${file}`);\n } else {\n failed++;\n lines.push(`${pc.red(\"✘\")} ${file}`);\n for (const failure of result.failures) {\n ruleFailures++;\n const where = pc.dim(`(value: ${displayValue(failure.value)})`);\n lines.push(\n ` ${pc.red(\"✘\")} ${pc.bold(failure.key)}: ${failure.message} ${where}`,\n );\n }\n }\n }\n\n const summary = [\n `${reports.length} files`,\n pc.green(`${passed} passed`),\n failed > 0 ? pc.red(`${failed} failed`) : `${failed} failed`,\n `${ruleFailures} rule failures`,\n ].join(\", \");\n\n lines.push(\"\");\n lines.push(summary);\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
package/dist/cli.cjs CHANGED
@@ -23,17 +23,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
- // src/cli.ts
26
+ // src/cli-main.ts
27
27
  var import_promises2 = require("fs/promises");
28
28
  var import_node_util = require("util");
29
29
  var import_tinyglobby = require("tinyglobby");
30
30
 
31
31
  // src/parse.ts
32
32
  var import_gray_matter = __toESM(require("gray-matter"), 1);
33
+ var import_js_yaml = __toESM(require("js-yaml"), 1);
34
+ var keepDatesAsStrings = (input) => import_js_yaml.default.load(input, { schema: import_js_yaml.default.CORE_SCHEMA }) ?? {};
33
35
  function parseFrontmatter(source) {
34
36
  const hasFrontmatter = /^?\s*---\r?\n/.test(source);
35
37
  try {
36
- const parsed = (0, import_gray_matter.default)(source);
38
+ const parsed = (0, import_gray_matter.default)(source, { engines: { yaml: keepDatesAsStrings } });
37
39
  const data = parsed.data ?? {};
38
40
  return {
39
41
  data,
@@ -55,6 +57,7 @@ var MISSING = /* @__PURE__ */ Symbol("missing");
55
57
  function valueType(value) {
56
58
  if (value === MISSING) return "undefined";
57
59
  if (value === null) return "null";
60
+ if (value instanceof Date) return "date";
58
61
  if (Array.isArray(value)) return "array";
59
62
  const t = typeof value;
60
63
  if (t === "string" || t === "number" || t === "boolean" || t === "object") {
@@ -211,6 +214,27 @@ var KeyAssertion = class {
211
214
  pattern.source
212
215
  );
213
216
  }
217
+ /**
218
+ * Assert the value is a calendar date written as `YYYY-MM-DD`.
219
+ *
220
+ * fluorite keeps unquoted YAML dates as strings (see {@link parseFrontmatter}),
221
+ * so this checks the written form exactly: a full timestamp
222
+ * (`2026-06-07 10:30:00`), a loosely-padded date (`2026-6-7`), or an
223
+ * impossible date (`2026-02-30`) all fail.
224
+ *
225
+ * ```ts
226
+ * fm.key("date").required().isoDate();
227
+ * ```
228
+ */
229
+ isoDate() {
230
+ const v = this.resolved;
231
+ const pass = typeof v === "string" && isCalendarDate(v);
232
+ return this.record(
233
+ "isoDate",
234
+ pass,
235
+ (neg) => neg ? `should not be a YYYY-MM-DD date` : `should be a YYYY-MM-DD date (was ${typeof v === "string" ? display(v) : valueType(v)})`
236
+ );
237
+ }
214
238
  /** Assert an array contains `item`, or a string contains the substring. */
215
239
  has(item) {
216
240
  const v = this.resolved;
@@ -339,7 +363,24 @@ var EachAssertion = class {
339
363
  pattern.source
340
364
  );
341
365
  }
366
+ /** Every element must be a `YYYY-MM-DD` calendar date string. */
367
+ isoDate() {
368
+ return this.record(
369
+ "isoDate",
370
+ (el) => typeof el === "string" && isCalendarDate(el),
371
+ (neg, invalid, isArray) => !isArray ? `should be an array of YYYY-MM-DD dates` : neg ? `every item should not be a YYYY-MM-DD date` : `every item should be a YYYY-MM-DD date${invalid.length ? ` (invalid: ${display(invalid)})` : ""}`
372
+ );
373
+ }
342
374
  };
375
+ function isCalendarDate(value) {
376
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
377
+ if (!m) return false;
378
+ const year = Number(m[1]);
379
+ const month = Number(m[2]);
380
+ const day = Number(m[3]);
381
+ const dt = new Date(Date.UTC(year, month - 1, day));
382
+ return dt.getUTCFullYear() === year && dt.getUTCMonth() === month - 1 && dt.getUTCDate() === day;
383
+ }
343
384
  function contains(container, item) {
344
385
  if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));
345
386
  if (typeof container === "string" && typeof item === "string")
@@ -489,7 +530,7 @@ function formatReports(reports, options = {}) {
489
530
  return lines.join("\n");
490
531
  }
491
532
 
492
- // src/cli.ts
533
+ // src/cli-main.ts
493
534
  var HELP = `fluorite \u2014 inspect and validate Markdown frontmatter
494
535
 
495
536
  Usage:
@@ -563,6 +604,8 @@ ${HELP}` : HELP
563
604
  process.stdout.write(formatReports(reports, { quiet: values.quiet }) + "\n");
564
605
  return reports.some((r) => !r.result.ok) ? 1 : 0;
565
606
  }
607
+
608
+ // src/cli.ts
566
609
  main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
567
610
  process.stderr.write(
568
611
  `fluorite: ${err instanceof Error ? err.stack ?? err.message : String(err)}
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/parse.ts","../src/assertion.ts","../src/recorder.ts","../src/check.ts","../src/config.ts","../src/report.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFile } from \"node:fs/promises\";\nimport { parseArgs } from \"node:util\";\nimport { glob } from \"tinyglobby\";\nimport { check } from \"./check.js\";\nimport { loadConfig, resolveConfigPath } from \"./config.js\";\nimport { formatReports, type FileReport } from \"./report.js\";\nimport type { FluoriteConfig } from \"./types.js\";\n\nconst HELP = `fluorite — inspect and validate Markdown frontmatter\n\nUsage:\n fluorite check [patterns...] [options]\n\nOptions:\n -c, --config <path> Path to a config file (default: fluorite.config.{js,mjs,cjs})\n -q, --quiet Only print files with failures\n -h, --help Show this help\n\nExamples:\n fluorite check \"docs/**/*.md\"\n fluorite check --config fluorite.config.mjs\n`;\n\nasync function main(argv: string[]): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\", short: \"c\" },\n quiet: { type: \"boolean\", short: \"q\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n\n if (values.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n const command = positionals[0];\n if (command !== \"check\") {\n process.stderr.write(\n command\n ? `Unknown command: ${command}\\n\\n${HELP}`\n : HELP,\n );\n return command ? 1 : 0;\n }\n\n const cliPatterns = positionals.slice(1);\n\n // Load config (required for the rule set).\n const configPath = await resolveConfigPath(values.config);\n if (!configPath) {\n process.stderr.write(\n \"No config file found. Create fluorite.config.mjs (export default defineConfig({ rules })) \" +\n \"or pass --config.\\n\",\n );\n return 1;\n }\n\n let config: FluoriteConfig;\n try {\n config = await loadConfig(configPath);\n } catch (err) {\n process.stderr.write(\n `Failed to load config: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n return 1;\n }\n\n const patterns =\n cliPatterns.length > 0\n ? cliPatterns\n : config.include ?? [\"**/*.md\"];\n\n const files = await glob(patterns, {\n ignore: config.exclude ?? [\"**/node_modules/**\"],\n });\n files.sort();\n\n if (files.length === 0) {\n process.stderr.write(`No files matched: ${patterns.join(\", \")}\\n`);\n return 1;\n }\n\n const reports: FileReport[] = [];\n for (const file of files) {\n const source = await readFile(file, \"utf8\");\n reports.push({ file, result: check(source, config.rules) });\n }\n\n process.stdout.write(formatReports(reports, { quiet: values.quiet }) + \"\\n\");\n\n return reports.some((r) => !r.result.ok) ? 1 : 0;\n}\n\nmain(process.argv.slice(2))\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(\n `fluorite: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`,\n );\n process.exit(1);\n });\n","import matter from \"gray-matter\";\n\n/** Outcome of parsing frontmatter out of a Markdown source string. */\nexport interface ParseResult {\n /** Parsed frontmatter data (empty object when none / on error). */\n data: Record<string, unknown>;\n /** The Markdown body following the frontmatter block. */\n content: string;\n /** Whether a frontmatter block was present at all. */\n hasFrontmatter: boolean;\n /** Parse error message, if the frontmatter (YAML) was malformed. */\n error?: string;\n}\n\n/**\n * Extract frontmatter from a Markdown source string.\n *\n * Never throws: malformed YAML is reported via the `error` field so callers\n * can record it as a failure rather than crash.\n */\nexport function parseFrontmatter(source: string): ParseResult {\n const hasFrontmatter = /^?\\s*---\\r?\\n/.test(source);\n try {\n const parsed = matter(source);\n const data = (parsed.data ?? {}) as Record<string, unknown>;\n return {\n data,\n content: parsed.content,\n hasFrontmatter,\n };\n } catch (err) {\n return {\n data: {},\n content: source,\n hasFrontmatter,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n","import type { Recorder } from \"./recorder.js\";\nimport type { RuleResult, ValueType } from \"./types.js\";\n\n/** Sentinel meaning \"the key was not present in the frontmatter\". */\nconst MISSING = Symbol(\"missing\");\n\nfunction valueType(value: unknown): ValueType | \"undefined\" {\n if (value === MISSING) return \"undefined\";\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n const t = typeof value;\n if (t === \"string\" || t === \"number\" || t === \"boolean\" || t === \"object\") {\n return t;\n }\n return \"undefined\";\n}\n\nfunction lengthOf(value: unknown): number | undefined {\n if (typeof value === \"string\" || Array.isArray(value)) return value.length;\n return undefined;\n}\n\nfunction display(value: unknown): string {\n if (value === MISSING) return \"undefined\";\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Fluent, chainable assertions for a single frontmatter key.\n *\n * Each terminal matcher records one {@link RuleResult} on the parent\n * {@link Recorder} and returns `this`, so matchers can be chained:\n *\n * ```ts\n * fm.key(\"title\").required().type(\"string\").lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * ```\n *\n * The `.not` modifier negates only the next matcher, then resets.\n */\nexport class KeyAssertion {\n #negated = false;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n private readonly present: boolean,\n ) {}\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n /** The raw value (resolved against the sentinel) used by matchers. */\n private get resolved(): unknown {\n return this.present ? this.value : MISSING;\n }\n\n /**\n * Record a result, applying the pending `.not` negation, then reset it.\n */\n private record(\n rule: string,\n rawPass: boolean,\n describe: (negated: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const ok = negated ? !rawPass : rawPass;\n const result: RuleResult = {\n key: this.key,\n rule,\n ok,\n negated,\n message: describe(negated),\n value: this.present ? this.value : undefined,\n expected,\n };\n this.recorder.push(result);\n return this;\n }\n\n /** Assert the key exists in the frontmatter. */\n required(): this {\n return this.record(\n \"required\",\n this.present,\n (neg) => (neg ? `should not exist` : `is required`),\n );\n }\n\n /** Alias of {@link required}. */\n exists(): this {\n return this.record(\n \"exists\",\n this.present,\n (neg) => (neg ? `should not exist` : `should exist`),\n );\n }\n\n /** Assert the value is of the given type. */\n type(expected: ValueType): this {\n const actual = valueType(this.resolved);\n return this.record(\n \"type\",\n actual === expected,\n (neg) =>\n neg\n ? `should not be of type ${expected}`\n : `should be of type ${expected} (was ${actual})`,\n expected,\n );\n }\n\n /** Assert the value strictly equals `expected` (deep for arrays/objects). */\n eq(expected: unknown): this {\n return this.record(\n \"eq\",\n deepEqual(this.resolved, expected),\n (neg) =>\n neg\n ? `should not equal ${display(expected)}`\n : `should equal ${display(expected)}`,\n expected,\n );\n }\n\n /**\n * Assert the value is an array whose every element is in `allowed` (enum).\n *\n * Designed for catching tag notation drift: define the canonical set once\n * and any stray / mistyped value is reported.\n *\n * ```ts\n * fm.key(\"tags\").subsetOf([\"ok\", \"release\", \"blog\"]);\n * ```\n */\n subsetOf(allowed: readonly unknown[]): this {\n const v = this.resolved;\n const isArray = Array.isArray(v);\n const invalid = isArray\n ? v.filter((el) => !allowed.some((a) => deepEqual(el, a)))\n : [];\n return this.record(\n \"subsetOf\",\n isArray && invalid.length === 0,\n (neg) => {\n if (!isArray) return `should be an array of values from ${display(allowed)}`;\n if (neg) return `should contain values outside ${display(allowed)}`;\n return invalid.length\n ? `all items should be one of ${display(allowed)} (invalid: ${display(invalid)})`\n : `all items should be one of ${display(allowed)}`;\n },\n allowed,\n );\n }\n\n /** Alias of {@link subsetOf}. */\n only(allowed: readonly unknown[]): this {\n return this.subsetOf(allowed);\n }\n\n /**\n * Apply matchers to every element of an array value.\n *\n * ```ts\n * fm.key(\"tags\").each.oneOf([\"ok\", \"release\"]);\n * fm.key(\"tags\").each.matches(/^[a-z0-9-]+$/);\n * ```\n */\n get each(): EachAssertion {\n const negated = this.#negated;\n this.#negated = false;\n return new EachAssertion(this.recorder, this.key, this.resolved, negated);\n }\n\n /** Assert the value is one of `allowed` (enum). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n allowed.some((a) => deepEqual(this.resolved, a)),\n (neg) =>\n neg\n ? `should not be one of ${display(allowed)}`\n : `should be one of ${display(allowed)}`,\n allowed,\n );\n }\n\n /** Assert a string value matches the given regular expression. */\n matches(pattern: RegExp): this {\n const v = this.resolved;\n const pass = typeof v === \"string\" && pattern.test(v);\n return this.record(\n \"matches\",\n pass,\n (neg) =>\n neg\n ? `should not match ${pattern}`\n : `should match ${pattern}`,\n pattern.source,\n );\n }\n\n /** Assert an array contains `item`, or a string contains the substring. */\n has(item: unknown): this {\n const v = this.resolved;\n let pass = false;\n if (Array.isArray(v)) pass = v.some((el) => deepEqual(el, item));\n else if (typeof v === \"string\" && typeof item === \"string\")\n pass = v.includes(item);\n return this.record(\n \"has\",\n pass,\n (neg) =>\n neg ? `should not have ${display(item)}` : `should have ${display(item)}`,\n item,\n );\n }\n\n /** Assert an array/string contains all of `items`. */\n hasAll(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.every((item) => contains(v, item));\n return this.record(\n \"hasAll\",\n pass,\n (neg) =>\n neg\n ? `should not have all of ${display(items)}`\n : `should have all of ${display(items)}`,\n items,\n );\n }\n\n /** Assert an array/string contains at least one of `items`. */\n hasAny(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.some((item) => contains(v, item));\n return this.record(\n \"hasAny\",\n pass,\n (neg) =>\n neg\n ? `should not have any of ${display(items)}`\n : `should have any of ${display(items)}`,\n items,\n );\n }\n\n /** Assert the string/array length equals `n`. */\n length(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"length\",\n len === n,\n (neg) =>\n neg\n ? `length should not be ${n} (was ${len ?? \"n/a\"})`\n : `length should be ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at least `n`. */\n lengthMin(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMin\",\n len !== undefined && len >= n,\n (neg) =>\n neg\n ? `length should be < ${n} (was ${len ?? \"n/a\"})`\n : `length should be >= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at most `n`. */\n lengthMax(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMax\",\n len !== undefined && len <= n,\n (neg) =>\n neg\n ? `length should be > ${n} (was ${len ?? \"n/a\"})`\n : `length should be <= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n}\n\n/**\n * Applies matchers to every element of an array frontmatter value.\n *\n * Obtained via {@link KeyAssertion.each}. Each terminal matcher records a\n * single {@link RuleResult}: it passes only when the value is an array and\n * every element satisfies the matcher; failures list the offending elements.\n */\nexport class EachAssertion {\n #negated: boolean;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n negated: boolean,\n ) {\n this.#negated = negated;\n }\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n private record(\n rule: string,\n perElement: (el: unknown) => boolean,\n describe: (negated: boolean, invalid: unknown[], isArray: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const isArray = Array.isArray(this.value);\n const invalid = isArray\n ? (this.value as unknown[]).filter((el) => !perElement(el))\n : [];\n const rawPass = isArray && invalid.length === 0;\n // A non-array can never satisfy a per-element check, even when negated.\n const ok = isArray ? (negated ? !rawPass : rawPass) : false;\n this.recorder.push({\n key: this.key,\n rule: `each.${rule}`,\n ok,\n negated,\n message: describe(negated, invalid, isArray),\n value: this.value === MISSING ? undefined : this.value,\n expected,\n });\n return this;\n }\n\n /** Every element must be one of `allowed` (enum over array contents). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n (el) => allowed.some((a) => deepEqual(el, a)),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of values from ${display(allowed)}`\n : neg\n ? `every item should be outside ${display(allowed)}`\n : `every item should be one of ${display(allowed)}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n allowed,\n );\n }\n\n /** Every element must be of the given type. */\n type(expected: ValueType): this {\n return this.record(\n \"type\",\n (el) => valueType(el) === expected,\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of ${expected}`\n : neg\n ? `every item should not be of type ${expected}`\n : `every item should be of type ${expected}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n expected,\n );\n }\n\n /** Every (string) element must match the pattern. */\n matches(pattern: RegExp): this {\n return this.record(\n \"matches\",\n (el) => typeof el === \"string\" && pattern.test(el),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of strings matching ${pattern}`\n : neg\n ? `every item should not match ${pattern}`\n : `every item should match ${pattern}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n pattern.source,\n );\n }\n}\n\nfunction contains(container: unknown, item: unknown): boolean {\n if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));\n if (typeof container === \"string\" && typeof item === \"string\")\n return container.includes(item);\n return false;\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((el, i) => deepEqual(el, b[i]));\n }\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const ak = Object.keys(ao);\n const bk = Object.keys(bo);\n if (ak.length !== bk.length) return false;\n return ak.every((k) => deepEqual(ao[k], bo[k]));\n}\n","import { KeyAssertion } from \"./assertion.js\";\nimport type { RuleResult } from \"./types.js\";\n\n/**\n * The `fm` object passed to a rule set. Holds the parsed frontmatter data,\n * hands out {@link KeyAssertion} instances via {@link key}, and accumulates\n * every {@link RuleResult} the matchers record.\n */\nexport class Recorder {\n readonly results: RuleResult[] = [];\n\n constructor(readonly data: Record<string, unknown>) {}\n\n /** Begin a chain of assertions against the given frontmatter key. */\n key(name: string): KeyAssertion {\n const present = Object.prototype.hasOwnProperty.call(this.data, name);\n return new KeyAssertion(this, name, this.data[name], present);\n }\n\n /** Internal: append a recorded rule result. */\n push(result: RuleResult): void {\n this.results.push(result);\n }\n}\n","import { parseFrontmatter } from \"./parse.js\";\nimport { Recorder } from \"./recorder.js\";\nimport type { CheckResult, RulesFn, RuleResult } from \"./types.js\";\n\n/**\n * Run a rule set against the frontmatter of a Markdown source string.\n *\n * Never throws: malformed YAML or a missing frontmatter block is surfaced as a\n * failing {@link RuleResult} so results can always be collected and reported.\n *\n * ```ts\n * const result = check(markdown, (fm) => {\n * fm.key(\"title\").required().lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * });\n * if (!result.ok) console.error(result.failures);\n * ```\n */\nexport function check(source: string, rules: RulesFn): CheckResult {\n const parsed = parseFrontmatter(source);\n const recorder = new Recorder(parsed.data);\n\n if (parsed.error) {\n recorder.push(parseErrorResult(`invalid frontmatter: ${parsed.error}`));\n } else if (!parsed.hasFrontmatter) {\n recorder.push(parseErrorResult(\"no frontmatter block found\"));\n } else {\n rules(recorder);\n }\n\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return {\n ok: failures.length === 0,\n results,\n failures,\n data: parsed.data,\n };\n}\n\n/** Run a rule set against already-parsed frontmatter data. */\nexport function checkData(\n data: Record<string, unknown>,\n rules: RulesFn,\n): CheckResult {\n const recorder = new Recorder(data);\n rules(recorder);\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return { ok: failures.length === 0, results, failures, data };\n}\n\nfunction parseErrorResult(message: string): RuleResult {\n return {\n key: \"(frontmatter)\",\n rule: \"parse\",\n ok: false,\n negated: false,\n message,\n value: undefined,\n };\n}\n","import { pathToFileURL } from \"node:url\";\nimport { access } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { FluoriteConfig } from \"./types.js\";\n\n/**\n * Identity helper for authoring a config file with full type inference:\n *\n * ```ts\n * import { defineConfig } from \"fluorite\";\n * export default defineConfig({\n * include: [\"docs/**\\/*.md\"],\n * rules: (fm) => fm.key(\"title\").required(),\n * });\n * ```\n */\nexport function defineConfig(config: FluoriteConfig): FluoriteConfig {\n return config;\n}\n\nconst CONFIG_NAMES = [\n \"fluorite.config.js\",\n \"fluorite.config.mjs\",\n \"fluorite.config.cjs\",\n];\n\n/**\n * Locate a config file: an explicit path, or the first conventional name found\n * in `cwd`. Returns `undefined` when none is found.\n */\nexport async function resolveConfigPath(\n explicit: string | undefined,\n cwd = process.cwd(),\n): Promise<string | undefined> {\n if (explicit) return resolve(cwd, explicit);\n for (const name of CONFIG_NAMES) {\n const candidate = resolve(cwd, name);\n if (await fileExists(candidate)) return candidate;\n }\n return undefined;\n}\n\n/** Dynamically import a config file and return its default export. */\nexport async function loadConfig(path: string): Promise<FluoriteConfig> {\n const mod = await import(pathToFileURL(path).href);\n const config = (mod.default ?? mod) as FluoriteConfig;\n if (!config || typeof config.rules !== \"function\") {\n throw new Error(\n `Config at ${path} must export a default object with a \"rules\" function.`,\n );\n }\n return config;\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n","import pc from \"picocolors\";\nimport type { CheckResult } from \"./types.js\";\n\n/** A check result paired with the file it came from. */\nexport interface FileReport {\n file: string;\n result: CheckResult;\n}\n\nexport interface FormatOptions {\n /** Suppress per-file lines for passing files. */\n quiet?: boolean;\n}\n\nfunction displayValue(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/** Format file reports into a human-readable, colorized string. */\nexport function formatReports(\n reports: FileReport[],\n options: FormatOptions = {},\n): string {\n const lines: string[] = [];\n let passed = 0;\n let failed = 0;\n let ruleFailures = 0;\n\n for (const { file, result } of reports) {\n if (result.ok) {\n passed++;\n if (!options.quiet) lines.push(`${pc.green(\"✔\")} ${file}`);\n } else {\n failed++;\n lines.push(`${pc.red(\"✘\")} ${file}`);\n for (const failure of result.failures) {\n ruleFailures++;\n const where = pc.dim(`(value: ${displayValue(failure.value)})`);\n lines.push(\n ` ${pc.red(\"✘\")} ${pc.bold(failure.key)}: ${failure.message} ${where}`,\n );\n }\n }\n }\n\n const summary = [\n `${reports.length} files`,\n pc.green(`${passed} passed`),\n failed > 0 ? pc.red(`${failed} failed`) : `${failed} failed`,\n `${ruleFailures} rule failures`,\n ].join(\", \");\n\n lines.push(\"\");\n lines.push(summary);\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["import_promises","matter","pc"]}
1
+ {"version":3,"sources":["../src/cli-main.ts","../src/parse.ts","../src/assertion.ts","../src/recorder.ts","../src/check.ts","../src/config.ts","../src/report.ts","../src/cli.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { parseArgs } from \"node:util\";\nimport { glob } from \"tinyglobby\";\nimport { check } from \"./check.js\";\nimport { loadConfig, resolveConfigPath } from \"./config.js\";\nimport { formatReports, type FileReport } from \"./report.js\";\nimport type { FluoriteConfig } from \"./types.js\";\n\nexport const HELP = `fluorite — inspect and validate Markdown frontmatter\n\nUsage:\n fluorite check [patterns...] [options]\n\nOptions:\n -c, --config <path> Path to a config file (default: fluorite.config.{js,mjs,cjs})\n -q, --quiet Only print files with failures\n -h, --help Show this help\n\nExamples:\n fluorite check \"docs/**/*.md\"\n fluorite check --config fluorite.config.mjs\n`;\n\n/**\n * Run the `fluorite` CLI. Returns the process exit code (`0` on success, `1`\n * on any file/rule failure or usage error) instead of calling `process.exit`,\n * so it can be driven and asserted on from tests.\n */\nexport async function main(argv: string[]): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\", short: \"c\" },\n quiet: { type: \"boolean\", short: \"q\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n\n if (values.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n const command = positionals[0];\n if (command !== \"check\") {\n process.stderr.write(\n command\n ? `Unknown command: ${command}\\n\\n${HELP}`\n : HELP,\n );\n return command ? 1 : 0;\n }\n\n const cliPatterns = positionals.slice(1);\n\n // Load config (required for the rule set).\n const configPath = await resolveConfigPath(values.config);\n if (!configPath) {\n process.stderr.write(\n \"No config file found. Create fluorite.config.mjs (export default defineConfig({ rules })) \" +\n \"or pass --config.\\n\",\n );\n return 1;\n }\n\n let config: FluoriteConfig;\n try {\n config = await loadConfig(configPath);\n } catch (err) {\n process.stderr.write(\n `Failed to load config: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n return 1;\n }\n\n const patterns =\n cliPatterns.length > 0\n ? cliPatterns\n : config.include ?? [\"**/*.md\"];\n\n const files = await glob(patterns, {\n ignore: config.exclude ?? [\"**/node_modules/**\"],\n });\n files.sort();\n\n if (files.length === 0) {\n process.stderr.write(`No files matched: ${patterns.join(\", \")}\\n`);\n return 1;\n }\n\n const reports: FileReport[] = [];\n for (const file of files) {\n const source = await readFile(file, \"utf8\");\n reports.push({ file, result: check(source, config.rules) });\n }\n\n process.stdout.write(formatReports(reports, { quiet: values.quiet }) + \"\\n\");\n\n return reports.some((r) => !r.result.ok) ? 1 : 0;\n}\n","import matter from \"gray-matter\";\nimport yaml from \"js-yaml\";\n\n/**\n * gray-matter's default YAML engine follows YAML 1.1 and coerces unquoted\n * dates (`date: 2026-06-07`) into JavaScript `Date` objects. That erases how\n * the value was written, so the format can no longer be linted — a clean\n * `YYYY-MM-DD`, a full timestamp, and an impossible date all collapse to a\n * `Date` (or get silently re-serialised).\n *\n * We swap in js-yaml's `CORE_SCHEMA`, which omits the `!!timestamp` type, so\n * every date stays a verbatim string. Matchers like `isoDate()` / `matches()`\n * can then validate the written `YYYY-MM-DD` form without requiring every\n * value to be quoted. Booleans, numbers and `null` are still parsed.\n */\nconst keepDatesAsStrings = (input: string): object =>\n (yaml.load(input, { schema: yaml.CORE_SCHEMA }) as object) ?? {};\n\n/** Outcome of parsing frontmatter out of a Markdown source string. */\nexport interface ParseResult {\n /** Parsed frontmatter data (empty object when none / on error). */\n data: Record<string, unknown>;\n /** The Markdown body following the frontmatter block. */\n content: string;\n /** Whether a frontmatter block was present at all. */\n hasFrontmatter: boolean;\n /** Parse error message, if the frontmatter (YAML) was malformed. */\n error?: string;\n}\n\n/**\n * Extract frontmatter from a Markdown source string.\n *\n * Never throws: malformed YAML is reported via the `error` field so callers\n * can record it as a failure rather than crash.\n */\nexport function parseFrontmatter(source: string): ParseResult {\n const hasFrontmatter = /^?\\s*---\\r?\\n/.test(source);\n try {\n const parsed = matter(source, { engines: { yaml: keepDatesAsStrings } });\n const data = (parsed.data ?? {}) as Record<string, unknown>;\n return {\n data,\n content: parsed.content,\n hasFrontmatter,\n };\n } catch (err) {\n return {\n data: {},\n content: source,\n hasFrontmatter,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n","import type { Recorder } from \"./recorder.js\";\nimport type { RuleResult, ValueType } from \"./types.js\";\n\n/** Sentinel meaning \"the key was not present in the frontmatter\". */\nconst MISSING = Symbol(\"missing\");\n\nfunction valueType(value: unknown): ValueType | \"undefined\" {\n if (value === MISSING) return \"undefined\";\n if (value === null) return \"null\";\n if (value instanceof Date) return \"date\";\n if (Array.isArray(value)) return \"array\";\n const t = typeof value;\n if (t === \"string\" || t === \"number\" || t === \"boolean\" || t === \"object\") {\n return t;\n }\n return \"undefined\";\n}\n\nfunction lengthOf(value: unknown): number | undefined {\n if (typeof value === \"string\" || Array.isArray(value)) return value.length;\n return undefined;\n}\n\nfunction display(value: unknown): string {\n if (value === MISSING) return \"undefined\";\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Fluent, chainable assertions for a single frontmatter key.\n *\n * Each terminal matcher records one {@link RuleResult} on the parent\n * {@link Recorder} and returns `this`, so matchers can be chained:\n *\n * ```ts\n * fm.key(\"title\").required().type(\"string\").lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * ```\n *\n * The `.not` modifier negates only the next matcher, then resets.\n */\nexport class KeyAssertion {\n #negated = false;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n private readonly present: boolean,\n ) {}\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n /** The raw value (resolved against the sentinel) used by matchers. */\n private get resolved(): unknown {\n return this.present ? this.value : MISSING;\n }\n\n /**\n * Record a result, applying the pending `.not` negation, then reset it.\n */\n private record(\n rule: string,\n rawPass: boolean,\n describe: (negated: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const ok = negated ? !rawPass : rawPass;\n const result: RuleResult = {\n key: this.key,\n rule,\n ok,\n negated,\n message: describe(negated),\n value: this.present ? this.value : undefined,\n expected,\n };\n this.recorder.push(result);\n return this;\n }\n\n /** Assert the key exists in the frontmatter. */\n required(): this {\n return this.record(\n \"required\",\n this.present,\n (neg) => (neg ? `should not exist` : `is required`),\n );\n }\n\n /** Alias of {@link required}. */\n exists(): this {\n return this.record(\n \"exists\",\n this.present,\n (neg) => (neg ? `should not exist` : `should exist`),\n );\n }\n\n /** Assert the value is of the given type. */\n type(expected: ValueType): this {\n const actual = valueType(this.resolved);\n return this.record(\n \"type\",\n actual === expected,\n (neg) =>\n neg\n ? `should not be of type ${expected}`\n : `should be of type ${expected} (was ${actual})`,\n expected,\n );\n }\n\n /** Assert the value strictly equals `expected` (deep for arrays/objects). */\n eq(expected: unknown): this {\n return this.record(\n \"eq\",\n deepEqual(this.resolved, expected),\n (neg) =>\n neg\n ? `should not equal ${display(expected)}`\n : `should equal ${display(expected)}`,\n expected,\n );\n }\n\n /**\n * Assert the value is an array whose every element is in `allowed` (enum).\n *\n * Designed for catching tag notation drift: define the canonical set once\n * and any stray / mistyped value is reported.\n *\n * ```ts\n * fm.key(\"tags\").subsetOf([\"ok\", \"release\", \"blog\"]);\n * ```\n */\n subsetOf(allowed: readonly unknown[]): this {\n const v = this.resolved;\n const isArray = Array.isArray(v);\n const invalid = isArray\n ? v.filter((el) => !allowed.some((a) => deepEqual(el, a)))\n : [];\n return this.record(\n \"subsetOf\",\n isArray && invalid.length === 0,\n (neg) => {\n if (!isArray) return `should be an array of values from ${display(allowed)}`;\n if (neg) return `should contain values outside ${display(allowed)}`;\n return invalid.length\n ? `all items should be one of ${display(allowed)} (invalid: ${display(invalid)})`\n : `all items should be one of ${display(allowed)}`;\n },\n allowed,\n );\n }\n\n /** Alias of {@link subsetOf}. */\n only(allowed: readonly unknown[]): this {\n return this.subsetOf(allowed);\n }\n\n /**\n * Apply matchers to every element of an array value.\n *\n * ```ts\n * fm.key(\"tags\").each.oneOf([\"ok\", \"release\"]);\n * fm.key(\"tags\").each.matches(/^[a-z0-9-]+$/);\n * ```\n */\n get each(): EachAssertion {\n const negated = this.#negated;\n this.#negated = false;\n return new EachAssertion(this.recorder, this.key, this.resolved, negated);\n }\n\n /** Assert the value is one of `allowed` (enum). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n allowed.some((a) => deepEqual(this.resolved, a)),\n (neg) =>\n neg\n ? `should not be one of ${display(allowed)}`\n : `should be one of ${display(allowed)}`,\n allowed,\n );\n }\n\n /** Assert a string value matches the given regular expression. */\n matches(pattern: RegExp): this {\n const v = this.resolved;\n const pass = typeof v === \"string\" && pattern.test(v);\n return this.record(\n \"matches\",\n pass,\n (neg) =>\n neg\n ? `should not match ${pattern}`\n : `should match ${pattern}`,\n pattern.source,\n );\n }\n\n /**\n * Assert the value is a calendar date written as `YYYY-MM-DD`.\n *\n * fluorite keeps unquoted YAML dates as strings (see {@link parseFrontmatter}),\n * so this checks the written form exactly: a full timestamp\n * (`2026-06-07 10:30:00`), a loosely-padded date (`2026-6-7`), or an\n * impossible date (`2026-02-30`) all fail.\n *\n * ```ts\n * fm.key(\"date\").required().isoDate();\n * ```\n */\n isoDate(): this {\n const v = this.resolved;\n const pass = typeof v === \"string\" && isCalendarDate(v);\n return this.record(\n \"isoDate\",\n pass,\n (neg) =>\n neg\n ? `should not be a YYYY-MM-DD date`\n : `should be a YYYY-MM-DD date (was ${\n typeof v === \"string\" ? display(v) : valueType(v)\n })`,\n );\n }\n\n /** Assert an array contains `item`, or a string contains the substring. */\n has(item: unknown): this {\n const v = this.resolved;\n let pass = false;\n if (Array.isArray(v)) pass = v.some((el) => deepEqual(el, item));\n else if (typeof v === \"string\" && typeof item === \"string\")\n pass = v.includes(item);\n return this.record(\n \"has\",\n pass,\n (neg) =>\n neg ? `should not have ${display(item)}` : `should have ${display(item)}`,\n item,\n );\n }\n\n /** Assert an array/string contains all of `items`. */\n hasAll(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.every((item) => contains(v, item));\n return this.record(\n \"hasAll\",\n pass,\n (neg) =>\n neg\n ? `should not have all of ${display(items)}`\n : `should have all of ${display(items)}`,\n items,\n );\n }\n\n /** Assert an array/string contains at least one of `items`. */\n hasAny(items: readonly unknown[]): this {\n const v = this.resolved;\n const pass = items.some((item) => contains(v, item));\n return this.record(\n \"hasAny\",\n pass,\n (neg) =>\n neg\n ? `should not have any of ${display(items)}`\n : `should have any of ${display(items)}`,\n items,\n );\n }\n\n /** Assert the string/array length equals `n`. */\n length(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"length\",\n len === n,\n (neg) =>\n neg\n ? `length should not be ${n} (was ${len ?? \"n/a\"})`\n : `length should be ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at least `n`. */\n lengthMin(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMin\",\n len !== undefined && len >= n,\n (neg) =>\n neg\n ? `length should be < ${n} (was ${len ?? \"n/a\"})`\n : `length should be >= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n\n /** Assert the string/array length is at most `n`. */\n lengthMax(n: number): this {\n const len = lengthOf(this.resolved);\n return this.record(\n \"lengthMax\",\n len !== undefined && len <= n,\n (neg) =>\n neg\n ? `length should be > ${n} (was ${len ?? \"n/a\"})`\n : `length should be <= ${n} (was ${len ?? \"n/a\"})`,\n n,\n );\n }\n}\n\n/**\n * Applies matchers to every element of an array frontmatter value.\n *\n * Obtained via {@link KeyAssertion.each}. Each terminal matcher records a\n * single {@link RuleResult}: it passes only when the value is an array and\n * every element satisfies the matcher; failures list the offending elements.\n */\nexport class EachAssertion {\n #negated: boolean;\n\n constructor(\n private readonly recorder: Recorder,\n private readonly key: string,\n private readonly value: unknown,\n negated: boolean,\n ) {\n this.#negated = negated;\n }\n\n /** Negate the next matcher in the chain. */\n get not(): this {\n this.#negated = true;\n return this;\n }\n\n private record(\n rule: string,\n perElement: (el: unknown) => boolean,\n describe: (negated: boolean, invalid: unknown[], isArray: boolean) => string,\n expected?: unknown,\n ): this {\n const negated = this.#negated;\n this.#negated = false;\n const isArray = Array.isArray(this.value);\n const invalid = isArray\n ? (this.value as unknown[]).filter((el) => !perElement(el))\n : [];\n const rawPass = isArray && invalid.length === 0;\n // A non-array can never satisfy a per-element check, even when negated.\n const ok = isArray ? (negated ? !rawPass : rawPass) : false;\n this.recorder.push({\n key: this.key,\n rule: `each.${rule}`,\n ok,\n negated,\n message: describe(negated, invalid, isArray),\n value: this.value === MISSING ? undefined : this.value,\n expected,\n });\n return this;\n }\n\n /** Every element must be one of `allowed` (enum over array contents). */\n oneOf(allowed: readonly unknown[]): this {\n return this.record(\n \"oneOf\",\n (el) => allowed.some((a) => deepEqual(el, a)),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of values from ${display(allowed)}`\n : neg\n ? `every item should be outside ${display(allowed)}`\n : `every item should be one of ${display(allowed)}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n allowed,\n );\n }\n\n /** Every element must be of the given type. */\n type(expected: ValueType): this {\n return this.record(\n \"type\",\n (el) => valueType(el) === expected,\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of ${expected}`\n : neg\n ? `every item should not be of type ${expected}`\n : `every item should be of type ${expected}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n expected,\n );\n }\n\n /** Every (string) element must match the pattern. */\n matches(pattern: RegExp): this {\n return this.record(\n \"matches\",\n (el) => typeof el === \"string\" && pattern.test(el),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of strings matching ${pattern}`\n : neg\n ? `every item should not match ${pattern}`\n : `every item should match ${pattern}${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n pattern.source,\n );\n }\n\n /** Every element must be a `YYYY-MM-DD` calendar date string. */\n isoDate(): this {\n return this.record(\n \"isoDate\",\n (el) => typeof el === \"string\" && isCalendarDate(el),\n (neg, invalid, isArray) =>\n !isArray\n ? `should be an array of YYYY-MM-DD dates`\n : neg\n ? `every item should not be a YYYY-MM-DD date`\n : `every item should be a YYYY-MM-DD date${invalid.length ? ` (invalid: ${display(invalid)})` : \"\"}`,\n );\n }\n}\n\n/**\n * True only for a string that is a real calendar date in `YYYY-MM-DD` form.\n *\n * The four-then-two-then-two digit shape is required (so `2026-6-7` fails), and\n * the round-trip through {@link Date.UTC} rejects impossible dates such as\n * `2026-02-30` or `2026-13-01` (which would otherwise roll over to a valid\n * day in the next month/year).\n */\nfunction isCalendarDate(value: string): boolean {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (!m) return false;\n const year = Number(m[1]);\n const month = Number(m[2]);\n const day = Number(m[3]);\n const dt = new Date(Date.UTC(year, month - 1, day));\n return (\n dt.getUTCFullYear() === year &&\n dt.getUTCMonth() === month - 1 &&\n dt.getUTCDate() === day\n );\n}\n\nfunction contains(container: unknown, item: unknown): boolean {\n if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));\n if (typeof container === \"string\" && typeof item === \"string\")\n return container.includes(item);\n return false;\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((el, i) => deepEqual(el, b[i]));\n }\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const ak = Object.keys(ao);\n const bk = Object.keys(bo);\n if (ak.length !== bk.length) return false;\n return ak.every((k) => deepEqual(ao[k], bo[k]));\n}\n","import { KeyAssertion } from \"./assertion.js\";\nimport type { RuleResult } from \"./types.js\";\n\n/**\n * The `fm` object passed to a rule set. Holds the parsed frontmatter data,\n * hands out {@link KeyAssertion} instances via {@link key}, and accumulates\n * every {@link RuleResult} the matchers record.\n */\nexport class Recorder {\n readonly results: RuleResult[] = [];\n\n constructor(readonly data: Record<string, unknown>) {}\n\n /** Begin a chain of assertions against the given frontmatter key. */\n key(name: string): KeyAssertion {\n const present = Object.prototype.hasOwnProperty.call(this.data, name);\n return new KeyAssertion(this, name, this.data[name], present);\n }\n\n /** Internal: append a recorded rule result. */\n push(result: RuleResult): void {\n this.results.push(result);\n }\n}\n","import { parseFrontmatter } from \"./parse.js\";\nimport { Recorder } from \"./recorder.js\";\nimport type { CheckResult, RulesFn, RuleResult } from \"./types.js\";\n\n/**\n * Run a rule set against the frontmatter of a Markdown source string.\n *\n * Never throws: malformed YAML or a missing frontmatter block is surfaced as a\n * failing {@link RuleResult} so results can always be collected and reported.\n *\n * ```ts\n * const result = check(markdown, (fm) => {\n * fm.key(\"title\").required().lengthMin(10);\n * fm.key(\"tags\").not.has(\"ng\");\n * });\n * if (!result.ok) console.error(result.failures);\n * ```\n */\nexport function check(source: string, rules: RulesFn): CheckResult {\n const parsed = parseFrontmatter(source);\n const recorder = new Recorder(parsed.data);\n\n if (parsed.error) {\n recorder.push(parseErrorResult(`invalid frontmatter: ${parsed.error}`));\n } else if (!parsed.hasFrontmatter) {\n recorder.push(parseErrorResult(\"no frontmatter block found\"));\n } else {\n rules(recorder);\n }\n\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return {\n ok: failures.length === 0,\n results,\n failures,\n data: parsed.data,\n };\n}\n\n/** Run a rule set against already-parsed frontmatter data. */\nexport function checkData(\n data: Record<string, unknown>,\n rules: RulesFn,\n): CheckResult {\n const recorder = new Recorder(data);\n rules(recorder);\n const results = recorder.results;\n const failures = results.filter((r) => !r.ok);\n return { ok: failures.length === 0, results, failures, data };\n}\n\nfunction parseErrorResult(message: string): RuleResult {\n return {\n key: \"(frontmatter)\",\n rule: \"parse\",\n ok: false,\n negated: false,\n message,\n value: undefined,\n };\n}\n","import { pathToFileURL } from \"node:url\";\nimport { access } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { FluoriteConfig } from \"./types.js\";\n\n/**\n * Identity helper for authoring a config file with full type inference:\n *\n * ```ts\n * import { defineConfig } from \"fluorite\";\n * export default defineConfig({\n * include: [\"docs/**\\/*.md\"],\n * rules: (fm) => fm.key(\"title\").required(),\n * });\n * ```\n */\nexport function defineConfig(config: FluoriteConfig): FluoriteConfig {\n return config;\n}\n\nconst CONFIG_NAMES = [\n \"fluorite.config.js\",\n \"fluorite.config.mjs\",\n \"fluorite.config.cjs\",\n];\n\n/**\n * Locate a config file: an explicit path, or the first conventional name found\n * in `cwd`. Returns `undefined` when none is found.\n */\nexport async function resolveConfigPath(\n explicit: string | undefined,\n cwd = process.cwd(),\n): Promise<string | undefined> {\n if (explicit) return resolve(cwd, explicit);\n for (const name of CONFIG_NAMES) {\n const candidate = resolve(cwd, name);\n if (await fileExists(candidate)) return candidate;\n }\n return undefined;\n}\n\n/** Dynamically import a config file and return its default export. */\nexport async function loadConfig(path: string): Promise<FluoriteConfig> {\n const mod = await import(pathToFileURL(path).href);\n const config = (mod.default ?? mod) as FluoriteConfig;\n if (!config || typeof config.rules !== \"function\") {\n throw new Error(\n `Config at ${path} must export a default object with a \"rules\" function.`,\n );\n }\n return config;\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n","import pc from \"picocolors\";\nimport type { CheckResult } from \"./types.js\";\n\n/** A check result paired with the file it came from. */\nexport interface FileReport {\n file: string;\n result: CheckResult;\n}\n\nexport interface FormatOptions {\n /** Suppress per-file lines for passing files. */\n quiet?: boolean;\n}\n\nfunction displayValue(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/** Format file reports into a human-readable, colorized string. */\nexport function formatReports(\n reports: FileReport[],\n options: FormatOptions = {},\n): string {\n const lines: string[] = [];\n let passed = 0;\n let failed = 0;\n let ruleFailures = 0;\n\n for (const { file, result } of reports) {\n if (result.ok) {\n passed++;\n if (!options.quiet) lines.push(`${pc.green(\"✔\")} ${file}`);\n } else {\n failed++;\n lines.push(`${pc.red(\"✘\")} ${file}`);\n for (const failure of result.failures) {\n ruleFailures++;\n const where = pc.dim(`(value: ${displayValue(failure.value)})`);\n lines.push(\n ` ${pc.red(\"✘\")} ${pc.bold(failure.key)}: ${failure.message} ${where}`,\n );\n }\n }\n }\n\n const summary = [\n `${reports.length} files`,\n pc.green(`${passed} passed`),\n failed > 0 ? pc.red(`${failed} failed`) : `${failed} failed`,\n `${ruleFailures} rule failures`,\n ].join(\", \");\n\n lines.push(\"\");\n lines.push(summary);\n return lines.join(\"\\n\");\n}\n","#!/usr/bin/env node\nimport { main } from \"./cli-main.js\";\n\nmain(process.argv.slice(2))\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(\n `fluorite: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`,\n );\n process.exit(1);\n });\n"],"mappings":"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","names":["import_promises","yaml","matter","pc"]}
package/dist/cli.js CHANGED
@@ -4,9 +4,9 @@ import {
4
4
  formatReports,
5
5
  loadConfig,
6
6
  resolveConfigPath
7
- } from "./chunk-2QW4LSG5.js";
7
+ } from "./chunk-PTAYLHQM.js";
8
8
 
9
- // src/cli.ts
9
+ // src/cli-main.ts
10
10
  import { readFile } from "fs/promises";
11
11
  import { parseArgs } from "util";
12
12
  import { glob } from "tinyglobby";
@@ -83,6 +83,8 @@ ${HELP}` : HELP
83
83
  process.stdout.write(formatReports(reports, { quiet: values.quiet }) + "\n");
84
84
  return reports.some((r) => !r.result.ok) ? 1 : 0;
85
85
  }
86
+
87
+ // src/cli.ts
86
88
  main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
87
89
  process.stderr.write(
88
90
  `fluorite: ${err instanceof Error ? err.stack ?? err.message : String(err)}
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFile } from \"node:fs/promises\";\nimport { parseArgs } from \"node:util\";\nimport { glob } from \"tinyglobby\";\nimport { check } from \"./check.js\";\nimport { loadConfig, resolveConfigPath } from \"./config.js\";\nimport { formatReports, type FileReport } from \"./report.js\";\nimport type { FluoriteConfig } from \"./types.js\";\n\nconst HELP = `fluorite — inspect and validate Markdown frontmatter\n\nUsage:\n fluorite check [patterns...] [options]\n\nOptions:\n -c, --config <path> Path to a config file (default: fluorite.config.{js,mjs,cjs})\n -q, --quiet Only print files with failures\n -h, --help Show this help\n\nExamples:\n fluorite check \"docs/**/*.md\"\n fluorite check --config fluorite.config.mjs\n`;\n\nasync function main(argv: string[]): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\", short: \"c\" },\n quiet: { type: \"boolean\", short: \"q\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n\n if (values.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n const command = positionals[0];\n if (command !== \"check\") {\n process.stderr.write(\n command\n ? `Unknown command: ${command}\\n\\n${HELP}`\n : HELP,\n );\n return command ? 1 : 0;\n }\n\n const cliPatterns = positionals.slice(1);\n\n // Load config (required for the rule set).\n const configPath = await resolveConfigPath(values.config);\n if (!configPath) {\n process.stderr.write(\n \"No config file found. Create fluorite.config.mjs (export default defineConfig({ rules })) \" +\n \"or pass --config.\\n\",\n );\n return 1;\n }\n\n let config: FluoriteConfig;\n try {\n config = await loadConfig(configPath);\n } catch (err) {\n process.stderr.write(\n `Failed to load config: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n return 1;\n }\n\n const patterns =\n cliPatterns.length > 0\n ? cliPatterns\n : config.include ?? [\"**/*.md\"];\n\n const files = await glob(patterns, {\n ignore: config.exclude ?? [\"**/node_modules/**\"],\n });\n files.sort();\n\n if (files.length === 0) {\n process.stderr.write(`No files matched: ${patterns.join(\", \")}\\n`);\n return 1;\n }\n\n const reports: FileReport[] = [];\n for (const file of files) {\n const source = await readFile(file, \"utf8\");\n reports.push({ file, result: check(source, config.rules) });\n }\n\n process.stdout.write(formatReports(reports, { quiet: values.quiet }) + \"\\n\");\n\n return reports.some((r) => !r.result.ok) ? 1 : 0;\n}\n\nmain(process.argv.slice(2))\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(\n `fluorite: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`,\n );\n process.exit(1);\n });\n"],"mappings":";;;;;;;;;AACA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AAMrB,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeb,eAAe,KAAK,MAAiC;AACnD,QAAM,EAAE,QAAQ,YAAY,IAAI,UAAU;AAAA,IACxC,MAAM;AAAA,IACN,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACP,QAAQ,EAAE,MAAM,UAAU,OAAO,IAAI;AAAA,MACrC,OAAO,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,MACrD,MAAM,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,IACtD;AAAA,EACF,CAAC;AAED,MAAI,OAAO,MAAM;AACf,YAAQ,OAAO,MAAM,IAAI;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,YAAY,CAAC;AAC7B,MAAI,YAAY,SAAS;AACvB,YAAQ,OAAO;AAAA,MACb,UACI,oBAAoB,OAAO;AAAA;AAAA,EAAO,IAAI,KACtC;AAAA,IACN;AACA,WAAO,UAAU,IAAI;AAAA,EACvB;AAEA,QAAM,cAAc,YAAY,MAAM,CAAC;AAGvC,QAAM,aAAa,MAAM,kBAAkB,OAAO,MAAM;AACxD,MAAI,CAAC,YAAY;AACf,YAAQ,OAAO;AAAA,MACb;AAAA,IAEF;AACA,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,WAAW,UAAU;AAAA,EACtC,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,0BAA0B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,IAC5E;AACA,WAAO;AAAA,EACT;AAEA,QAAM,WACJ,YAAY,SAAS,IACjB,cACA,OAAO,WAAW,CAAC,SAAS;AAElC,QAAM,QAAQ,MAAM,KAAK,UAAU;AAAA,IACjC,QAAQ,OAAO,WAAW,CAAC,oBAAoB;AAAA,EACjD,CAAC;AACD,QAAM,KAAK;AAEX,MAAI,MAAM,WAAW,GAAG;AACtB,YAAQ,OAAO,MAAM,qBAAqB,SAAS,KAAK,IAAI,CAAC;AAAA,CAAI;AACjE,WAAO;AAAA,EACT;AAEA,QAAM,UAAwB,CAAC;AAC/B,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,MAAM,SAAS,MAAM,MAAM;AAC1C,YAAQ,KAAK,EAAE,MAAM,QAAQ,MAAM,QAAQ,OAAO,KAAK,EAAE,CAAC;AAAA,EAC5D;AAEA,UAAQ,OAAO,MAAM,cAAc,SAAS,EAAE,OAAO,OAAO,MAAM,CAAC,IAAI,IAAI;AAE3E,SAAO,QAAQ,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,IAAI;AACjD;AAEA,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,EACvB,KAAK,CAAC,SAAS,QAAQ,KAAK,IAAI,CAAC,EACjC,MAAM,CAAC,QAAQ;AACd,UAAQ,OAAO;AAAA,IACb,aAAa,eAAe,QAAQ,IAAI,SAAS,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,EAC5E;AACA,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/cli-main.ts","../src/cli.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { parseArgs } from \"node:util\";\nimport { glob } from \"tinyglobby\";\nimport { check } from \"./check.js\";\nimport { loadConfig, resolveConfigPath } from \"./config.js\";\nimport { formatReports, type FileReport } from \"./report.js\";\nimport type { FluoriteConfig } from \"./types.js\";\n\nexport const HELP = `fluorite — inspect and validate Markdown frontmatter\n\nUsage:\n fluorite check [patterns...] [options]\n\nOptions:\n -c, --config <path> Path to a config file (default: fluorite.config.{js,mjs,cjs})\n -q, --quiet Only print files with failures\n -h, --help Show this help\n\nExamples:\n fluorite check \"docs/**/*.md\"\n fluorite check --config fluorite.config.mjs\n`;\n\n/**\n * Run the `fluorite` CLI. Returns the process exit code (`0` on success, `1`\n * on any file/rule failure or usage error) instead of calling `process.exit`,\n * so it can be driven and asserted on from tests.\n */\nexport async function main(argv: string[]): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\", short: \"c\" },\n quiet: { type: \"boolean\", short: \"q\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n\n if (values.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n const command = positionals[0];\n if (command !== \"check\") {\n process.stderr.write(\n command\n ? `Unknown command: ${command}\\n\\n${HELP}`\n : HELP,\n );\n return command ? 1 : 0;\n }\n\n const cliPatterns = positionals.slice(1);\n\n // Load config (required for the rule set).\n const configPath = await resolveConfigPath(values.config);\n if (!configPath) {\n process.stderr.write(\n \"No config file found. Create fluorite.config.mjs (export default defineConfig({ rules })) \" +\n \"or pass --config.\\n\",\n );\n return 1;\n }\n\n let config: FluoriteConfig;\n try {\n config = await loadConfig(configPath);\n } catch (err) {\n process.stderr.write(\n `Failed to load config: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n return 1;\n }\n\n const patterns =\n cliPatterns.length > 0\n ? cliPatterns\n : config.include ?? [\"**/*.md\"];\n\n const files = await glob(patterns, {\n ignore: config.exclude ?? [\"**/node_modules/**\"],\n });\n files.sort();\n\n if (files.length === 0) {\n process.stderr.write(`No files matched: ${patterns.join(\", \")}\\n`);\n return 1;\n }\n\n const reports: FileReport[] = [];\n for (const file of files) {\n const source = await readFile(file, \"utf8\");\n reports.push({ file, result: check(source, config.rules) });\n }\n\n process.stdout.write(formatReports(reports, { quiet: values.quiet }) + \"\\n\");\n\n return reports.some((r) => !r.result.ok) ? 1 : 0;\n}\n","#!/usr/bin/env node\nimport { main } from \"./cli-main.js\";\n\nmain(process.argv.slice(2))\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(\n `fluorite: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`,\n );\n process.exit(1);\n });\n"],"mappings":";;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AAMd,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBpB,eAAsB,KAAK,MAAiC;AAC1D,QAAM,EAAE,QAAQ,YAAY,IAAI,UAAU;AAAA,IACxC,MAAM;AAAA,IACN,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACP,QAAQ,EAAE,MAAM,UAAU,OAAO,IAAI;AAAA,MACrC,OAAO,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,MACrD,MAAM,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,IACtD;AAAA,EACF,CAAC;AAED,MAAI,OAAO,MAAM;AACf,YAAQ,OAAO,MAAM,IAAI;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,YAAY,CAAC;AAC7B,MAAI,YAAY,SAAS;AACvB,YAAQ,OAAO;AAAA,MACb,UACI,oBAAoB,OAAO;AAAA;AAAA,EAAO,IAAI,KACtC;AAAA,IACN;AACA,WAAO,UAAU,IAAI;AAAA,EACvB;AAEA,QAAM,cAAc,YAAY,MAAM,CAAC;AAGvC,QAAM,aAAa,MAAM,kBAAkB,OAAO,MAAM;AACxD,MAAI,CAAC,YAAY;AACf,YAAQ,OAAO;AAAA,MACb;AAAA,IAEF;AACA,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,WAAW,UAAU;AAAA,EACtC,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,0BAA0B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,IAC5E;AACA,WAAO;AAAA,EACT;AAEA,QAAM,WACJ,YAAY,SAAS,IACjB,cACA,OAAO,WAAW,CAAC,SAAS;AAElC,QAAM,QAAQ,MAAM,KAAK,UAAU;AAAA,IACjC,QAAQ,OAAO,WAAW,CAAC,oBAAoB;AAAA,EACjD,CAAC;AACD,QAAM,KAAK;AAEX,MAAI,MAAM,WAAW,GAAG;AACtB,YAAQ,OAAO,MAAM,qBAAqB,SAAS,KAAK,IAAI,CAAC;AAAA,CAAI;AACjE,WAAO;AAAA,EACT;AAEA,QAAM,UAAwB,CAAC;AAC/B,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,MAAM,SAAS,MAAM,MAAM;AAC1C,YAAQ,KAAK,EAAE,MAAM,QAAQ,MAAM,QAAQ,OAAO,KAAK,EAAE,CAAC;AAAA,EAC5D;AAEA,UAAQ,OAAO,MAAM,cAAc,SAAS,EAAE,OAAO,OAAO,MAAM,CAAC,IAAI,IAAI;AAE3E,SAAO,QAAQ,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,IAAI;AACjD;;;ACjGA,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,EACvB,KAAK,CAAC,SAAS,QAAQ,KAAK,IAAI,CAAC,EACjC,MAAM,CAAC,QAAQ;AACd,UAAQ,OAAO;AAAA,IACb,aAAa,eAAe,QAAQ,IAAI,SAAS,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,EAC5E;AACA,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
package/dist/index.cjs CHANGED
@@ -45,10 +45,12 @@ module.exports = __toCommonJS(src_exports);
45
45
 
46
46
  // src/parse.ts
47
47
  var import_gray_matter = __toESM(require("gray-matter"), 1);
48
+ var import_js_yaml = __toESM(require("js-yaml"), 1);
49
+ var keepDatesAsStrings = (input) => import_js_yaml.default.load(input, { schema: import_js_yaml.default.CORE_SCHEMA }) ?? {};
48
50
  function parseFrontmatter(source) {
49
51
  const hasFrontmatter = /^?\s*---\r?\n/.test(source);
50
52
  try {
51
- const parsed = (0, import_gray_matter.default)(source);
53
+ const parsed = (0, import_gray_matter.default)(source, { engines: { yaml: keepDatesAsStrings } });
52
54
  const data = parsed.data ?? {};
53
55
  return {
54
56
  data,
@@ -70,6 +72,7 @@ var MISSING = /* @__PURE__ */ Symbol("missing");
70
72
  function valueType(value) {
71
73
  if (value === MISSING) return "undefined";
72
74
  if (value === null) return "null";
75
+ if (value instanceof Date) return "date";
73
76
  if (Array.isArray(value)) return "array";
74
77
  const t = typeof value;
75
78
  if (t === "string" || t === "number" || t === "boolean" || t === "object") {
@@ -226,6 +229,27 @@ var KeyAssertion = class {
226
229
  pattern.source
227
230
  );
228
231
  }
232
+ /**
233
+ * Assert the value is a calendar date written as `YYYY-MM-DD`.
234
+ *
235
+ * fluorite keeps unquoted YAML dates as strings (see {@link parseFrontmatter}),
236
+ * so this checks the written form exactly: a full timestamp
237
+ * (`2026-06-07 10:30:00`), a loosely-padded date (`2026-6-7`), or an
238
+ * impossible date (`2026-02-30`) all fail.
239
+ *
240
+ * ```ts
241
+ * fm.key("date").required().isoDate();
242
+ * ```
243
+ */
244
+ isoDate() {
245
+ const v = this.resolved;
246
+ const pass = typeof v === "string" && isCalendarDate(v);
247
+ return this.record(
248
+ "isoDate",
249
+ pass,
250
+ (neg) => neg ? `should not be a YYYY-MM-DD date` : `should be a YYYY-MM-DD date (was ${typeof v === "string" ? display(v) : valueType(v)})`
251
+ );
252
+ }
229
253
  /** Assert an array contains `item`, or a string contains the substring. */
230
254
  has(item) {
231
255
  const v = this.resolved;
@@ -354,7 +378,24 @@ var EachAssertion = class {
354
378
  pattern.source
355
379
  );
356
380
  }
381
+ /** Every element must be a `YYYY-MM-DD` calendar date string. */
382
+ isoDate() {
383
+ return this.record(
384
+ "isoDate",
385
+ (el) => typeof el === "string" && isCalendarDate(el),
386
+ (neg, invalid, isArray) => !isArray ? `should be an array of YYYY-MM-DD dates` : neg ? `every item should not be a YYYY-MM-DD date` : `every item should be a YYYY-MM-DD date${invalid.length ? ` (invalid: ${display(invalid)})` : ""}`
387
+ );
388
+ }
357
389
  };
390
+ function isCalendarDate(value) {
391
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
392
+ if (!m) return false;
393
+ const year = Number(m[1]);
394
+ const month = Number(m[2]);
395
+ const day = Number(m[3]);
396
+ const dt = new Date(Date.UTC(year, month - 1, day));
397
+ return dt.getUTCFullYear() === year && dt.getUTCMonth() === month - 1 && dt.getUTCDate() === day;
398
+ }
358
399
  function contains(container, item) {
359
400
  if (Array.isArray(container)) return container.some((el) => deepEqual(el, item));
360
401
  if (typeof container === "string" && typeof item === "string")