@descent-vtt/spec-graph 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +430 -5
- package/README.md +137 -7
- package/dist/baseline.d.ts +18 -4
- package/dist/baseline.js +14 -2
- package/dist/baseline.js.map +1 -1
- package/dist/cli.d.ts +12 -4
- package/dist/cli.js +267 -59
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +45 -2
- package/dist/config.js +61 -2
- package/dist/config.js.map +1 -1
- package/dist/extract.d.ts +20 -11
- package/dist/extract.js +58 -44
- package/dist/extract.js.map +1 -1
- package/dist/identity.d.ts +10 -0
- package/dist/identity.js +37 -0
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/project-rules.d.ts +92 -0
- package/dist/project-rules.js +201 -0
- package/dist/project-rules.js.map +1 -0
- package/dist/regex.d.ts +64 -0
- package/dist/regex.js +708 -0
- package/dist/regex.js.map +1 -0
- package/dist/report.d.ts +34 -7
- package/dist/report.js +172 -5
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +8 -1
- package/dist/resolve.js +154 -30
- package/dist/resolve.js.map +1 -1
- package/dist/rules.d.ts +21 -5
- package/dist/rules.js +92 -5
- package/dist/rules.js.map +1 -1
- package/dist/runner.d.ts +5 -2
- package/dist/runner.js +1 -0
- package/dist/runner.js.map +1 -1
- package/dist/select.d.ts +19 -0
- package/dist/select.js +64 -14
- package/dist/select.js.map +1 -1
- package/dist/types.d.ts +36 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/identity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Document identity and reference normalisation.\n *\n * Foreign-key validation across a corpus of Markdown only works if `ADR-7`,\n * `ADR-0007`, `adr 7`, `[[0007-sharding]]` and `../adr/0007-sharding.md` are\n * understood to name the same document. Getting that wrong in either direction\n * is fatal: too strict and every real repository drowns in false \"broken\n * reference\" findings, too loose and `RFC 7` silently resolves to `ADR-7`.\n *\n * The resolution here is deliberately asymmetric. **Textual aliases** (`adr-7`)\n * are global, because a prefix already names the family. **Bare numbers** (`7`,\n * `#0007`) are resolved only within the citing document's own family, because a\n * repository with both `adr/0007` and `rfc/0007` is normal and guessing between\n * them would be worse than reporting nothing.\n */\n\n/** Directory names that name a specification family, mapped to a canonical prefix. */\nconst FAMILY_DIRECTORIES: Readonly<Record<string, string>> = {\n adr: 'ADR',\n adrs: 'ADR',\n 'architecture-decisions': 'ADR',\n 'decision-records': 'ADR',\n decisions: 'ADR',\n rfc: 'RFC',\n rfcs: 'RFC',\n kep: 'KEP',\n keps: 'KEP',\n enhancements: 'KEP',\n proposal: 'PROPOSAL',\n proposals: 'PROPOSAL',\n design: 'DESIGN',\n designs: 'DESIGN',\n prd: 'PRD',\n prds: 'PRD',\n spec: 'SPEC',\n specs: 'SPEC',\n};\n\n/** Front-matter keys that may carry an explicit identifier, in preference order. */\nexport const ID_KEYS: readonly string[] = ['id', 'adr', 'adr-id', 'rfc', 'rfc-id', 'kep-number', 'number', 'slug'];\n\n/** `ADR-0007`, `KEP 1234`, `rfc2119`: a family prefix followed by a number. */\nconst PREFIXED_ID = /^([A-Za-z][A-Za-z0-9_]{0,15}?)[\\s._-]*(\\d{1,6})$/;\n/** A bare number, optionally hash-prefixed: `7`, `#0007`. */\nconst BARE_NUMBER = /^#?(\\d{1,6})$/;\n\nexport interface DocumentIdentity {\n /** The canonical id: `ADR-0007`, or a path when nothing better exists. */\n readonly id: string;\n /** Family prefix, upper-cased. `null` when the document is not numbered. */\n readonly family: string | null;\n /** Sequence number without leading zeros. `null` when not numbered. */\n readonly number: number | null;\n /** Every lower-cased spelling that should resolve to this document. */\n readonly aliases: readonly string[];\n}\n\nexport interface IdentityInput {\n /** Repository-relative POSIX path. */\n readonly path: string;\n /** An id declared by a directive or front matter, if any. */\n readonly declaredId: string | null;\n /** Extra aliases declared by a directive or front matter. */\n readonly declaredAliases: readonly string[];\n /** The document H1, if any. */\n readonly heading: string | null;\n /**\n * Register the file path and stem as aliases. Default true.\n *\n * A specification that is a *region* of a file must not claim the file's\n * path: the file already answers to it, and two nodes answering to one path\n * would make every link to that file ambiguous.\n */\n readonly includePathAliases?: boolean | undefined;\n}\n\n/** An id has to be a name. Punctuation alone is what a bad parse leaves behind. */\nconst HAS_LETTER_OR_DIGIT = /[\\p{L}\\p{N}]/u;\n\n/**\n * Derives a document's canonical id and every alias that should reach it.\n *\n * Evidence is used in order of how deliberate it is: an explicit declaration,\n * then the file name, then the H1. Whatever loses still contributes aliases, so\n * a reference written in any of those forms still resolves.\n */\nexport function identify(input: IdentityInput): DocumentIdentity {\n // A declaration carrying no letter or digit is discarded rather than honoured.\n // `<!-- @spec-node id=\\\"ADR-9\\\" -->`, written inside a JavaScript string in a\n // worked example, parses its bare value as a lone backslash - and a document\n // whose id is `\\` collides with every other one that made the same mistake,\n // exports as invalid Mermaid, and names itself in findings a reader cannot act\n // on. There is nothing to gain by taking such a value seriously.\n const declaredId = input.declaredId !== null && HAS_LETTER_OR_DIGIT.test(input.declaredId) ? input.declaredId : null;\n const stem = fileStem(input.path);\n const directoryFamily = familyFromPath(input.path);\n const aliases = new Set<string>();\n\n const add = (value: string | null | undefined): void => {\n if (!value) return;\n const key = normaliseRef(value);\n if (key.length > 0) aliases.add(key);\n };\n\n const candidates: { family: string | null; number: number | null; label: string }[] = [];\n\n const consider = (raw: string | null, allowBare: boolean): void => {\n if (!raw) return;\n const cleaned = raw.trim();\n if (cleaned.length === 0) return;\n const prefixed = PREFIXED_ID.exec(cleaned);\n if (prefixed) {\n candidates.push({\n family: (prefixed[1] as string).toUpperCase(),\n number: Number.parseInt(prefixed[2] as string, 10),\n label: cleaned,\n });\n return;\n }\n const bare = BARE_NUMBER.exec(cleaned);\n if (bare && allowBare) {\n candidates.push({\n family: directoryFamily,\n number: Number.parseInt(bare[1] as string, 10),\n label: cleaned,\n });\n return;\n }\n candidates.push({ family: null, number: null, label: cleaned });\n };\n\n consider(declaredId, true);\n // `0007-sharding-the-write-path.md` and `kep-1234-foo.md` both start with the\n // identifier; take the leading token rather than the whole stem.\n consider(leadingToken(stem), true);\n consider(headingId(input.heading), false);\n\n const numbered = candidates.find((c) => c.number !== null);\n const family = numbered?.family ?? directoryFamily;\n const number = numbered?.number ?? null;\n\n // Witnesses to the local zero-padding convention, most authoritative first.\n // The spelling that actually produced the number comes first: an id read from\n // the H1 of `sharding.md` must still render as ADR-0007, not ADR-7.\n const witnesses = [numbered?.label, declaredId, stem];\n\n let id: string;\n if (number !== null && family !== null) {\n id = `${family}-${padNumber(number, witnesses)}`;\n } else if (declaredId) {\n id = declaredId.trim();\n } else if (number !== null) {\n // Numbered but with no family to prefix - a Rust RFC in `text/0001-foo.md`,\n // say. The file stem is a far better identity than a bare \"1\": it is what\n // people actually type, and it stays unique across the repository.\n id = stem.length > 0 ? stem : String(number);\n } else {\n id = stem.length > 0 ? stem : input.path;\n }\n\n add(id);\n if (input.includePathAliases !== false) {\n add(stem);\n add(input.path);\n add(stripExtension(input.path));\n }\n add(declaredId);\n for (const alias of input.declaredAliases) add(alias);\n if (input.heading) add(headingId(input.heading));\n\n if (number !== null) {\n const spellings = numberSpellings(number, witnesses);\n if (family !== null) {\n // Every spelling a human might type. `normaliseRef` folds separators, so\n // `adr-7`, `adr 7` and `adr7` collapse to one key; the padding variants do\n // not, and each needs registering.\n for (const digits of spellings) add(`${family}-${digits}`);\n } else {\n // With no family there is only one number space in the repository, so a\n // bare `0001` is unambiguous and worth resolving.\n for (const digits of spellings) add(digits);\n }\n }\n\n return { id, family, number, aliases: [...aliases] };\n}\n\n/**\n * Folds a reference to its lookup key.\n *\n * Case, separators and surrounding punctuation carry no meaning in a citation,\n * so `ADR-0007`, `adr 0007` and `Adr_0007` all become `adr0007`.\n */\nexport function normaliseRef(value: string): string {\n return value\n .trim()\n .toLowerCase()\n .replace(/^[#<([]+|[)\\]>.,;:]+$/g, '')\n .replace(/[\\s._-]+/g, '')\n .trim();\n}\n\n/** Splits a `target#anchor` reference. */\nexport function splitAnchor(target: string): { target: string; anchor: string | null } {\n const hash = target.indexOf('#');\n if (hash === -1) return { target, anchor: null };\n // A leading `#` is an in-document anchor, not a separator.\n if (hash === 0) return { target: '', anchor: target.slice(1) };\n return { target: target.slice(0, hash), anchor: target.slice(hash + 1) };\n}\n\n/** Parses `ADR-0007` into its family and number, or returns `null`. */\nexport function parsePrefixedRef(value: string): { family: string; number: number } | null {\n const match = PREFIXED_ID.exec(value.trim().replace(/^[#([]+/, ''));\n if (!match) return null;\n return { family: (match[1] as string).toUpperCase(), number: Number.parseInt(match[2] as string, 10) };\n}\n\n/** Parses a bare `7` or `#0007`, or returns `null`. */\nexport function parseBareRef(value: string): number | null {\n const match = BARE_NUMBER.exec(value.trim());\n return match ? Number.parseInt(match[1] as string, 10) : null;\n}\n\n/** True when a reference target names a location rather than an identifier. */\nexport function looksLikePath(target: string): boolean {\n return target.includes('/') || DOCUMENT_EXTENSION.test(target);\n}\n\nconst DOCUMENT_EXTENSION = /\\.(md|markdown|mdx|txt|rst|adoc|asciidoc)$/i;\n\n/**\n * True when a path-like target could name a specification.\n *\n * A specification linking to `../../src/rules.ts` is citing source code, which\n * is a different tool's job. Treating it as a dangling specification reference\n * would report an error on one of the most ordinary things a design document\n * does, so targets carrying a non-document extension are not references at all.\n */\nexport function isDocumentTarget(target: string): boolean {\n const withoutAnchor = splitAnchor(target).target;\n const base = withoutAnchor.slice(withoutAnchor.lastIndexOf('/') + 1);\n const dot = base.lastIndexOf('.');\n // No extension: a directory, or an extensionless spec such as `rfcs/0007`.\n if (dot <= 0) return true;\n const extension = base.slice(dot);\n // Only a plausible file extension disqualifies a target. An identifier like\n // `v1.2` or `ADR-0007.1` keeps its dot and is still resolved as an id.\n if (!/^\\.[A-Za-z][A-Za-z0-9]{0,5}$/.test(extension)) return true;\n return DOCUMENT_EXTENSION.test(base);\n}\n\n/** True when a reference target points outside the repository. */\nexport function isExternal(target: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(target) && !/^[a-z]:[\\\\/]/i.test(target);\n}\n\n/** The family prefix implied by a document's directory, if any. */\nexport function familyFromPath(posixPath: string): string | null {\n const segments = posixPath.toLowerCase().split('/');\n // Nearest enclosing directory wins: `docs/rfcs/adr/0007.md` is an ADR.\n for (let i = segments.length - 2; i >= 0; i -= 1) {\n const found = FAMILY_DIRECTORIES[segments[i] as string];\n if (found) return found;\n }\n return null;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Internals */\n/* -------------------------------------------------------------------------- */\n\nfunction fileStem(posixPath: string): string {\n const base = posixPath.slice(posixPath.lastIndexOf('/') + 1);\n const dot = base.lastIndexOf('.');\n const stem = dot > 0 ? base.slice(0, dot) : base;\n // `docs/adr/0007-sharding/README.md` is identified by its directory - but a\n // README at the repository root has no directory to be named after, and must\n // keep its own name.\n const slash = posixPath.lastIndexOf('/');\n if (slash > 0 && /^(readme|index)$/i.test(stem)) {\n const parent = posixPath.slice(0, slash);\n return parent.slice(parent.lastIndexOf('/') + 1);\n }\n return stem;\n}\n\nfunction stripExtension(posixPath: string): string {\n const dot = posixPath.lastIndexOf('.');\n const slash = posixPath.lastIndexOf('/');\n return dot > slash ? posixPath.slice(0, dot) : posixPath;\n}\n\n/** `kep-1234-foo` -> `kep-1234`; `0007-sharding` -> `0007`; `sharding` -> `sharding`. */\nfunction leadingToken(stem: string): string {\n const prefixed = /^([A-Za-z]{1,15}[\\s._-]?\\d{1,6})(?:[\\s._-]|$)/.exec(stem);\n if (prefixed) return prefixed[1] as string;\n const bare = /^(\\d{1,6})(?:[\\s._-]|$)/.exec(stem);\n if (bare) return bare[1] as string;\n return stem;\n}\n\n/** `# ADR-0007: Sharding the write path` -> `ADR-0007`. */\nfunction headingId(heading: string | null): string | null {\n if (!heading) return null;\n const match = /^\\s*([A-Za-z]{1,15}[\\s._-]?\\d{1,6})\\b/.exec(heading);\n return match ? (match[1] as string) : null;\n}\n\n/**\n * Chooses the digit width of the canonical id.\n *\n * A repository that writes `0007` everywhere should see `ADR-0007` in reports,\n * not `ADR-7`. The file name is the most reliable witness of the local\n * convention, so it decides.\n */\nfunction padNumber(number: number, witnesses: readonly (string | null | undefined)[]): string {\n const width = paddingWidth(number, witnesses) ?? String(number).length;\n return String(number).padStart(width, '0');\n}\n\n/** The digit width of the first witness that actually spells this number. */\nfunction paddingWidth(number: number, witnesses: readonly (string | null | undefined)[]): number | null {\n for (const witness of witnesses) {\n if (!witness) continue;\n const digits = /(\\d{1,6})/.exec(witness)?.[1];\n if (digits && Number.parseInt(digits, 10) === number) return digits.length;\n }\n return null;\n}\n\n/** Zero-padded spellings a citation might reasonably use. */\nfunction numberSpellings(number: number, witnesses: readonly (string | null | undefined)[]): string[] {\n const out = new Set<string>([String(number)]);\n const observed = paddingWidth(number, witnesses);\n if (observed !== null) out.add(String(number).padStart(observed, '0'));\n for (const width of [3, 4]) out.add(String(number).padStart(width, '0'));\n return [...out];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Document identity and reference normalisation.\n *\n * Foreign-key validation across a corpus of Markdown only works if `ADR-7`,\n * `ADR-0007`, `adr 7`, `[[0007-sharding]]` and `../adr/0007-sharding.md` are\n * understood to name the same document. Getting that wrong in either direction\n * is fatal: too strict and every real repository drowns in false \"broken\n * reference\" findings, too loose and `RFC 7` silently resolves to `ADR-7`.\n *\n * The resolution here is deliberately asymmetric. **Textual aliases** (`adr-7`)\n * are global, because a prefix already names the family. **Bare numbers** (`7`,\n * `#0007`) are resolved only within the citing document's own family, because a\n * repository with both `adr/0007` and `rfc/0007` is normal and guessing between\n * them would be worse than reporting nothing.\n */\n\n/** Directory names that name a specification family, mapped to a canonical prefix. */\nconst FAMILY_DIRECTORIES: Readonly<Record<string, string>> = {\n adr: 'ADR',\n adrs: 'ADR',\n 'architecture-decisions': 'ADR',\n 'decision-records': 'ADR',\n decisions: 'ADR',\n rfc: 'RFC',\n rfcs: 'RFC',\n kep: 'KEP',\n keps: 'KEP',\n enhancements: 'KEP',\n proposal: 'PROPOSAL',\n proposals: 'PROPOSAL',\n design: 'DESIGN',\n designs: 'DESIGN',\n prd: 'PRD',\n prds: 'PRD',\n spec: 'SPEC',\n specs: 'SPEC',\n};\n\n/** Front-matter keys that may carry an explicit identifier, in preference order. */\nexport const ID_KEYS: readonly string[] = ['id', 'adr', 'adr-id', 'rfc', 'rfc-id', 'kep-number', 'number', 'slug'];\n\n/** `ADR-0007`, `KEP 1234`, `rfc2119`: a family prefix followed by a number. */\nconst PREFIXED_ID = /^([A-Za-z][A-Za-z0-9_]{0,15}?)[\\s._-]*(\\d{1,6})$/;\n/** A bare number, optionally hash-prefixed: `7`, `#0007`. */\nconst BARE_NUMBER = /^#?(\\d{1,6})$/;\n\nexport interface DocumentIdentity {\n /** The canonical id: `ADR-0007`, or a path when nothing better exists. */\n readonly id: string;\n /** Family prefix, upper-cased. `null` when the document is not numbered. */\n readonly family: string | null;\n /** Sequence number without leading zeros. `null` when not numbered. */\n readonly number: number | null;\n /** Every lower-cased spelling that should resolve to this document. */\n readonly aliases: readonly string[];\n}\n\nexport interface IdentityInput {\n /** Repository-relative POSIX path. */\n readonly path: string;\n /** An id declared by a directive or front matter, if any. */\n readonly declaredId: string | null;\n /** Extra aliases declared by a directive or front matter. */\n readonly declaredAliases: readonly string[];\n /** The document H1, if any. */\n readonly heading: string | null;\n /**\n * Register the file path and stem as aliases. Default true.\n *\n * A specification that is a *region* of a file must not claim the file's\n * path: the file already answers to it, and two nodes answering to one path\n * would make every link to that file ambiguous.\n */\n readonly includePathAliases?: boolean | undefined;\n}\n\n/** An id has to be a name. Punctuation alone is what a bad parse leaves behind. */\nconst HAS_LETTER_OR_DIGIT = /[\\p{L}\\p{N}]/u;\n\n/**\n * Derives a document's canonical id and every alias that should reach it.\n *\n * Evidence is used in order of how deliberate it is: an explicit declaration,\n * then the file name, then the H1. Whatever loses still contributes aliases, so\n * a reference written in any of those forms still resolves.\n */\nexport function identify(input: IdentityInput): DocumentIdentity {\n // A declaration carrying no letter or digit is discarded rather than honoured.\n // `<!-- @spec-node id=\\\"ADR-9\\\" -->`, written inside a JavaScript string in a\n // worked example, parses its bare value as a lone backslash - and a document\n // whose id is `\\` collides with every other one that made the same mistake,\n // exports as invalid Mermaid, and names itself in findings a reader cannot act\n // on. There is nothing to gain by taking such a value seriously.\n const declaredId = input.declaredId !== null && HAS_LETTER_OR_DIGIT.test(input.declaredId) ? input.declaredId : null;\n const stem = fileStem(input.path);\n const directoryFamily = familyFromPath(input.path);\n const aliases = new Set<string>();\n\n const add = (value: string | null | undefined): void => {\n if (!value) return;\n const key = normaliseRef(value);\n if (key.length > 0) aliases.add(key);\n };\n\n const candidates: { family: string | null; number: number | null; label: string }[] = [];\n\n const consider = (raw: string | null, allowBare: boolean): void => {\n if (!raw) return;\n const cleaned = raw.trim();\n if (cleaned.length === 0) return;\n const prefixed = PREFIXED_ID.exec(cleaned);\n if (prefixed) {\n candidates.push({\n family: (prefixed[1] as string).toUpperCase(),\n number: Number.parseInt(prefixed[2] as string, 10),\n label: cleaned,\n });\n return;\n }\n const bare = BARE_NUMBER.exec(cleaned);\n if (bare && allowBare) {\n candidates.push({\n family: directoryFamily,\n number: Number.parseInt(bare[1] as string, 10),\n label: cleaned,\n });\n return;\n }\n candidates.push({ family: null, number: null, label: cleaned });\n };\n\n consider(declaredId, true);\n // `0007-sharding-the-write-path.md` and `kep-1234-foo.md` both start with the\n // identifier; take the leading token rather than the whole stem.\n consider(leadingToken(stem), true);\n consider(headingId(input.heading), false);\n\n const numbered = candidates.find((c) => c.number !== null);\n const family = numbered?.family ?? directoryFamily;\n const number = numbered?.number ?? null;\n\n // Witnesses to the local zero-padding convention, most authoritative first.\n // The spelling that actually produced the number comes first: an id read from\n // the H1 of `sharding.md` must still render as ADR-0007, not ADR-7.\n const witnesses = [numbered?.label, declaredId, stem];\n\n let id: string;\n if (number !== null && family !== null) {\n id = `${family}-${padNumber(number, witnesses)}`;\n } else if (declaredId) {\n id = declaredId.trim();\n } else if (number !== null) {\n // Numbered but with no family to prefix - a Rust RFC in `text/0001-foo.md`,\n // say. The file stem is a far better identity than a bare \"1\": it is what\n // people actually type, and it stays unique across the repository.\n id = stem.length > 0 ? stem : String(number);\n } else {\n id = stem.length > 0 ? stem : input.path;\n }\n\n add(id);\n if (input.includePathAliases !== false) {\n add(stem);\n add(input.path);\n add(stripExtension(input.path));\n }\n add(declaredId);\n for (const alias of input.declaredAliases) add(alias);\n if (input.heading) add(headingId(input.heading));\n\n if (number !== null) {\n const spellings = numberSpellings(number, witnesses);\n if (family !== null) {\n // Every spelling a human might type. `normaliseRef` folds separators, so\n // `adr-7`, `adr 7` and `adr7` collapse to one key; the padding variants do\n // not, and each needs registering.\n for (const digits of spellings) add(`${family}-${digits}`);\n } else {\n // With no family there is only one number space in the repository, so a\n // bare `0001` is unambiguous and worth resolving.\n for (const digits of spellings) add(digits);\n }\n }\n\n return { id, family, number, aliases: [...aliases] };\n}\n\n/**\n * Folds a reference to its lookup key.\n *\n * Case, separators and surrounding punctuation carry no meaning in a citation,\n * so `ADR-0007`, `adr 0007` and `Adr_0007` all become `adr0007`.\n */\nexport function normaliseRef(value: string): string {\n return value\n .trim()\n .toLowerCase()\n .replace(/^[#<([]+|[)\\]>.,;:]+$/g, '')\n .replace(/[\\s._-]+/g, '')\n .trim();\n}\n\n/** Splits a `target#anchor` reference. */\nexport function splitAnchor(target: string): { target: string; anchor: string | null } {\n const hash = target.indexOf('#');\n if (hash === -1) return { target, anchor: null };\n // A leading `#` is an in-document anchor, not a separator.\n if (hash === 0) return { target: '', anchor: target.slice(1) };\n return { target: target.slice(0, hash), anchor: target.slice(hash + 1) };\n}\n\n/** Parses `ADR-0007` into its family and number, or returns `null`. */\nexport function parsePrefixedRef(value: string): { family: string; number: number } | null {\n const match = PREFIXED_ID.exec(value.trim().replace(/^[#([]+/, ''));\n if (!match) return null;\n return { family: (match[1] as string).toUpperCase(), number: Number.parseInt(match[2] as string, 10) };\n}\n\n/** Parses a bare `7` or `#0007`, or returns `null`. */\nexport function parseBareRef(value: string): number | null {\n const match = BARE_NUMBER.exec(value.trim());\n return match ? Number.parseInt(match[1] as string, 10) : null;\n}\n\n/** True when a reference target names a location rather than an identifier. */\nexport function looksLikePath(target: string): boolean {\n return target.includes('/') || DOCUMENT_EXTENSION.test(target);\n}\n\nconst DOCUMENT_EXTENSION = /\\.(md|markdown|mdx|txt|rst|adoc|asciidoc)$/i;\n\n/**\n * True when a path-like target could name a specification.\n *\n * A specification linking to `../../src/rules.ts` is citing source code, which\n * is a different tool's job. Treating it as a dangling specification reference\n * would report an error on one of the most ordinary things a design document\n * does, so targets carrying a non-document extension are not references at all.\n */\nexport function isDocumentTarget(target: string): boolean {\n const withoutAnchor = splitAnchor(target).target;\n const base = withoutAnchor.slice(withoutAnchor.lastIndexOf('/') + 1);\n const dot = base.lastIndexOf('.');\n // No extension: a directory, or an extensionless spec such as `rfcs/0007`.\n if (dot <= 0) return true;\n const extension = base.slice(dot);\n // Only a plausible file extension disqualifies a target. An identifier like\n // `v1.2` or `ADR-0007.1` keeps its dot and is still resolved as an id.\n if (!/^\\.[A-Za-z][A-Za-z0-9]{0,5}$/.test(extension)) return true;\n return DOCUMENT_EXTENSION.test(base);\n}\n\n/** True when a reference target points outside the repository. */\nexport function isExternal(target: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(target) && !/^[a-z]:[\\\\/]/i.test(target);\n}\n\n/** The family prefix implied by a document's directory, if any. */\nexport function familyFromPath(posixPath: string): string | null {\n const segments = posixPath.toLowerCase().split('/');\n // Nearest enclosing directory wins: `docs/rfcs/adr/0007.md` is an ADR.\n for (let i = segments.length - 2; i >= 0; i -= 1) {\n const found = FAMILY_DIRECTORIES[segments[i] as string];\n if (found) return found;\n }\n return null;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Internals */\n/* -------------------------------------------------------------------------- */\n\nfunction fileStem(posixPath: string): string {\n const base = posixPath.slice(posixPath.lastIndexOf('/') + 1);\n const dot = base.lastIndexOf('.');\n const stem = dot > 0 ? base.slice(0, dot) : base;\n // `docs/adr/0007-sharding/README.md` is identified by its directory - but a\n // README at the repository root has no directory to be named after, and must\n // keep its own name.\n const slash = posixPath.lastIndexOf('/');\n if (slash > 0 && /^(readme|index)$/i.test(stem)) {\n const parent = posixPath.slice(0, slash);\n return parent.slice(parent.lastIndexOf('/') + 1);\n }\n return stem;\n}\n\nfunction stripExtension(posixPath: string): string {\n const dot = posixPath.lastIndexOf('.');\n const slash = posixPath.lastIndexOf('/');\n return dot > slash ? posixPath.slice(0, dot) : posixPath;\n}\n\n/** `kep-1234-foo` -> `kep-1234`; `0007-sharding` -> `0007`; `sharding` -> `sharding`. */\nfunction leadingToken(stem: string): string {\n const prefixed = /^([A-Za-z]{1,15}[\\s._-]?\\d{1,6})(?:[\\s._-]|$)/.exec(stem);\n if (prefixed) return prefixed[1] as string;\n const bare = /^(\\d{1,6})(?:[\\s._-]|$)/.exec(stem);\n if (bare) return bare[1] as string;\n return stem;\n}\n\n/** `# ADR-0007: Sharding the write path` -> `ADR-0007`. */\nfunction headingId(heading: string | null): string | null {\n if (!heading) return null;\n const match = /^\\s*([A-Za-z]{1,15}[\\s._-]?\\d{1,6})\\b/.exec(heading);\n return match ? (match[1] as string) : null;\n}\n\n/**\n * Chooses the digit width of the canonical id.\n *\n * A repository that writes `0007` everywhere should see `ADR-0007` in reports,\n * not `ADR-7`. The file name is the most reliable witness of the local\n * convention, so it decides.\n */\nfunction padNumber(number: number, witnesses: readonly (string | null | undefined)[]): string {\n const width = paddingWidth(number, witnesses) ?? String(number).length;\n return String(number).padStart(width, '0');\n}\n\n/** The digit width of the first witness that actually spells this number. */\nfunction paddingWidth(number: number, witnesses: readonly (string | null | undefined)[]): number | null {\n for (const witness of witnesses) {\n if (!witness) continue;\n const digits = /(\\d{1,6})/.exec(witness)?.[1];\n if (digits && Number.parseInt(digits, 10) === number) return digits.length;\n }\n return null;\n}\n\n/** Zero-padded spellings a citation might reasonably use. */\nfunction numberSpellings(number: number, witnesses: readonly (string | null | undefined)[]): string[] {\n const out = new Set<string>([String(number)]);\n const observed = paddingWidth(number, witnesses);\n if (observed !== null) out.add(String(number).padStart(observed, '0'));\n for (const width of [3, 4]) out.add(String(number).padStart(width, '0'));\n return [...out];\n}\n\n/**\n * True when two folded spellings are one edit apart.\n *\n * One edit, not two, and a transposition counts as one: `supercedesby` for\n * `supercededby` is a slip of the finger, `categories` for `dependencies` is a\n * different word. Written out rather than as a distance matrix because the\n * answer is a yes or a no and the bound is one - the general algorithm would be\n * more code, not less.\n */\nexport function withinOneEdit(a: string, b: string): boolean {\n if (a === b) return false;\n if (a.length === b.length) {\n let first = -1;\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] === b[i]) continue;\n if (first === -1) {\n first = i;\n continue;\n }\n // A second difference is allowed only if the two are a swapped pair, and\n // only if there is no third.\n return first === i - 1 && a[first] === b[i] && a[i] === b[first] && a.slice(i + 1) === b.slice(i + 1);\n }\n return true;\n }\n const [short, long] = a.length < b.length ? [a, b] : [b, a];\n if (long.length - short.length !== 1) return false;\n for (let i = 0; i < short.length; i += 1) {\n if (short[i] === long[i]) continue;\n return short.slice(i) === long.slice(i + 1);\n }\n return true;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,22 +22,26 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export { applyBaseline, formatBaseline, fingerprintOf, parseBaseline, BASELINE_VERSION, EMPTY_BASELINE, } from './baseline.js';
|
|
25
|
-
export type { Baseline, BaselineEntry, BaselineOutcome, ParsedBaseline } from './baseline.js';
|
|
26
|
-
export { loadConfig, parseConfig, CONFIG_FILES, CONFIG_PACKAGE_KEY } from './config.js';
|
|
27
|
-
export type { LoadedConfig, SpecGraphConfig } from './config.js';
|
|
25
|
+
export type { Baseline, BaselineEntry, BaselineOutcome, ParsedBaseline, StaleEntry } from './baseline.js';
|
|
26
|
+
export { discoverConfig, loadConfig, parseConfig, CONFIG_FILES, CONFIG_PACKAGE_KEY } from './config.js';
|
|
27
|
+
export type { DiscoveredConfig, DiscoveryIO, LoadedConfig, SpecGraphConfig } from './config.js';
|
|
28
28
|
export { main, parseArgs, HELP, UsageError, EXIT_OK, EXIT_FAILED, EXIT_ERROR } from './cli.js';
|
|
29
29
|
export type { CliIO, CliOptions, Command } from './cli.js';
|
|
30
30
|
export { analyse, analyseSources, createFamilyFilter, createHistoryMatcher, withDiagnostics, DEFAULT_CONCURRENCY, DEFAULT_PATTERNS, } from './runner.js';
|
|
31
31
|
export type { AnalyseOptions, AnalyseSourcesOptions, AnalysisResult, Source } from './runner.js';
|
|
32
32
|
export { buildGraph, LOAD_BEARING_EDGES, OBLIGATION_EDGES } from './graph.js';
|
|
33
33
|
export type { SpecGraph } from './graph.js';
|
|
34
|
-
export { attributesOf, execute, matches, parseQuery, query, renderMatch, QueryError, DEFAULT_MATCH_LIMIT, } from './select.js';
|
|
34
|
+
export { attributesOf, execute, matches, parseQuery, pattern, query, renderMatch, QueryError, DEFAULT_MATCH_LIMIT, SELECTOR_KEYS, } from './select.js';
|
|
35
|
+
export { compilePattern, PatternError } from './regex.js';
|
|
36
|
+
export type { Matcher } from './regex.js';
|
|
35
37
|
export type { ExecuteOptions, Match, NodeMatcher, Operator, Predicate, QuerySpec, StepSpec, } from './select.js';
|
|
36
|
-
export { resolveStrict, runRules, sortDiagnostics, DEFAULT_SEVERITIES, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
|
|
38
|
+
export { resolveStrict, runRules, sortDiagnostics, DEFAULT_SEVERITIES, RULE_DECISIONS, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
|
|
37
39
|
export type { RuleOptions } from './rules.js';
|
|
38
|
-
export {
|
|
40
|
+
export { compileProjectRules, renderTemplate } from './project-rules.js';
|
|
41
|
+
export type { CompiledProjectRules, ProjectRule } from './project-rules.js';
|
|
42
|
+
export { createPainter, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
39
43
|
export type { ColorEnvironment, GraphExportOptions, GraphFormat, Painter, ReporterOptions } from './report.js';
|
|
40
|
-
export { extractDocument, extractSpecifications, classifyReference, sectionPathAt } from './extract.js';
|
|
44
|
+
export { extractDocument, extractSpecifications, classifyReference, inheritableFrontMatter, sectionPathAt, } from './extract.js';
|
|
41
45
|
export { findSpecificationRegions, readRegionStatus, regionAt } from './sections.js';
|
|
42
46
|
export type { RegionStatus, SpecificationRegion } from './sections.js';
|
|
43
47
|
export type { Classification, ExtractedDocument, ExtractInput, ReferenceCandidate } from './extract.js';
|
|
@@ -59,5 +63,5 @@ export type { GlobMatcher, WalkedFile, WalkOptions } from './glob.js';
|
|
|
59
63
|
export { compareRefs, createLineIndex, formatRef, refOf, spanOf } from './source.js';
|
|
60
64
|
export type { LineIndex } from './source.js';
|
|
61
65
|
export { basenamePosix, dirnamePosix, extnamePosix, joinPosix, normalisePosix, resolveFrom, toPosix, } from './paths.js';
|
|
62
|
-
export { DISPOSITIONS, EDGE_KINDS, EDGE_TRAITS, OPENNESS_OF } from './types.js';
|
|
63
|
-
export type { AnalysisSummary, DanglingReason, DanglingRef, Diagnostic, Disposition, DocumentNode, Edge, EdgeKind, EdgeOrigin, EdgeTraits, ItemNode, MisreadKey, NodeKind, Openness, ParseProblem, Phase, Position, Receptivity, RelatedLocation, RuleId, Severity, SourceRef, Span, SpecNode, StateSignal, } from './types.js';
|
|
66
|
+
export { isProjectRule, DISPOSITIONS, EDGE_KINDS, EDGE_TRAITS, OPENNESS_OF, PROJECT_RULE_PREFIX } from './types.js';
|
|
67
|
+
export type { AnalysisSummary, AnyRuleId, DanglingReason, DanglingRef, Diagnostic, Disposition, DocumentNode, Edge, EdgeKind, EdgeOrigin, EdgeTraits, ItemNode, MisreadKey, NodeKind, Openness, ParseProblem, Phase, Position, Receptivity, ProjectRuleId, RelatedLocation, RuleId, Severity, SourceRef, Span, SpecNode, StateSignal, SuppressedRef, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -22,14 +22,16 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export { applyBaseline, formatBaseline, fingerprintOf, parseBaseline, BASELINE_VERSION, EMPTY_BASELINE, } from './baseline.js';
|
|
25
|
-
export { loadConfig, parseConfig, CONFIG_FILES, CONFIG_PACKAGE_KEY } from './config.js';
|
|
25
|
+
export { discoverConfig, loadConfig, parseConfig, CONFIG_FILES, CONFIG_PACKAGE_KEY } from './config.js';
|
|
26
26
|
export { main, parseArgs, HELP, UsageError, EXIT_OK, EXIT_FAILED, EXIT_ERROR } from './cli.js';
|
|
27
27
|
export { analyse, analyseSources, createFamilyFilter, createHistoryMatcher, withDiagnostics, DEFAULT_CONCURRENCY, DEFAULT_PATTERNS, } from './runner.js';
|
|
28
28
|
export { buildGraph, LOAD_BEARING_EDGES, OBLIGATION_EDGES } from './graph.js';
|
|
29
|
-
export { attributesOf, execute, matches, parseQuery, query, renderMatch, QueryError, DEFAULT_MATCH_LIMIT, } from './select.js';
|
|
30
|
-
export {
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
29
|
+
export { attributesOf, execute, matches, parseQuery, pattern, query, renderMatch, QueryError, DEFAULT_MATCH_LIMIT, SELECTOR_KEYS, } from './select.js';
|
|
30
|
+
export { compilePattern, PatternError } from './regex.js';
|
|
31
|
+
export { resolveStrict, runRules, sortDiagnostics, DEFAULT_SEVERITIES, RULE_DECISIONS, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
|
|
32
|
+
export { compileProjectRules, renderTemplate } from './project-rules.js';
|
|
33
|
+
export { createPainter, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
34
|
+
export { extractDocument, extractSpecifications, classifyReference, inheritableFrontMatter, sectionPathAt, } from './extract.js';
|
|
33
35
|
export { findSpecificationRegions, readRegionStatus, regionAt } from './sections.js';
|
|
34
36
|
export { resolveCorpus, documentOf } from './resolve.js';
|
|
35
37
|
export { scanMarkdown, slugify } from './markdown.js';
|
|
@@ -41,5 +43,5 @@ export { familyFromPath, identify, isDocumentTarget, isExternal, looksLikePath,
|
|
|
41
43
|
export { createGlobMatcher, createReferenceFilter, globBase, globToRegExp, isGlob, walkFiles, DEFAULT_IGNORED_DIRECTORIES, MAX_FILE_SIZE, } from './glob.js';
|
|
42
44
|
export { compareRefs, createLineIndex, formatRef, refOf, spanOf } from './source.js';
|
|
43
45
|
export { basenamePosix, dirnamePosix, extnamePosix, joinPosix, normalisePosix, resolveFrom, toPosix, } from './paths.js';
|
|
44
|
-
export { DISPOSITIONS, EDGE_KINDS, EDGE_TRAITS, OPENNESS_OF } from './types.js';
|
|
46
|
+
export { isProjectRule, DISPOSITIONS, EDGE_KINDS, EDGE_TRAITS, OPENNESS_OF, PROJECT_RULE_PREFIX } from './types.js';
|
|
45
47
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,cAAc,EACd,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,cAAc,GACf,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,cAAc,EACd,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,cAAc,GACf,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGxG,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG/F,OAAO,EACL,OAAO,EACP,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,YAAY,EACZ,OAAO,EACP,OAAO,EACP,UAAU,EACV,OAAO,EACP,KAAK,EACL,WAAW,EACX,UAAU,EACV,mBAAmB,EACnB,aAAa,GACd,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAY1D,OAAO,EACL,aAAa,EACb,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzE,OAAO,EACL,aAAa,EACb,WAAW,EACX,UAAU,EACV,cAAc,EACd,YAAY,EACZ,WAAW,EACX,cAAc,EACd,cAAc,GACf,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIrF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAatD,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGhF,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,EACL,SAAS,EACT,eAAe,EACf,eAAe,EACf,aAAa,EACb,OAAO,EACP,aAAa,EACb,qBAAqB,EACrB,WAAW,GACZ,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGjE,OAAO,EACL,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,GACR,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,SAAS,EACT,2BAA2B,EAC3B,aAAa,GACd,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrF,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,OAAO,GACR,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC","sourcesContent":["/**\n * Programmatic API.\n *\n * The CLI is a shell over `analyse`; anything the CLI can do, a build script, a\n * custom reporter or an editor extension can do too.\n *\n * ```ts\n * import { analyse, formatReport } from '@descent-vtt/spec-graph';\n *\n * const result = await analyse({ root: process.cwd(), patterns: ['docs/**\\/*.md'] });\n * if (!result.ok) console.error(formatReport(result, { color: true }));\n * ```\n *\n * For a corpus that is already in memory - a monorepo tool, a language server,\n * a test - skip the filesystem entirely:\n *\n * ```ts\n * import { analyseSources, query } from '@descent-vtt/spec-graph';\n *\n * const { graph } = analyseSources([{ path: 'adr/0001.md', text: '...' }]);\n * const ghosts = query(graph, 'item[openness=open] -delegates-to-> document[phase=retired]');\n * ```\n */\n\nexport {\n applyBaseline,\n formatBaseline,\n fingerprintOf,\n parseBaseline,\n BASELINE_VERSION,\n EMPTY_BASELINE,\n} from './baseline.js';\nexport type { Baseline, BaselineEntry, BaselineOutcome, ParsedBaseline, StaleEntry } from './baseline.js';\n\nexport { discoverConfig, loadConfig, parseConfig, CONFIG_FILES, CONFIG_PACKAGE_KEY } from './config.js';\nexport type { DiscoveredConfig, DiscoveryIO, LoadedConfig, SpecGraphConfig } from './config.js';\n\nexport { main, parseArgs, HELP, UsageError, EXIT_OK, EXIT_FAILED, EXIT_ERROR } from './cli.js';\nexport type { CliIO, CliOptions, Command } from './cli.js';\n\nexport {\n analyse,\n analyseSources,\n createFamilyFilter,\n createHistoryMatcher,\n withDiagnostics,\n DEFAULT_CONCURRENCY,\n DEFAULT_PATTERNS,\n} from './runner.js';\nexport type { AnalyseOptions, AnalyseSourcesOptions, AnalysisResult, Source } from './runner.js';\n\nexport { buildGraph, LOAD_BEARING_EDGES, OBLIGATION_EDGES } from './graph.js';\nexport type { SpecGraph } from './graph.js';\n\nexport {\n attributesOf,\n execute,\n matches,\n parseQuery,\n pattern,\n query,\n renderMatch,\n QueryError,\n DEFAULT_MATCH_LIMIT,\n SELECTOR_KEYS,\n} from './select.js';\n\nexport { compilePattern, PatternError } from './regex.js';\nexport type { Matcher } from './regex.js';\nexport type {\n ExecuteOptions,\n Match,\n NodeMatcher,\n Operator,\n Predicate,\n QuerySpec,\n StepSpec,\n} from './select.js';\n\nexport {\n resolveStrict,\n runRules,\n sortDiagnostics,\n DEFAULT_SEVERITIES,\n RULE_DECISIONS,\n RULE_DESCRIPTIONS,\n RULE_IDS,\n RULE_QUERIES,\n} from './rules.js';\nexport type { RuleOptions } from './rules.js';\n\nexport { compileProjectRules, renderTemplate } from './project-rules.js';\nexport type { CompiledProjectRules, ProjectRule } from './project-rules.js';\n\nexport {\n createPainter,\n formatGraph,\n formatJson,\n formatMarkdown,\n formatReport,\n formatSarif,\n shouldUseAscii,\n shouldUseColor,\n} from './report.js';\nexport type { ColorEnvironment, GraphExportOptions, GraphFormat, Painter, ReporterOptions } from './report.js';\n\nexport {\n extractDocument,\n extractSpecifications,\n classifyReference,\n inheritableFrontMatter,\n sectionPathAt,\n} from './extract.js';\nexport { findSpecificationRegions, readRegionStatus, regionAt } from './sections.js';\nexport type { RegionStatus, SpecificationRegion } from './sections.js';\nexport type { Classification, ExtractedDocument, ExtractInput, ReferenceCandidate } from './extract.js';\n\nexport { resolveCorpus, documentOf } from './resolve.js';\nexport type { ResolvedCorpus, ResolveOptions } from './resolve.js';\n\nexport { scanMarkdown, slugify } from './markdown.js';\nexport type {\n FrontMatter,\n Heading,\n HtmlComment,\n Link,\n LinkForm,\n ListItem,\n Range,\n ScannedDocument,\n ScannedLine,\n} from './markdown.js';\n\nexport { parseDirectives, attr, attrList, directiveFor } from './directives.js';\nexport type { Attribute, Directive, DirectiveName } from './directives.js';\n\nexport { parseFrontMatter, toRecord, valuesOf } from './yaml.js';\nexport type { YamlEntry } from './yaml.js';\n\nexport {\n isRetired,\n isStatusHeading,\n normaliseStatus,\n phaseFromPath,\n phaseOf,\n receptivityOf,\n supersessionTargetsIn,\n STATUS_KEYS,\n} from './lifecycle.js';\n\nexport { resolveItemState, KNOWN_STATE_WORDS } from './state.js';\nexport type { ResolvedState, StateInput } from './state.js';\n\nexport {\n familyFromPath,\n identify,\n isDocumentTarget,\n isExternal,\n looksLikePath,\n normaliseRef,\n parseBareRef,\n parsePrefixedRef,\n splitAnchor,\n ID_KEYS,\n} from './identity.js';\nexport type { DocumentIdentity, IdentityInput } from './identity.js';\n\nexport {\n createGlobMatcher,\n createReferenceFilter,\n globBase,\n globToRegExp,\n isGlob,\n walkFiles,\n DEFAULT_IGNORED_DIRECTORIES,\n MAX_FILE_SIZE,\n} from './glob.js';\nexport type { GlobMatcher, WalkedFile, WalkOptions } from './glob.js';\n\nexport { compareRefs, createLineIndex, formatRef, refOf, spanOf } from './source.js';\nexport type { LineIndex } from './source.js';\n\nexport {\n basenamePosix,\n dirnamePosix,\n extnamePosix,\n joinPosix,\n normalisePosix,\n resolveFrom,\n toPosix,\n} from './paths.js';\n\nexport { isProjectRule, DISPOSITIONS, EDGE_KINDS, EDGE_TRAITS, OPENNESS_OF, PROJECT_RULE_PREFIX } from './types.js';\nexport type {\n AnalysisSummary,\n AnyRuleId,\n DanglingReason,\n DanglingRef,\n Diagnostic,\n Disposition,\n DocumentNode,\n Edge,\n EdgeKind,\n EdgeOrigin,\n EdgeTraits,\n ItemNode,\n MisreadKey,\n NodeKind,\n Openness,\n ParseProblem,\n Phase,\n Position,\n Receptivity,\n ProjectRuleId,\n RelatedLocation,\n RuleId,\n Severity,\n SourceRef,\n Span,\n SpecNode,\n StateSignal,\n SuppressedRef,\n} from './types.js';\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules a repository writes for itself.
|
|
3
|
+
*
|
|
4
|
+
* [ADR-0005](../docs/adr/0005-rules-are-queries.md) claimed the selector
|
|
5
|
+
* language was expressive enough that a team could express a convention
|
|
6
|
+
* spec-graph never anticipated. For three releases that claim was only true at a
|
|
7
|
+
* command prompt: `spec-graph query` could find the thing, and nothing could
|
|
8
|
+
* make finding it fail a build. Three ADRs recorded the same gap and the same
|
|
9
|
+
* reason - a query needs a severity and a sentence before it is a rule.
|
|
10
|
+
*
|
|
11
|
+
* This is the sentence. A project rule is a selector, a message, a hint and a
|
|
12
|
+
* severity, written in `.spec-graph.json` and run beside the built-ins:
|
|
13
|
+
*
|
|
14
|
+
* ```json
|
|
15
|
+
* "rules": {
|
|
16
|
+
* "no-draft-dependency": {
|
|
17
|
+
* "query": "document[phase=active] -depends-on-> document[phase=draft]",
|
|
18
|
+
* "message": "{0} depends on {1}, which is still a draft",
|
|
19
|
+
* "hint": "wait for {1} to be accepted, or drop the dependency"
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Everything here is a pure function of the parsed configuration. Reading the
|
|
25
|
+
* file stays in `config.ts`, for the same reason extraction never touches a
|
|
26
|
+
* disk.
|
|
27
|
+
*/
|
|
28
|
+
import { type Match, type QuerySpec } from './select.js';
|
|
29
|
+
import type { SpecGraph } from './graph.js';
|
|
30
|
+
import type { ProjectRuleId, Severity } from './types.js';
|
|
31
|
+
/** A project rule, compiled and known to be runnable. */
|
|
32
|
+
export interface ProjectRule {
|
|
33
|
+
/** Namespaced, so it can never collide with a built-in. */
|
|
34
|
+
readonly id: ProjectRuleId;
|
|
35
|
+
/** The name as the repository wrote it, for error messages. */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/**
|
|
38
|
+
* The selectors, in the order they were written.
|
|
39
|
+
*
|
|
40
|
+
* More than one because a convention is often a union - "no live document may
|
|
41
|
+
* depend on a draft *or* on a record" - and a union of queries is clearer than
|
|
42
|
+
* a disjunction inside the grammar. See ADR-0005.
|
|
43
|
+
*/
|
|
44
|
+
readonly queries: readonly QuerySpec[];
|
|
45
|
+
/**
|
|
46
|
+
* The selectors as written, kept beside the compiled form.
|
|
47
|
+
*
|
|
48
|
+
* A report has to describe the rule, and the only honest description of a
|
|
49
|
+
* project rule is the query behind it. Echoing the text back is exact and
|
|
50
|
+
* free; rendering a `QuerySpec` would be a second grammar to keep correct.
|
|
51
|
+
*/
|
|
52
|
+
readonly sources: readonly string[];
|
|
53
|
+
readonly message: string;
|
|
54
|
+
readonly hint: string;
|
|
55
|
+
/**
|
|
56
|
+
* What the repository declared, before `--rule` and `--strict` have their say.
|
|
57
|
+
*
|
|
58
|
+
* Defaults to `warn` rather than to `error`. A rule a team has just written
|
|
59
|
+
* has not yet earned the right to stop their build, and the first run of a new
|
|
60
|
+
* convention is exactly when it is most likely to be wrong.
|
|
61
|
+
*/
|
|
62
|
+
readonly severity: Severity;
|
|
63
|
+
}
|
|
64
|
+
export interface CompiledProjectRules {
|
|
65
|
+
readonly rules: readonly ProjectRule[];
|
|
66
|
+
readonly problems: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Turns the `rules` section of a configuration into runnable rules.
|
|
70
|
+
*
|
|
71
|
+
* Every failure is collected rather than thrown. A repository whose eighth
|
|
72
|
+
* custom rule has a typo in it should still get the findings from the other
|
|
73
|
+
* seven, and should be told exactly which one stopped working - a configuration
|
|
74
|
+
* that silently does less than it says is the failure mode this whole file
|
|
75
|
+
* exists to avoid.
|
|
76
|
+
*/
|
|
77
|
+
export declare function compileProjectRules(raw: unknown, source: string): CompiledProjectRules;
|
|
78
|
+
/**
|
|
79
|
+
* Fills a validated template in from one path through the graph.
|
|
80
|
+
*
|
|
81
|
+
* Sharing one global regex between this and {@link placeholderProblems} is safe
|
|
82
|
+
* and not an oversight: `matchAll` works on a clone, and `replace` resets
|
|
83
|
+
* `lastIndex` on entry and on exit. The hazard `CLAUDE.md` warns about is a
|
|
84
|
+
* `/g` regex whose cursor survives a call, and neither of these keeps one.
|
|
85
|
+
*
|
|
86
|
+
* The index cannot be out of range when `execute` produced the match - a path is
|
|
87
|
+
* always one node longer than the query has steps, which is what validation
|
|
88
|
+
* checks against. The guard is for the exported case: this takes any `Match`,
|
|
89
|
+
* and a caller assembling one by hand gets the placeholder back rather than a
|
|
90
|
+
* crash.
|
|
91
|
+
*/
|
|
92
|
+
export declare function renderTemplate(template: string, match: Match, graph: SpecGraph): string;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules a repository writes for itself.
|
|
3
|
+
*
|
|
4
|
+
* [ADR-0005](../docs/adr/0005-rules-are-queries.md) claimed the selector
|
|
5
|
+
* language was expressive enough that a team could express a convention
|
|
6
|
+
* spec-graph never anticipated. For three releases that claim was only true at a
|
|
7
|
+
* command prompt: `spec-graph query` could find the thing, and nothing could
|
|
8
|
+
* make finding it fail a build. Three ADRs recorded the same gap and the same
|
|
9
|
+
* reason - a query needs a severity and a sentence before it is a rule.
|
|
10
|
+
*
|
|
11
|
+
* This is the sentence. A project rule is a selector, a message, a hint and a
|
|
12
|
+
* severity, written in `.spec-graph.json` and run beside the built-ins:
|
|
13
|
+
*
|
|
14
|
+
* ```json
|
|
15
|
+
* "rules": {
|
|
16
|
+
* "no-draft-dependency": {
|
|
17
|
+
* "query": "document[phase=active] -depends-on-> document[phase=draft]",
|
|
18
|
+
* "message": "{0} depends on {1}, which is still a draft",
|
|
19
|
+
* "hint": "wait for {1} to be accepted, or drop the dependency"
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Everything here is a pure function of the parsed configuration. Reading the
|
|
25
|
+
* file stays in `config.ts`, for the same reason extraction never touches a
|
|
26
|
+
* disk.
|
|
27
|
+
*/
|
|
28
|
+
import { attributesOf, parseQuery, QueryError, SELECTOR_KEYS } from './select.js';
|
|
29
|
+
import { PROJECT_RULE_PREFIX } from './types.js';
|
|
30
|
+
/* -------------------------------------------------------------------------- */
|
|
31
|
+
/* Compilation */
|
|
32
|
+
/* -------------------------------------------------------------------------- */
|
|
33
|
+
/**
|
|
34
|
+
* What a name may contain.
|
|
35
|
+
*
|
|
36
|
+
* No colon, because the namespace separator has to stay unambiguous. No
|
|
37
|
+
* whitespace, because the id is printed in a column of a report. And no tab in
|
|
38
|
+
* particular: a baseline key is three fields joined by one, and a rule id that
|
|
39
|
+
* could contain a tab would be a rule whose accepted debt could be forged by
|
|
40
|
+
* typing carefully. See ADR-0012.
|
|
41
|
+
*/
|
|
42
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
43
|
+
const MAX_NAME_LENGTH = 64;
|
|
44
|
+
/**
|
|
45
|
+
* `{0}`, `{1.phase}`, `{2.fm.owner}`.
|
|
46
|
+
*
|
|
47
|
+
* Positional because a path is positional: step zero is where the finding
|
|
48
|
+
* starts and the last step is where it ends. Naming them would mean inventing a
|
|
49
|
+
* second vocabulary for something the selector already numbers.
|
|
50
|
+
*/
|
|
51
|
+
const PLACEHOLDER = /\{(\d+)(?:\.([A-Za-z][A-Za-z0-9_.:-]*))?\}/g;
|
|
52
|
+
/**
|
|
53
|
+
* Turns the `rules` section of a configuration into runnable rules.
|
|
54
|
+
*
|
|
55
|
+
* Every failure is collected rather than thrown. A repository whose eighth
|
|
56
|
+
* custom rule has a typo in it should still get the findings from the other
|
|
57
|
+
* seven, and should be told exactly which one stopped working - a configuration
|
|
58
|
+
* that silently does less than it says is the failure mode this whole file
|
|
59
|
+
* exists to avoid.
|
|
60
|
+
*/
|
|
61
|
+
export function compileProjectRules(raw, source) {
|
|
62
|
+
const problems = [];
|
|
63
|
+
if (!isRecord(raw)) {
|
|
64
|
+
return { rules: [], problems: [`${source}: "rules" must be an object of name to rule`] };
|
|
65
|
+
}
|
|
66
|
+
const rules = [];
|
|
67
|
+
for (const [name, body] of Object.entries(raw)) {
|
|
68
|
+
const rule = compileOne(name, body, source, problems);
|
|
69
|
+
if (rule !== null)
|
|
70
|
+
rules.push(rule);
|
|
71
|
+
}
|
|
72
|
+
// Sorted by id, so two runs of one configuration produce findings in the same
|
|
73
|
+
// order however the JSON was keyed. Two ids are never equal - they are object
|
|
74
|
+
// keys - so there is no third case to write, and `<` against `<=` here is a
|
|
75
|
+
// genuinely equivalent mutant with no test behind it for that reason.
|
|
76
|
+
return { rules: rules.sort((a, b) => (a.id < b.id ? -1 : 1)), problems };
|
|
77
|
+
}
|
|
78
|
+
function compileOne(name, body, source, problems) {
|
|
79
|
+
const where = `${source}: rules.${name}`;
|
|
80
|
+
if (!NAME.test(name) || name.length > MAX_NAME_LENGTH) {
|
|
81
|
+
problems.push(`${where}: a rule name must be letters, digits, "." "_" or "-", at most ${MAX_NAME_LENGTH} characters`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (!isRecord(body)) {
|
|
85
|
+
problems.push(`${where} must be an object with "query" and "message"`);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
for (const key of Object.keys(body)) {
|
|
89
|
+
if (!RULE_KEYS.has(key))
|
|
90
|
+
problems.push(`${where}: unknown key "${key}"`);
|
|
91
|
+
}
|
|
92
|
+
const written = body['query'];
|
|
93
|
+
const listed = typeof written === 'string' ? [written] : Array.isArray(written) ? written : [];
|
|
94
|
+
if (listed.length === 0 || listed.some((entry) => typeof entry !== 'string')) {
|
|
95
|
+
problems.push(`${where}: "query" must be a selector, or an array of them`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const sources = listed;
|
|
99
|
+
const queries = [];
|
|
100
|
+
for (const selector of sources) {
|
|
101
|
+
try {
|
|
102
|
+
queries.push(parseQuery(selector));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const detail = error instanceof QueryError ? `${error.message} (at character ${error.offset + 1})` : String(error);
|
|
106
|
+
problems.push(`${where}: ${detail}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const message = body['message'];
|
|
111
|
+
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
112
|
+
problems.push(`${where}: "message" must be the one-line headline of the finding`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// A finding has to name a next action, so the fallback names the only thing
|
|
116
|
+
// this file can be certain of: where the rule was written down.
|
|
117
|
+
const hint = body['hint'] ?? `check this against rules.${name} in ${source}`;
|
|
118
|
+
if (typeof hint !== 'string' || hint.trim().length === 0) {
|
|
119
|
+
problems.push(`${where}: "hint" must be a concrete next action`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const declared = body['severity'] ?? 'warn';
|
|
123
|
+
if (declared !== 'error' && declared !== 'warn' && declared !== 'info' && declared !== 'off') {
|
|
124
|
+
problems.push(`${where}: "severity" must be error, warn, info or off`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
// The shortest path a rule can produce, because a placeholder has to be
|
|
128
|
+
// resolvable by *every* query behind the rule and not merely by the longest.
|
|
129
|
+
const arity = Math.min(...queries.map((query) => query.steps.length + 1));
|
|
130
|
+
let usable = true;
|
|
131
|
+
for (const text of [message, hint]) {
|
|
132
|
+
for (const problem of placeholderProblems(text, arity, where)) {
|
|
133
|
+
problems.push(problem);
|
|
134
|
+
usable = false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!usable)
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
id: `${PROJECT_RULE_PREFIX}${name}`,
|
|
141
|
+
name,
|
|
142
|
+
queries,
|
|
143
|
+
sources,
|
|
144
|
+
message: message.trim(),
|
|
145
|
+
hint: hint.trim(),
|
|
146
|
+
severity: declared,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const RULE_KEYS = new Set(['query', 'message', 'hint', 'severity']);
|
|
150
|
+
/**
|
|
151
|
+
* Why a template would not render.
|
|
152
|
+
*
|
|
153
|
+
* Checked here rather than tolerated at render time, because both failures are
|
|
154
|
+
* silent where it matters: an out-of-range index leaves `{3}` sitting in a
|
|
155
|
+
* sentence a human has to act on, and a misspelled attribute renders as nothing
|
|
156
|
+
* at all, which reads as a finding about an unnamed document.
|
|
157
|
+
*/
|
|
158
|
+
function placeholderProblems(template, arity, where) {
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const match of template.matchAll(PLACEHOLDER)) {
|
|
161
|
+
const index = Number(match[1]);
|
|
162
|
+
if (index >= arity) {
|
|
163
|
+
out.push(`${where}: "${match[0]}" names step ${index}, and the query has ${arity} ${arity === 1 ? 'step' : 'steps'} (0 to ${arity - 1})`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const key = match[2];
|
|
167
|
+
if (key === undefined)
|
|
168
|
+
continue;
|
|
169
|
+
const folded = key.toLowerCase();
|
|
170
|
+
if (folded.startsWith('fm.') || SELECTOR_KEYS.includes(folded))
|
|
171
|
+
continue;
|
|
172
|
+
out.push(`${where}: "${match[0]}" asks for an attribute nothing has - known: ${SELECTOR_KEYS.join(', ')}, fm.*`);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Fills a validated template in from one path through the graph.
|
|
178
|
+
*
|
|
179
|
+
* Sharing one global regex between this and {@link placeholderProblems} is safe
|
|
180
|
+
* and not an oversight: `matchAll` works on a clone, and `replace` resets
|
|
181
|
+
* `lastIndex` on entry and on exit. The hazard `CLAUDE.md` warns about is a
|
|
182
|
+
* `/g` regex whose cursor survives a call, and neither of these keeps one.
|
|
183
|
+
*
|
|
184
|
+
* The index cannot be out of range when `execute` produced the match - a path is
|
|
185
|
+
* always one node longer than the query has steps, which is what validation
|
|
186
|
+
* checks against. The guard is for the exported case: this takes any `Match`,
|
|
187
|
+
* and a caller assembling one by hand gets the placeholder back rather than a
|
|
188
|
+
* crash.
|
|
189
|
+
*/
|
|
190
|
+
export function renderTemplate(template, match, graph) {
|
|
191
|
+
return template.replace(PLACEHOLDER, (whole, index, key) => {
|
|
192
|
+
const node = match.nodes[Number(index)];
|
|
193
|
+
if (node === undefined)
|
|
194
|
+
return whole;
|
|
195
|
+
return attributesOf(node, (key ?? 'id').toLowerCase(), graph)[0] ?? whole;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function isRecord(value) {
|
|
199
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=project-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-rules.js","sourceRoot":"","sources":["../src/project-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Rules a repository writes for itself.\n *\n * [ADR-0005](../docs/adr/0005-rules-are-queries.md) claimed the selector\n * language was expressive enough that a team could express a convention\n * spec-graph never anticipated. For three releases that claim was only true at a\n * command prompt: `spec-graph query` could find the thing, and nothing could\n * make finding it fail a build. Three ADRs recorded the same gap and the same\n * reason - a query needs a severity and a sentence before it is a rule.\n *\n * This is the sentence. A project rule is a selector, a message, a hint and a\n * severity, written in `.spec-graph.json` and run beside the built-ins:\n *\n * ```json\n * \"rules\": {\n * \"no-draft-dependency\": {\n * \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n * \"message\": \"{0} depends on {1}, which is still a draft\",\n * \"hint\": \"wait for {1} to be accepted, or drop the dependency\"\n * }\n * }\n * ```\n *\n * Everything here is a pure function of the parsed configuration. Reading the\n * file stays in `config.ts`, for the same reason extraction never touches a\n * disk.\n */\n\nimport { attributesOf, parseQuery, QueryError, SELECTOR_KEYS, type Match, type QuerySpec } from './select.js';\nimport type { SpecGraph } from './graph.js';\nimport type { ProjectRuleId, Severity } from './types.js';\nimport { PROJECT_RULE_PREFIX } from './types.js';\n\n/* -------------------------------------------------------------------------- */\n/* Shapes */\n/* -------------------------------------------------------------------------- */\n\n/** A project rule, compiled and known to be runnable. */\nexport interface ProjectRule {\n /** Namespaced, so it can never collide with a built-in. */\n readonly id: ProjectRuleId;\n /** The name as the repository wrote it, for error messages. */\n readonly name: string;\n /**\n * The selectors, in the order they were written.\n *\n * More than one because a convention is often a union - \"no live document may\n * depend on a draft *or* on a record\" - and a union of queries is clearer than\n * a disjunction inside the grammar. See ADR-0005.\n */\n readonly queries: readonly QuerySpec[];\n /**\n * The selectors as written, kept beside the compiled form.\n *\n * A report has to describe the rule, and the only honest description of a\n * project rule is the query behind it. Echoing the text back is exact and\n * free; rendering a `QuerySpec` would be a second grammar to keep correct.\n */\n readonly sources: readonly string[];\n readonly message: string;\n readonly hint: string;\n /**\n * What the repository declared, before `--rule` and `--strict` have their say.\n *\n * Defaults to `warn` rather than to `error`. A rule a team has just written\n * has not yet earned the right to stop their build, and the first run of a new\n * convention is exactly when it is most likely to be wrong.\n */\n readonly severity: Severity;\n}\n\nexport interface CompiledProjectRules {\n readonly rules: readonly ProjectRule[];\n readonly problems: readonly string[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Compilation */\n/* -------------------------------------------------------------------------- */\n\n/**\n * What a name may contain.\n *\n * No colon, because the namespace separator has to stay unambiguous. No\n * whitespace, because the id is printed in a column of a report. And no tab in\n * particular: a baseline key is three fields joined by one, and a rule id that\n * could contain a tab would be a rule whose accepted debt could be forged by\n * typing carefully. See ADR-0012.\n */\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nconst MAX_NAME_LENGTH = 64;\n\n/**\n * `{0}`, `{1.phase}`, `{2.fm.owner}`.\n *\n * Positional because a path is positional: step zero is where the finding\n * starts and the last step is where it ends. Naming them would mean inventing a\n * second vocabulary for something the selector already numbers.\n */\nconst PLACEHOLDER = /\\{(\\d+)(?:\\.([A-Za-z][A-Za-z0-9_.:-]*))?\\}/g;\n\n/**\n * Turns the `rules` section of a configuration into runnable rules.\n *\n * Every failure is collected rather than thrown. A repository whose eighth\n * custom rule has a typo in it should still get the findings from the other\n * seven, and should be told exactly which one stopped working - a configuration\n * that silently does less than it says is the failure mode this whole file\n * exists to avoid.\n */\nexport function compileProjectRules(raw: unknown, source: string): CompiledProjectRules {\n const problems: string[] = [];\n if (!isRecord(raw)) {\n return { rules: [], problems: [`${source}: \"rules\" must be an object of name to rule`] };\n }\n\n const rules: ProjectRule[] = [];\n for (const [name, body] of Object.entries(raw)) {\n const rule = compileOne(name, body, source, problems);\n if (rule !== null) rules.push(rule);\n }\n // Sorted by id, so two runs of one configuration produce findings in the same\n // order however the JSON was keyed. Two ids are never equal - they are object\n // keys - so there is no third case to write, and `<` against `<=` here is a\n // genuinely equivalent mutant with no test behind it for that reason.\n return { rules: rules.sort((a, b) => (a.id < b.id ? -1 : 1)), problems };\n}\n\nfunction compileOne(\n name: string,\n body: unknown,\n source: string,\n problems: string[],\n): ProjectRule | null {\n const where = `${source}: rules.${name}`;\n if (!NAME.test(name) || name.length > MAX_NAME_LENGTH) {\n problems.push(\n `${where}: a rule name must be letters, digits, \".\" \"_\" or \"-\", at most ${MAX_NAME_LENGTH} characters`,\n );\n return null;\n }\n if (!isRecord(body)) {\n problems.push(`${where} must be an object with \"query\" and \"message\"`);\n return null;\n }\n\n for (const key of Object.keys(body)) {\n if (!RULE_KEYS.has(key)) problems.push(`${where}: unknown key \"${key}\"`);\n }\n\n const written = body['query'];\n const listed: unknown[] = typeof written === 'string' ? [written] : Array.isArray(written) ? written : [];\n if (listed.length === 0 || listed.some((entry) => typeof entry !== 'string')) {\n problems.push(`${where}: \"query\" must be a selector, or an array of them`);\n return null;\n }\n const sources = listed as string[];\n\n const queries: QuerySpec[] = [];\n for (const selector of sources) {\n try {\n queries.push(parseQuery(selector));\n } catch (error) {\n const detail = error instanceof QueryError ? `${error.message} (at character ${error.offset + 1})` : String(error);\n problems.push(`${where}: ${detail}`);\n return null;\n }\n }\n\n const message = body['message'];\n if (typeof message !== 'string' || message.trim().length === 0) {\n problems.push(`${where}: \"message\" must be the one-line headline of the finding`);\n return null;\n }\n\n // A finding has to name a next action, so the fallback names the only thing\n // this file can be certain of: where the rule was written down.\n const hint = body['hint'] ?? `check this against rules.${name} in ${source}`;\n if (typeof hint !== 'string' || hint.trim().length === 0) {\n problems.push(`${where}: \"hint\" must be a concrete next action`);\n return null;\n }\n\n const declared = body['severity'] ?? 'warn';\n if (declared !== 'error' && declared !== 'warn' && declared !== 'info' && declared !== 'off') {\n problems.push(`${where}: \"severity\" must be error, warn, info or off`);\n return null;\n }\n\n // The shortest path a rule can produce, because a placeholder has to be\n // resolvable by *every* query behind the rule and not merely by the longest.\n const arity = Math.min(...queries.map((query) => query.steps.length + 1));\n let usable = true;\n for (const text of [message, hint]) {\n for (const problem of placeholderProblems(text, arity, where)) {\n problems.push(problem);\n usable = false;\n }\n }\n if (!usable) return null;\n\n return {\n id: `${PROJECT_RULE_PREFIX}${name}`,\n name,\n queries,\n sources,\n message: message.trim(),\n hint: hint.trim(),\n severity: declared,\n };\n}\n\nconst RULE_KEYS: ReadonlySet<string> = new Set(['query', 'message', 'hint', 'severity']);\n\n/**\n * Why a template would not render.\n *\n * Checked here rather than tolerated at render time, because both failures are\n * silent where it matters: an out-of-range index leaves `{3}` sitting in a\n * sentence a human has to act on, and a misspelled attribute renders as nothing\n * at all, which reads as a finding about an unnamed document.\n */\nfunction placeholderProblems(template: string, arity: number, where: string): string[] {\n const out: string[] = [];\n for (const match of template.matchAll(PLACEHOLDER)) {\n const index = Number(match[1]);\n if (index >= arity) {\n out.push(\n `${where}: \"${match[0]}\" names step ${index}, and the query has ${arity} ${arity === 1 ? 'step' : 'steps'} (0 to ${arity - 1})`,\n );\n continue;\n }\n const key = match[2];\n if (key === undefined) continue;\n const folded = key.toLowerCase();\n if (folded.startsWith('fm.') || SELECTOR_KEYS.includes(folded)) continue;\n out.push(`${where}: \"${match[0]}\" asks for an attribute nothing has - known: ${SELECTOR_KEYS.join(', ')}, fm.*`);\n }\n return out;\n}\n\n/**\n * Fills a validated template in from one path through the graph.\n *\n * Sharing one global regex between this and {@link placeholderProblems} is safe\n * and not an oversight: `matchAll` works on a clone, and `replace` resets\n * `lastIndex` on entry and on exit. The hazard `CLAUDE.md` warns about is a\n * `/g` regex whose cursor survives a call, and neither of these keeps one.\n *\n * The index cannot be out of range when `execute` produced the match - a path is\n * always one node longer than the query has steps, which is what validation\n * checks against. The guard is for the exported case: this takes any `Match`,\n * and a caller assembling one by hand gets the placeholder back rather than a\n * crash.\n */\nexport function renderTemplate(template: string, match: Match, graph: SpecGraph): string {\n return template.replace(PLACEHOLDER, (whole, index: string, key: string | undefined) => {\n const node = match.nodes[Number(index)];\n if (node === undefined) return whole;\n return attributesOf(node, (key ?? 'id').toLowerCase(), graph)[0] ?? whole;\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|