@descent-vtt/spec-graph 0.2.3 → 0.3.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 +102 -0
- package/README.md +87 -2
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +34 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/extract.d.ts +49 -1
- package/dist/extract.js +156 -8
- package/dist/extract.js.map +1 -1
- package/dist/identity.js +14 -5
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +16 -2
- package/dist/markdown.js +89 -22
- package/dist/markdown.js.map +1 -1
- package/dist/report.d.ts +21 -4
- package/dist/report.js +123 -7
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +2 -1
- package/dist/resolve.js +32 -1
- package/dist/resolve.js.map +1 -1
- package/dist/rules.js +30 -0
- package/dist/rules.js.map +1 -1
- package/dist/types.d.ts +23 -3
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -106,6 +106,12 @@ function readFields(raw, source) {
|
|
|
106
106
|
else
|
|
107
107
|
config.strict = raw['strict'];
|
|
108
108
|
}
|
|
109
|
+
if (raw['ratchet'] !== undefined) {
|
|
110
|
+
if (typeof raw['ratchet'] !== 'boolean')
|
|
111
|
+
problems.push(`${source}: "ratchet" must be true or false`);
|
|
112
|
+
else
|
|
113
|
+
config.ratchet = raw['ratchet'];
|
|
114
|
+
}
|
|
109
115
|
if (raw['maxRelated'] !== undefined) {
|
|
110
116
|
const value = raw['maxRelated'];
|
|
111
117
|
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
@@ -150,6 +156,7 @@ const KNOWN_KEYS = new Set([
|
|
|
150
156
|
'ignoreFamilies',
|
|
151
157
|
'historyPatterns',
|
|
152
158
|
'baseline',
|
|
159
|
+
'ratchet',
|
|
153
160
|
'severities',
|
|
154
161
|
'strict',
|
|
155
162
|
'maxRelated',
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,YAAY,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC3D,kBAAkB;IAClB,wBAAwB;CACzB,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAqC/C,MAAM,KAAK,GAAiB,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAEvE;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAI,GAA6B,WAAW;IACnF,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;QAC7C,IAAI,GAAG,KAAK,IAAI;YAAE,SAAS;QAC3B,OAAO,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAChC,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,eAAe,CAAC,CAAC;IAClD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC/B,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,MAAM,OAAO,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAC3C,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,IAAI,kBAAkB,qBAAqB,CAAC,EAAE,CAAC;IACzG,CAAC;IACD,OAAO,UAAU,CAAC,OAAO,EAAE,gBAAgB,kBAAkB,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,MAAc;IACrD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,uBAAwB,KAAe,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;IACxG,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,6BAA6B,CAAC,EAAE,CAAC;IACzG,OAAO,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,UAAU,CAAC,GAA4B,EAAE,MAAc;IAC9D,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,MAAM,GAER,EAAE,CAAC;IAEP,MAAM,OAAO,GAAG,CAAC,GAA0B,EAAiC,EAAE;QAC5E,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC1C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,EAAE,CAAC;YAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,MAAM,GAAG,+BAA+B,CAAC,CAAC;YACjE,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,KAAiB,CAAC;IAC3B,CAAC,CAAC;IAEF,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,CAAC,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACtD,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,CAAC,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClD,MAAM,CAAC,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAEpD,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,OAAO,GAAG,CAAC,UAAU,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/E,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,6BAA6B,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;QAChC,IAAI,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,kCAAkC,CAAC,CAAC;;YAC9F,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;IAED,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACvE,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,oDAAoD,CAAC,CAAC;QAC/E,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACrB,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,sDAAsD,CAAC,CAAC;QACjF,CAAC;aAAM,CAAC;YACN,MAAM,UAAU,GAAsC,EAAE,CAAC;YACzD,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAc,CAAC,EAAE,CAAC;oBACvC,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,mBAAmB,IAAI,mBAAmB,CAAC,CAAC;oBACnE,SAAS;gBACX,CAAC;gBACD,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;oBACjF,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,iBAAiB,IAAI,oCAAoC,CAAC,CAAC;oBAClF,SAAS;gBACX,CAAC;gBACD,UAAU,CAAC,IAAc,CAAC,GAAG,KAAK,CAAC;YACrC,CAAC;YACD,MAAM,CAAC,UAAU,GAAG,UAAU,CAAC;QACjC,CAAC;IACH,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,kBAAkB,GAAG,GAAG,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC;IAC9C,UAAU;IACV,QAAQ;IACR,kBAAkB;IAClB,UAAU;IACV,gBAAgB;IAChB,iBAAiB;IACjB,UAAU;IACV,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,4EAA4E;IAC5E,SAAS;CACV,CAAC,CAAC;AAEH,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,OAAO,CAAC,IAA8B,EAAE,IAAY;IAC3D,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * Repository configuration.\n *\n * Three ADRs left the same open question: `--ignore-ref` patterns, include\n * globs and rule severities all belong to a repository rather than to whoever\n * typed the command, and an npm script was doing that job by accumulating\n * flags. A CI invocation carrying eight `--ignore-ref` arguments is a\n * configuration file that has not admitted what it is.\n *\n * The format is JSON, parsed with `JSON.parse`. That is not a shortcut - it is\n * the reason this file exists at all. spec-graph has no runtime dependencies,\n * and a YAML or TOML reader would be the largest thing in the package, to read\n * a file with nine keys in it.\n *\n * Every key mirrors a flag, and a flag always wins. Configuration is where a\n * team writes down what is true of the repository; a flag is where somebody\n * overrides it for one run.\n */\n\nimport { readFileSync } from 'node:fs';\n\nimport { RULE_IDS } from './rules.js';\nimport type { RuleId, Severity } from './types.js';\n\n/** File names looked for, in order. The first that exists is the one used. */\nexport const CONFIG_FILES: readonly string[] = Object.freeze([\n '.spec-graph.json',\n 'spec-graph.config.json',\n]);\n\n/** The `package.json` key read when no config file is present. */\nexport const CONFIG_PACKAGE_KEY = 'spec-graph';\n\nexport interface SpecGraphConfig {\n readonly patterns?: readonly string[] | undefined;\n readonly ignore?: readonly string[] | undefined;\n readonly ignoreReferences?: readonly string[] | undefined;\n /**\n * Families a bare identifier in prose may name.\n *\n * When set, `ADR-0099` is only ever read as a citation if `ADR` is listed.\n * Everything else in prose stays prose.\n */\n readonly families?: readonly string[] | undefined;\n /** Families that are never citations, whatever else the corpus contains. */\n readonly ignoreFamilies?: readonly string[] | undefined;\n /**\n * Files that are logs of what was decided rather than decisions themselves.\n *\n * Journals, changelogs, minutes. Their links are still checked; their\n * obligations and lifecycle are not. See ADR-0011.\n */\n readonly historyPatterns?: readonly string[] | undefined;\n /** Path to the accepted-debt baseline, relative to the root. See ADR-0012. */\n readonly baseline?: string | undefined;\n readonly severities?: Partial<Record<RuleId, Severity>> | undefined;\n readonly strict?: boolean | undefined;\n readonly maxRelated?: number | undefined;\n}\n\nexport interface LoadedConfig {\n readonly config: SpecGraphConfig;\n /** Where it came from, for `--verbose` and for error messages. */\n readonly source: string | null;\n /** Keys that were present but unusable. Reported, never silently dropped. */\n readonly problems: readonly string[];\n}\n\nconst EMPTY: LoadedConfig = { config: {}, source: null, problems: [] };\n\n/**\n * Reads configuration from a directory.\n *\n * Never throws. A malformed config is reported as a problem and the run\n * continues with defaults: a broken config file should not stop a team seeing\n * the findings it was going to show them anyway.\n */\nexport function loadConfig(root: string, read: (path: string) => string = defaultRead): LoadedConfig {\n for (const name of CONFIG_FILES) {\n const raw = tryRead(read, `${root}/${name}`);\n if (raw === null) continue;\n return parseConfig(raw, name);\n }\n\n const pkg = tryRead(read, `${root}/package.json`);\n if (pkg === null) return EMPTY;\n let parsed: unknown;\n try {\n parsed = JSON.parse(pkg);\n } catch {\n return EMPTY;\n }\n if (!isRecord(parsed)) return EMPTY;\n const section = parsed[CONFIG_PACKAGE_KEY];\n if (section === undefined) return EMPTY;\n if (!isRecord(section)) {\n return { config: {}, source: `package.json`, problems: [`\"${CONFIG_PACKAGE_KEY}\" must be an object`] };\n }\n return readFields(section, `package.json#${CONFIG_PACKAGE_KEY}`);\n}\n\n/** Parses config text. Exported so the shape can be tested without a disk. */\nexport function parseConfig(raw: string, source: string): LoadedConfig {\n let parsed: unknown;\n try {\n parsed = JSON.parse(withoutBom(raw));\n } catch (error) {\n return { config: {}, source, problems: [`${source} is not valid JSON: ${(error as Error).message}`] };\n }\n if (!isRecord(parsed)) return { config: {}, source, problems: [`${source} must contain a JSON object`] };\n return readFields(parsed, source);\n}\n\nfunction readFields(raw: Record<string, unknown>, source: string): LoadedConfig {\n const problems: string[] = [];\n const config: {\n -readonly [K in keyof SpecGraphConfig]: SpecGraphConfig[K];\n } = {};\n\n const strings = (key: keyof SpecGraphConfig): readonly string[] | undefined => {\n const value = raw[key];\n if (value === undefined) return undefined;\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {\n problems.push(`${source}: \"${key}\" must be an array of strings`);\n return undefined;\n }\n return value as string[];\n };\n\n config.patterns = strings('patterns');\n config.ignore = strings('ignore');\n config.ignoreReferences = strings('ignoreReferences');\n config.families = strings('families');\n config.ignoreFamilies = strings('ignoreFamilies');\n config.historyPatterns = strings('historyPatterns');\n\n if (raw['baseline'] !== undefined) {\n if (typeof raw['baseline'] !== 'string' || raw['baseline'].trim().length === 0) {\n problems.push(`${source}: \"baseline\" must be a path`);\n } else {\n config.baseline = raw['baseline'];\n }\n }\n\n if (raw['strict'] !== undefined) {\n if (typeof raw['strict'] !== 'boolean') problems.push(`${source}: \"strict\" must be true or false`);\n else config.strict = raw['strict'];\n }\n\n if (raw['maxRelated'] !== undefined) {\n const value = raw['maxRelated'];\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n problems.push(`${source}: \"maxRelated\" must be a non-negative whole number`);\n } else {\n config.maxRelated = value;\n }\n }\n\n if (raw['severities'] !== undefined) {\n const value = raw['severities'];\n if (!isRecord(value)) {\n problems.push(`${source}: \"severities\" must be an object of rule to severity`);\n } else {\n const severities: Partial<Record<RuleId, Severity>> = {};\n for (const [rule, level] of Object.entries(value)) {\n if (!RULE_IDS.includes(rule as RuleId)) {\n problems.push(`${source}: unknown rule \"${rule}\" in \"severities\"`);\n continue;\n }\n if (level !== 'error' && level !== 'warn' && level !== 'info' && level !== 'off') {\n problems.push(`${source}: \"severities.${rule}\" must be error, warn, info or off`);\n continue;\n }\n severities[rule as RuleId] = level;\n }\n config.severities = severities;\n }\n }\n\n for (const key of Object.keys(raw)) {\n if (!KNOWN_KEYS.has(key)) problems.push(`${source}: unknown key \"${key}\"`);\n }\n\n return { config, source, problems };\n}\n\nconst KNOWN_KEYS: ReadonlySet<string> = new Set([\n 'patterns',\n 'ignore',\n 'ignoreReferences',\n 'families',\n 'ignoreFamilies',\n 'historyPatterns',\n 'baseline',\n 'severities',\n 'strict',\n 'maxRelated',\n // Tolerated so an editor can be pointed at a schema without being told off.\n '$schema',\n]);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction tryRead(read: (path: string) => string, path: string): string | null {\n try {\n return withoutBom(read(path));\n } catch {\n return null;\n }\n}\n\n/**\n * Drops a leading byte-order mark.\n *\n * `JSON.parse` rejects one, and several Windows editors write one by default -\n * a config that silently stops applying because of an invisible first character\n * is the worst kind of configuration bug.\n */\nfunction withoutBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nfunction defaultRead(path: string): string {\n return readFileSync(path, 'utf8');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Repository configuration.\n *\n * Three ADRs left the same open question: `--ignore-ref` patterns, include\n * globs and rule severities all belong to a repository rather than to whoever\n * typed the command, and an npm script was doing that job by accumulating\n * flags. A CI invocation carrying eight `--ignore-ref` arguments is a\n * configuration file that has not admitted what it is.\n *\n * The format is JSON, parsed with `JSON.parse`. That is not a shortcut - it is\n * the reason this file exists at all. spec-graph has no runtime dependencies,\n * and a YAML or TOML reader would be the largest thing in the package, to read\n * a file with nine keys in it.\n *\n * Every key mirrors a flag, and a flag always wins. Configuration is where a\n * team writes down what is true of the repository; a flag is where somebody\n * overrides it for one run.\n */\n\nimport { readFileSync } from 'node:fs';\n\nimport { RULE_IDS } from './rules.js';\nimport type { RuleId, Severity } from './types.js';\n\n/** File names looked for, in order. The first that exists is the one used. */\nexport const CONFIG_FILES: readonly string[] = Object.freeze([\n '.spec-graph.json',\n 'spec-graph.config.json',\n]);\n\n/** The `package.json` key read when no config file is present. */\nexport const CONFIG_PACKAGE_KEY = 'spec-graph';\n\nexport interface SpecGraphConfig {\n readonly patterns?: readonly string[] | undefined;\n readonly ignore?: readonly string[] | undefined;\n readonly ignoreReferences?: readonly string[] | undefined;\n /**\n * Families a bare identifier in prose may name.\n *\n * When set, `ADR-0099` is only ever read as a citation if `ADR` is listed.\n * Everything else in prose stays prose.\n */\n readonly families?: readonly string[] | undefined;\n /** Families that are never citations, whatever else the corpus contains. */\n readonly ignoreFamilies?: readonly string[] | undefined;\n /**\n * Files that are logs of what was decided rather than decisions themselves.\n *\n * Journals, changelogs, minutes. Their links are still checked; their\n * obligations and lifecycle are not. See ADR-0011.\n */\n readonly historyPatterns?: readonly string[] | undefined;\n /** Path to the accepted-debt baseline, relative to the root. See ADR-0012. */\n readonly baseline?: string | undefined;\n /** Fail when a baseline entry no longer occurs. See ADR-0012. */\n readonly ratchet?: boolean | undefined;\n readonly severities?: Partial<Record<RuleId, Severity>> | undefined;\n readonly strict?: boolean | undefined;\n readonly maxRelated?: number | undefined;\n}\n\nexport interface LoadedConfig {\n readonly config: SpecGraphConfig;\n /** Where it came from, for `--verbose` and for error messages. */\n readonly source: string | null;\n /** Keys that were present but unusable. Reported, never silently dropped. */\n readonly problems: readonly string[];\n}\n\nconst EMPTY: LoadedConfig = { config: {}, source: null, problems: [] };\n\n/**\n * Reads configuration from a directory.\n *\n * Never throws. A malformed config is reported as a problem and the run\n * continues with defaults: a broken config file should not stop a team seeing\n * the findings it was going to show them anyway.\n */\nexport function loadConfig(root: string, read: (path: string) => string = defaultRead): LoadedConfig {\n for (const name of CONFIG_FILES) {\n const raw = tryRead(read, `${root}/${name}`);\n if (raw === null) continue;\n return parseConfig(raw, name);\n }\n\n const pkg = tryRead(read, `${root}/package.json`);\n if (pkg === null) return EMPTY;\n let parsed: unknown;\n try {\n parsed = JSON.parse(pkg);\n } catch {\n return EMPTY;\n }\n if (!isRecord(parsed)) return EMPTY;\n const section = parsed[CONFIG_PACKAGE_KEY];\n if (section === undefined) return EMPTY;\n if (!isRecord(section)) {\n return { config: {}, source: `package.json`, problems: [`\"${CONFIG_PACKAGE_KEY}\" must be an object`] };\n }\n return readFields(section, `package.json#${CONFIG_PACKAGE_KEY}`);\n}\n\n/** Parses config text. Exported so the shape can be tested without a disk. */\nexport function parseConfig(raw: string, source: string): LoadedConfig {\n let parsed: unknown;\n try {\n parsed = JSON.parse(withoutBom(raw));\n } catch (error) {\n return { config: {}, source, problems: [`${source} is not valid JSON: ${(error as Error).message}`] };\n }\n if (!isRecord(parsed)) return { config: {}, source, problems: [`${source} must contain a JSON object`] };\n return readFields(parsed, source);\n}\n\nfunction readFields(raw: Record<string, unknown>, source: string): LoadedConfig {\n const problems: string[] = [];\n const config: {\n -readonly [K in keyof SpecGraphConfig]: SpecGraphConfig[K];\n } = {};\n\n const strings = (key: keyof SpecGraphConfig): readonly string[] | undefined => {\n const value = raw[key];\n if (value === undefined) return undefined;\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {\n problems.push(`${source}: \"${key}\" must be an array of strings`);\n return undefined;\n }\n return value as string[];\n };\n\n config.patterns = strings('patterns');\n config.ignore = strings('ignore');\n config.ignoreReferences = strings('ignoreReferences');\n config.families = strings('families');\n config.ignoreFamilies = strings('ignoreFamilies');\n config.historyPatterns = strings('historyPatterns');\n\n if (raw['baseline'] !== undefined) {\n if (typeof raw['baseline'] !== 'string' || raw['baseline'].trim().length === 0) {\n problems.push(`${source}: \"baseline\" must be a path`);\n } else {\n config.baseline = raw['baseline'];\n }\n }\n\n if (raw['strict'] !== undefined) {\n if (typeof raw['strict'] !== 'boolean') problems.push(`${source}: \"strict\" must be true or false`);\n else config.strict = raw['strict'];\n }\n\n if (raw['ratchet'] !== undefined) {\n if (typeof raw['ratchet'] !== 'boolean') problems.push(`${source}: \"ratchet\" must be true or false`);\n else config.ratchet = raw['ratchet'];\n }\n\n if (raw['maxRelated'] !== undefined) {\n const value = raw['maxRelated'];\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n problems.push(`${source}: \"maxRelated\" must be a non-negative whole number`);\n } else {\n config.maxRelated = value;\n }\n }\n\n if (raw['severities'] !== undefined) {\n const value = raw['severities'];\n if (!isRecord(value)) {\n problems.push(`${source}: \"severities\" must be an object of rule to severity`);\n } else {\n const severities: Partial<Record<RuleId, Severity>> = {};\n for (const [rule, level] of Object.entries(value)) {\n if (!RULE_IDS.includes(rule as RuleId)) {\n problems.push(`${source}: unknown rule \"${rule}\" in \"severities\"`);\n continue;\n }\n if (level !== 'error' && level !== 'warn' && level !== 'info' && level !== 'off') {\n problems.push(`${source}: \"severities.${rule}\" must be error, warn, info or off`);\n continue;\n }\n severities[rule as RuleId] = level;\n }\n config.severities = severities;\n }\n }\n\n for (const key of Object.keys(raw)) {\n if (!KNOWN_KEYS.has(key)) problems.push(`${source}: unknown key \"${key}\"`);\n }\n\n return { config, source, problems };\n}\n\nconst KNOWN_KEYS: ReadonlySet<string> = new Set([\n 'patterns',\n 'ignore',\n 'ignoreReferences',\n 'families',\n 'ignoreFamilies',\n 'historyPatterns',\n 'baseline',\n 'ratchet',\n 'severities',\n 'strict',\n 'maxRelated',\n // Tolerated so an editor can be pointed at a schema without being told off.\n '$schema',\n]);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction tryRead(read: (path: string) => string, path: string): string | null {\n try {\n return withoutBom(read(path));\n } catch {\n return null;\n }\n}\n\n/**\n * Drops a leading byte-order mark.\n *\n * `JSON.parse` rejects one, and several Windows editors write one by default -\n * a config that silently stops applying because of an invisible first character\n * is the worst kind of configuration bug.\n */\nfunction withoutBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nfunction defaultRead(path: string): string {\n return readFileSync(path, 'utf8');\n}\n"]}
|
package/dist/extract.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type DocumentIdentity } from './identity.js';
|
|
18
18
|
import { type ScannedDocument } from './markdown.js';
|
|
19
|
-
import type { DocumentNode, EdgeKind, EdgeOrigin, ItemNode, ParseProblem, SourceRef } from './types.js';
|
|
19
|
+
import type { DocumentNode, EdgeKind, EdgeOrigin, ItemNode, MisreadKey, ParseProblem, SourceRef } from './types.js';
|
|
20
20
|
/**
|
|
21
21
|
* A relation that has been read but not yet resolved to a target node.
|
|
22
22
|
*
|
|
@@ -50,6 +50,13 @@ export interface ExtractedDocument {
|
|
|
50
50
|
readonly items: readonly ItemNode[];
|
|
51
51
|
readonly references: readonly ReferenceCandidate[];
|
|
52
52
|
readonly problems: readonly ParseProblem[];
|
|
53
|
+
/**
|
|
54
|
+
* Front-matter keys that read as relations and declared none.
|
|
55
|
+
*
|
|
56
|
+
* Kept apart from `problems` because a problem is about the text and this is
|
|
57
|
+
* about the graph: the edge the author wrote down is not in it.
|
|
58
|
+
*/
|
|
59
|
+
readonly misreadKeys: readonly MisreadKey[];
|
|
53
60
|
/** Heading slugs, for resolving `#anchor` references into this document. */
|
|
54
61
|
readonly anchors: ReadonlySet<string>;
|
|
55
62
|
readonly identity: DocumentIdentity;
|
|
@@ -70,6 +77,47 @@ export interface ExtractedDocument {
|
|
|
70
77
|
*/
|
|
71
78
|
readonly containerId: string | null;
|
|
72
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Front-matter keys that declare a relation, and what they mean.
|
|
82
|
+
*
|
|
83
|
+
* Every directional kind is spelled in **both** directions. A register that
|
|
84
|
+
* records `depends-on` but not `depended-on-by` is not a smaller vocabulary, it
|
|
85
|
+
* is a trap: the author writes the inverse, the key means nothing, and the edge
|
|
86
|
+
* they declared is missing from a graph that reports itself as consistent. Which
|
|
87
|
+
* half of a pair a repository writes is a filing convention, and a filing
|
|
88
|
+
* convention is not something a linter gets to have an opinion about.
|
|
89
|
+
*
|
|
90
|
+
* `relates-to` is the exception, and it is not one: the relation is symmetric,
|
|
91
|
+
* so it has no other direction to spell. `contains` is structural and never
|
|
92
|
+
* written by hand. `tests/parsing.test.ts` holds both facts to the fire.
|
|
93
|
+
*/
|
|
94
|
+
export declare const RELATION_KEYS: Readonly<Record<string, {
|
|
95
|
+
kind: EdgeKind;
|
|
96
|
+
inverted: boolean;
|
|
97
|
+
}>>;
|
|
98
|
+
/**
|
|
99
|
+
* True when two folded keys are one edit apart.
|
|
100
|
+
*
|
|
101
|
+
* One edit, not two, and a transposition counts as one: `supercedesby` for
|
|
102
|
+
* `supercededby` is a slip of the finger, `categories` for `dependencies` is a
|
|
103
|
+
* different word. Written out rather than as a distance matrix because the
|
|
104
|
+
* answer is a yes or a no and the bound is one - the general algorithm would be
|
|
105
|
+
* more code, not less.
|
|
106
|
+
*/
|
|
107
|
+
export declare function withinOneEdit(a: string, b: string): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Folds a front-matter key to its lookup form.
|
|
110
|
+
*
|
|
111
|
+
* `depends-on`, `depends_on`, `dependsOn` and `Depends On` are one key written
|
|
112
|
+
* four ways, and which one a repository uses is decided by whatever wrote the
|
|
113
|
+
* front matter first. Dropping every separator collapses them - the YAML reader
|
|
114
|
+
* has already lower-cased the key, which is what makes camel case fold too.
|
|
115
|
+
*
|
|
116
|
+
* Which case this folds to is arbitrary and no test pins it: both sides of every
|
|
117
|
+
* comparison come through here, so upper would work exactly as well. Asserting
|
|
118
|
+
* on it would be asserting on the implementation.
|
|
119
|
+
*/
|
|
120
|
+
export declare function foldRelationKey(key: string): string;
|
|
73
121
|
/** Headings under which a link is bookkeeping rather than a commitment. */
|
|
74
122
|
declare const WEAK_SECTIONS: ReadonlySet<string>;
|
|
75
123
|
/** Headings whose bullets are obligations even without a checkbox. */
|
package/dist/extract.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* broken reference to specification 256.
|
|
16
16
|
*/
|
|
17
17
|
import { attr, attrList, directiveFor, parseDirectives } from './directives.js';
|
|
18
|
-
import { identify, isExternal, normaliseRef, ID_KEYS } from './identity.js';
|
|
18
|
+
import { identify, isDocumentTarget, isExternal, looksLikePath, normaliseRef, parsePrefixedRef, ID_KEYS, } from './identity.js';
|
|
19
19
|
import { isStatusHeading, phaseFromPath, phaseOf, STATUS_KEYS, supersessionTargetsIn } from './lifecycle.js';
|
|
20
20
|
import { scanMarkdown, slugify } from './markdown.js';
|
|
21
21
|
import { findSpecificationRegions, regionAt } from './sections.js';
|
|
@@ -25,34 +25,176 @@ import { parseFrontMatter, toRecord, valuesOf } from './yaml.js';
|
|
|
25
25
|
/* -------------------------------------------------------------------------- */
|
|
26
26
|
/* Vocabulary */
|
|
27
27
|
/* -------------------------------------------------------------------------- */
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Front-matter keys that declare a relation, and what they mean.
|
|
30
|
+
*
|
|
31
|
+
* Every directional kind is spelled in **both** directions. A register that
|
|
32
|
+
* records `depends-on` but not `depended-on-by` is not a smaller vocabulary, it
|
|
33
|
+
* is a trap: the author writes the inverse, the key means nothing, and the edge
|
|
34
|
+
* they declared is missing from a graph that reports itself as consistent. Which
|
|
35
|
+
* half of a pair a repository writes is a filing convention, and a filing
|
|
36
|
+
* convention is not something a linter gets to have an opinion about.
|
|
37
|
+
*
|
|
38
|
+
* `relates-to` is the exception, and it is not one: the relation is symmetric,
|
|
39
|
+
* so it has no other direction to spell. `contains` is structural and never
|
|
40
|
+
* written by hand. `tests/parsing.test.ts` holds both facts to the fire.
|
|
41
|
+
*/
|
|
42
|
+
export const RELATION_KEYS = {
|
|
30
43
|
supersedes: { kind: 'supersedes', inverted: false },
|
|
31
44
|
supercedes: { kind: 'supersedes', inverted: false },
|
|
32
45
|
replaces: { kind: 'supersedes', inverted: false },
|
|
33
46
|
obsoletes: { kind: 'supersedes', inverted: false },
|
|
47
|
+
deprecates: { kind: 'supersedes', inverted: false },
|
|
34
48
|
'superseded-by': { kind: 'supersedes', inverted: true },
|
|
35
49
|
'superceded-by': { kind: 'supersedes', inverted: true },
|
|
36
50
|
'replaced-by': { kind: 'supersedes', inverted: true },
|
|
37
51
|
'obsoleted-by': { kind: 'supersedes', inverted: true },
|
|
52
|
+
'deprecated-by': { kind: 'supersedes', inverted: true },
|
|
53
|
+
'rolled-into': { kind: 'supersedes', inverted: true },
|
|
38
54
|
amends: { kind: 'amends', inverted: false },
|
|
39
55
|
extends: { kind: 'amends', inverted: false },
|
|
56
|
+
refines: { kind: 'amends', inverted: false },
|
|
57
|
+
clarifies: { kind: 'amends', inverted: false },
|
|
58
|
+
revises: { kind: 'amends', inverted: false },
|
|
40
59
|
'amended-by': { kind: 'amends', inverted: true },
|
|
60
|
+
'extended-by': { kind: 'amends', inverted: true },
|
|
61
|
+
'refined-by': { kind: 'amends', inverted: true },
|
|
62
|
+
'clarified-by': { kind: 'amends', inverted: true },
|
|
63
|
+
'revised-by': { kind: 'amends', inverted: true },
|
|
41
64
|
'depends-on': { kind: 'depends-on', inverted: false },
|
|
65
|
+
'dependent-on': { kind: 'depends-on', inverted: false },
|
|
42
66
|
dependencies: { kind: 'depends-on', inverted: false },
|
|
43
67
|
requires: { kind: 'depends-on', inverted: false },
|
|
68
|
+
'builds-on': { kind: 'depends-on', inverted: false },
|
|
69
|
+
'relies-on': { kind: 'depends-on', inverted: false },
|
|
70
|
+
'depended-on-by': { kind: 'depends-on', inverted: true },
|
|
71
|
+
'required-by': { kind: 'depends-on', inverted: true },
|
|
72
|
+
dependents: { kind: 'depends-on', inverted: true },
|
|
44
73
|
assumes: { kind: 'assumes', inverted: false },
|
|
74
|
+
'assumed-by': { kind: 'assumes', inverted: true },
|
|
45
75
|
'blocked-by': { kind: 'blocked-by', inverted: false },
|
|
76
|
+
'blocked-on': { kind: 'blocked-by', inverted: false },
|
|
77
|
+
'waiting-on': { kind: 'blocked-by', inverted: false },
|
|
78
|
+
'waiting-for': { kind: 'blocked-by', inverted: false },
|
|
79
|
+
'gated-on': { kind: 'blocked-by', inverted: false },
|
|
80
|
+
'gated-by': { kind: 'blocked-by', inverted: false },
|
|
46
81
|
blocks: { kind: 'blocked-by', inverted: true },
|
|
47
82
|
'delegates-to': { kind: 'delegates-to', inverted: false },
|
|
48
83
|
'delegated-to': { kind: 'delegates-to', inverted: false },
|
|
84
|
+
'deferred-to': { kind: 'delegates-to', inverted: false },
|
|
49
85
|
'tracked-in': { kind: 'delegates-to', inverted: false },
|
|
86
|
+
'tracked-by': { kind: 'delegates-to', inverted: false },
|
|
87
|
+
'continued-in': { kind: 'delegates-to', inverted: false },
|
|
88
|
+
'delegated-from': { kind: 'delegates-to', inverted: true },
|
|
50
89
|
related: { kind: 'relates-to', inverted: false },
|
|
51
90
|
'relates-to': { kind: 'relates-to', inverted: false },
|
|
91
|
+
'related-to': { kind: 'relates-to', inverted: false },
|
|
52
92
|
'see-also': { kind: 'relates-to', inverted: false },
|
|
53
93
|
references: { kind: 'references', inverted: false },
|
|
94
|
+
reference: { kind: 'references', inverted: false },
|
|
54
95
|
refs: { kind: 'references', inverted: false },
|
|
96
|
+
'referenced-by': { kind: 'references', inverted: true },
|
|
55
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* True when two folded keys are one edit apart.
|
|
100
|
+
*
|
|
101
|
+
* One edit, not two, and a transposition counts as one: `supercedesby` for
|
|
102
|
+
* `supercededby` is a slip of the finger, `categories` for `dependencies` is a
|
|
103
|
+
* different word. Written out rather than as a distance matrix because the
|
|
104
|
+
* answer is a yes or a no and the bound is one - the general algorithm would be
|
|
105
|
+
* more code, not less.
|
|
106
|
+
*/
|
|
107
|
+
export function withinOneEdit(a, b) {
|
|
108
|
+
if (a === b)
|
|
109
|
+
return false;
|
|
110
|
+
if (a.length === b.length) {
|
|
111
|
+
let first = -1;
|
|
112
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
113
|
+
if (a[i] === b[i])
|
|
114
|
+
continue;
|
|
115
|
+
if (first === -1) {
|
|
116
|
+
first = i;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// A second difference is allowed only if the two are a swapped pair, and
|
|
120
|
+
// only if there is no third.
|
|
121
|
+
return first === i - 1 && a[first] === b[i] && a[i] === b[first] && a.slice(i + 1) === b.slice(i + 1);
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const [short, long] = a.length < b.length ? [a, b] : [b, a];
|
|
126
|
+
if (long.length - short.length !== 1)
|
|
127
|
+
return false;
|
|
128
|
+
for (let i = 0; i < short.length; i += 1) {
|
|
129
|
+
if (short[i] === long[i])
|
|
130
|
+
continue;
|
|
131
|
+
return short.slice(i) === long.slice(i + 1);
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* True when a front-matter value is shaped like a citation.
|
|
137
|
+
*
|
|
138
|
+
* The second half of the gate on {@link misreadRelationKeys}, and the half that
|
|
139
|
+
* does the work. `sidebar_position: 4` and `ref: main` are a hair away from a
|
|
140
|
+
* relation key by spelling alone; neither carries anything that could name a
|
|
141
|
+
* document, and reporting them would make the rule an irritation rather than a
|
|
142
|
+
* catch (ADR-0006).
|
|
143
|
+
*/
|
|
144
|
+
function looksLikeCitation(value) {
|
|
145
|
+
const trimmed = value.trim();
|
|
146
|
+
if (trimmed.length === 0)
|
|
147
|
+
return false;
|
|
148
|
+
if (parsePrefixedRef(trimmed) !== null)
|
|
149
|
+
return true;
|
|
150
|
+
return looksLikePath(trimmed) && isDocumentTarget(trimmed) && !isExternal(trimmed);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Front-matter keys that read as relations and declare none.
|
|
154
|
+
*
|
|
155
|
+
* Front matter is an open vocabulary and most of what lives there is nobody
|
|
156
|
+
* else's business, so the bar is deliberately high: the key has to be one edit
|
|
157
|
+
* from a relation key *and* carry something that could name a document. Both
|
|
158
|
+
* conditions together are what separates `dependson: ADR-0001`, which is an edge
|
|
159
|
+
* the author believes exists, from `description: ...`, which is a title.
|
|
160
|
+
*/
|
|
161
|
+
function misreadRelationKeys(byKey, from, at) {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const [key, entry] of byKey) {
|
|
164
|
+
const folded = foldRelationKey(key);
|
|
165
|
+
if (folded.length === 0 || RELATION_INDEX.has(folded))
|
|
166
|
+
continue;
|
|
167
|
+
if (!valuesOf(entry).some(looksLikeCitation))
|
|
168
|
+
continue;
|
|
169
|
+
const near = [...RELATION_INDEX.values()]
|
|
170
|
+
.filter((relation) => withinOneEdit(folded, foldRelationKey(relation.canonical)))
|
|
171
|
+
.map((relation) => relation.canonical);
|
|
172
|
+
if (near.length === 0)
|
|
173
|
+
continue;
|
|
174
|
+
out.push({ key, suggestion: near.join(', '), from, at: at(entry.start, entry.end) });
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Folds a front-matter key to its lookup form.
|
|
180
|
+
*
|
|
181
|
+
* `depends-on`, `depends_on`, `dependsOn` and `Depends On` are one key written
|
|
182
|
+
* four ways, and which one a repository uses is decided by whatever wrote the
|
|
183
|
+
* front matter first. Dropping every separator collapses them - the YAML reader
|
|
184
|
+
* has already lower-cased the key, which is what makes camel case fold too.
|
|
185
|
+
*
|
|
186
|
+
* Which case this folds to is arbitrary and no test pins it: both sides of every
|
|
187
|
+
* comparison come through here, so upper would work exactly as well. Asserting
|
|
188
|
+
* on it would be asserting on the implementation.
|
|
189
|
+
*/
|
|
190
|
+
export function foldRelationKey(key) {
|
|
191
|
+
return key.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
192
|
+
}
|
|
193
|
+
/** The vocabulary above, indexed by folded key. */
|
|
194
|
+
const RELATION_INDEX = new Map(Object.entries(RELATION_KEYS).map(([canonical, relation]) => [
|
|
195
|
+
foldRelationKey(canonical),
|
|
196
|
+
{ ...relation, canonical },
|
|
197
|
+
]));
|
|
56
198
|
/**
|
|
57
199
|
* Phrases that give a link its meaning, longest-matching-nearest wins.
|
|
58
200
|
*
|
|
@@ -432,6 +574,7 @@ export function extractDocument(input) {
|
|
|
432
574
|
// Hand each region the items and references that fall inside it.
|
|
433
575
|
const ownItems = allItems.filter((item) => item.document === identity.id);
|
|
434
576
|
const ownReferences = allReferences.filter((reference) => belongsTo(reference, identity.id, allItems));
|
|
577
|
+
const misreadKeys = misreadRelationKeys(byKey, identity.id, at);
|
|
435
578
|
const anchors = new Set(scanned.headings.map((h) => h.slug));
|
|
436
579
|
const filled = subSpecifications.map((spec) => ({
|
|
437
580
|
...spec.extracted,
|
|
@@ -448,6 +591,7 @@ export function extractDocument(input) {
|
|
|
448
591
|
items: ownItems,
|
|
449
592
|
references: ownReferences,
|
|
450
593
|
problems,
|
|
594
|
+
misreadKeys,
|
|
451
595
|
anchors,
|
|
452
596
|
identity,
|
|
453
597
|
scanned,
|
|
@@ -526,6 +670,8 @@ function buildRegion(context) {
|
|
|
526
670
|
items: [],
|
|
527
671
|
references,
|
|
528
672
|
problems: [],
|
|
673
|
+
// A region has no front matter of its own to misread.
|
|
674
|
+
misreadKeys: [],
|
|
529
675
|
anchors,
|
|
530
676
|
identity,
|
|
531
677
|
scanned,
|
|
@@ -717,10 +863,12 @@ function extractReferences(context) {
|
|
|
717
863
|
const { scanned, directives, document, documentAt, items, byKey, status, claimed, file, index } = context;
|
|
718
864
|
const out = [];
|
|
719
865
|
const at = (start, end) => refOf(file, index, start, end);
|
|
720
|
-
// Front matter
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
866
|
+
// Front matter, walked in the order it was written rather than in the order
|
|
867
|
+
// the vocabulary happens to be listed: a key the table does not know still has
|
|
868
|
+
// to be looked at, and only the document knows which those are.
|
|
869
|
+
for (const [key, entry] of byKey) {
|
|
870
|
+
const relation = RELATION_INDEX.get(foldRelationKey(key));
|
|
871
|
+
if (!relation)
|
|
724
872
|
continue;
|
|
725
873
|
for (const value of valuesOf(entry)) {
|
|
726
874
|
const target = cleanTarget(value);
|
|
@@ -1081,7 +1229,7 @@ function renderLink(link) {
|
|
|
1081
1229
|
return `<${link.target}>`;
|
|
1082
1230
|
case 'reference':
|
|
1083
1231
|
case 'shortcut':
|
|
1084
|
-
return `[${link.text}][${link.
|
|
1232
|
+
return `[${link.text}][${link.label ?? ''}]`;
|
|
1085
1233
|
default:
|
|
1086
1234
|
return `[${link.text}](${link.target})`;
|
|
1087
1235
|
}
|