@descent-vtt/spec-graph 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +627 -0
- package/bin/spec-graph.js +24 -0
- package/dist/baseline.d.ts +93 -0
- package/dist/baseline.js +187 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cli.d.ts +66 -0
- package/dist/cli.js +565 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/directives.d.ts +56 -0
- package/dist/directives.js +125 -0
- package/dist/directives.js.map +1 -0
- package/dist/extract.d.ts +116 -0
- package/dist/extract.js +1043 -0
- package/dist/extract.js.map +1 -0
- package/dist/glob.d.ts +73 -0
- package/dist/glob.js +284 -0
- package/dist/glob.js.map +1 -0
- package/dist/graph.d.ts +55 -0
- package/dist/graph.js +201 -0
- package/dist/graph.js.map +1 -0
- package/dist/identity.d.ts +87 -0
- package/dist/identity.js +294 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +57 -0
- package/dist/lifecycle.js +258 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/markdown.d.ts +152 -0
- package/dist/markdown.js +820 -0
- package/dist/markdown.js.map +1 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +58 -0
- package/dist/paths.js.map +1 -0
- package/dist/report.d.ts +89 -0
- package/dist/report.js +326 -0
- package/dist/report.js.map +1 -0
- package/dist/resolve.d.ts +61 -0
- package/dist/resolve.js +340 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rules.d.ts +61 -0
- package/dist/rules.js +557 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +108 -0
- package/dist/runner.js +186 -0
- package/dist/runner.js.map +1 -0
- package/dist/sections.d.ts +94 -0
- package/dist/sections.js +372 -0
- package/dist/sections.js.map +1 -0
- package/dist/select.d.ts +96 -0
- package/dist/select.js +413 -0
- package/dist/select.js.map +1 -0
- package/dist/source.d.ts +43 -0
- package/dist/source.js +109 -0
- package/dist/source.js.map +1 -0
- package/dist/state.d.ts +45 -0
- package/dist/state.js +330 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +52 -0
- package/dist/types.js.map +1 -0
- package/dist/yaml.d.ts +37 -0
- package/dist/yaml.js +240 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The built-in diagnostics.\n *\n * Two of them - the two the whole project exists for - are written as selector\n * queries rather than as bespoke traversals. That is not a stunt: it is the\n * proof that the query language is expressive enough to be worth exposing. A\n * team whose convention spec-graph never anticipated can write the equivalent\n * of a built-in rule on the command line.\n *\n * ```text\n * ghost-handover *[openness!=closed][phase!=retired] -delegates-to,blocked-by-> *[receptivity=sealed]\n * stale-premise *[phase!=retired] -depends-on,assumes,amends,blocked-by-> document[phase=retired]\n * ```\n *\n * The rest are not path queries and are not pretended to be: dangling\n * references never became edges, and a cycle is not a fixed-length path.\n *\n * Every rule here obeys one discipline - **report the place a human can fix**.\n * For a supersession that the superseded document never acknowledged, that is\n * the superseded document, not the one that declared the relation.\n */\n\nimport { OBLIGATION_EDGES, type SpecGraph } from './graph.js';\nimport { dirnamePosix, resolveFrom } from './paths.js';\nimport { execute, parseQuery, type QuerySpec } from './select.js';\nimport type { ResolvedCorpus } from './resolve.js';\nimport type {\n DanglingRef,\n Diagnostic,\n DocumentNode,\n Edge,\n ItemNode,\n RelatedLocation,\n RuleId,\n Severity,\n SourceRef,\n SpecNode,\n} from './types.js';\nimport { EDGE_TRAITS } from './types.js';\n\n/* -------------------------------------------------------------------------- */\n/* Configuration */\n/* -------------------------------------------------------------------------- */\n\nexport const DEFAULT_SEVERITIES: Readonly<Record<RuleId, Severity>> = Object.freeze({\n 'ghost-handover': 'error',\n 'stale-premise': 'error',\n 'broken-reference': 'error',\n 'reference-outside-corpus': 'warn',\n 'ambiguous-reference': 'warn',\n 'circular-delegation': 'error',\n 'orphaned-obligation': 'error',\n 'live-supersession': 'error',\n 'unreciprocated-supersession': 'warn',\n 'state-conflict': 'warn',\n 'self-reference': 'info',\n});\n\nexport const RULE_IDS = Object.keys(DEFAULT_SEVERITIES) as readonly RuleId[];\n\n/** One-line descriptions, printed by `spec-graph rules`. */\nexport const RULE_DESCRIPTIONS: Readonly<Record<RuleId, string>> = Object.freeze({\n 'ghost-handover': 'an open obligation is handed to a document that can no longer absorb it',\n 'stale-premise': 'a live document rests on a decision that has been retired or obviated',\n 'broken-reference': 'a citation names a document or anchor that does not exist',\n 'reference-outside-corpus': 'a citation names a real document the include patterns did not reach',\n 'ambiguous-reference': 'a citation matches more than one document',\n 'circular-delegation': 'obligations or supersessions form a cycle, so none of them can ever land',\n 'orphaned-obligation': 'a retired or frozen document still holds open obligations',\n 'live-supersession': 'a document has been superseded but still presents itself as current',\n 'unreciprocated-supersession': 'a retired document does not say what replaced it',\n 'state-conflict': 'an item declares two states that disagree about whether work remains',\n 'self-reference': 'a document delegates to or depends on itself',\n});\n\n/**\n * The severity map `--strict` implies, and which rules it actually moved.\n *\n * Strict raises `warn` to `error` and leaves `info` alone. That asymmetry is\n * deliberate: the `info` rules are advisory by nature - a self-link in a table\n * of contents is a formatting quirk - and promoting them would resurrect\n * precisely the false-positive problem ADR-0006 exists to prevent.\n *\n * An explicit `--rule` always wins over strict, so `--strict --rule x=warn`\n * keeps `x` at warn. That is what makes strict usable: a team can turn it on\n * and exempt the one rule their repository disagrees with, rather than choosing\n * between all of it and none of it.\n */\nexport function resolveStrict(\n overrides: Partial<Record<RuleId, Severity>> = {},\n strict = false,\n): { severities: Partial<Record<RuleId, Severity>>; escalated: ReadonlySet<RuleId> } {\n const severities: Partial<Record<RuleId, Severity>> = { ...overrides };\n const escalated = new Set<RuleId>();\n if (!strict) return { severities, escalated };\n\n for (const id of RULE_IDS) {\n if (overrides[id] !== undefined) continue;\n if (DEFAULT_SEVERITIES[id] !== 'warn') continue;\n severities[id] = 'error';\n escalated.add(id);\n }\n return { severities, escalated };\n}\n\nexport interface RuleOptions {\n readonly severities?: Partial<Record<RuleId, Severity>> | undefined;\n /** Cap on related locations attached to one finding. */\n readonly maxRelated?: number | undefined;\n}\n\nconst DEFAULT_MAX_RELATED = 8;\n\n/* -------------------------------------------------------------------------- */\n/* Entry point */\n/* -------------------------------------------------------------------------- */\n\n/** Runs every enabled rule and returns findings in report order. */\nexport function runRules(graph: SpecGraph, corpus: ResolvedCorpus, options: RuleOptions = {}): Diagnostic[] {\n const severities = { ...DEFAULT_SEVERITIES, ...(options.severities ?? {}) };\n const maxRelated = options.maxRelated ?? DEFAULT_MAX_RELATED;\n const out: Diagnostic[] = [];\n\n const emit = (rule: RuleId, build: () => Built): void => {\n const severity = severities[rule];\n if (severity === 'off') return;\n const body = build();\n if (exempt(graph, rule, body.nodes)) return;\n // `target` ahead of the spread: a rule that names one overrides the default,\n // and the eight that have nothing to name say nothing.\n out.push({ rule, severity, target: null, ...body, related: body.related.slice(0, maxRelated) });\n };\n\n // Ghost handovers are reported first and claim their edges, because\n // `blocked-by` is both obligation-transferring and load-bearing: without this,\n // one open question blocked by an archived decision would produce two\n // findings on the same line, for the same defect, with the same fix.\n const claimed = ghostHandovers(graph, emit);\n stalePremises(graph, emit, claimed);\n brokenReferences(corpus, emit);\n circularDelegations(graph, emit);\n orphanedObligations(graph, emit, maxRelated);\n supersessions(graph, emit);\n stateConflicts(graph, emit);\n selfReferences(graph, emit);\n\n return sortDiagnostics(out);\n}\n\n/** What a rule returns: the finding, minus what `emit` knows better. */\ntype Built = Omit<Diagnostic, 'rule' | 'severity' | 'target'> & { readonly target?: string };\n\ntype Emit = (rule: RuleId, build: () => Built) => void;\n\n/**\n * Rules about what a document cites, rather than about what it owes.\n *\n * These are the ones a historical record still answers for. A link that goes\n * nowhere is broken whoever wrote it and whenever they wrote it.\n */\nconst REFERENCE_RULES: ReadonlySet<RuleId> = new Set<RuleId>([\n 'broken-reference',\n 'reference-outside-corpus',\n 'ambiguous-reference',\n]);\n\n/**\n * Whether a finding is about history rather than about work.\n *\n * A record reports that something was decided; it does not decide anything. Its\n * open checkboxes are minutes, its delegations are things that were said, and\n * its lifecycle is not a lifecycle. So a record is never the *subject* of a\n * finding about obligations or lifecycle - though it is still a legitimate\n * target of one, because delegating live work into a log is exactly the ghost\n * handover this tool exists to find.\n *\n * Stated once, here, rather than as a guard repeated in eight rules: the\n * exemption is a property of what counts as a finding, not of any one rule, and\n * a rule added later inherits it without having to remember. See ADR-0011.\n */\nfunction exempt(graph: SpecGraph, rule: RuleId, nodes: readonly string[]): boolean {\n if (REFERENCE_RULES.has(rule)) return false;\n const subject = nodes[0];\n if (subject === undefined) return false;\n return graph.owningDocument(subject)?.phase === 'record';\n}\n\n/* -------------------------------------------------------------------------- */\n/* Ghost handovers */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The flagship failure mode: an obligation handed into a closed ledger.\n *\n * The source must still be open and must not itself be retired - a retired\n * document delegating to another retired document is history, not a defect,\n * and reporting it would bury the live findings underneath the archive.\n */\nconst GHOST_HANDOVER: QuerySpec = parseQuery(\n '*[openness!=closed][phase!=retired] -delegates-to,blocked-by-> *[receptivity=sealed]',\n);\n\n/** Emits ghost-handover findings and returns the edges they account for. */\nfunction ghostHandovers(graph: SpecGraph, emit: Emit): Set<string> {\n const claimed = new Set<string>();\n for (const match of execute(graph, GHOST_HANDOVER)) {\n const source = match.nodes[0] as SpecNode;\n const target = match.nodes[1] as SpecNode;\n const edge = match.edges[0] as Edge;\n const targetDocument = graph.owningDocument(target.id);\n if (!targetDocument) continue;\n\n const sealed = SEALED_WORD[targetDocument.phase] ?? 'frozen';\n const what = source.kind === 'item' ? 'open obligation' : 'obligation';\n const verb = EDGE_TRAITS[edge.kind].phrase;\n claimed.add(edgeKey(edge));\n\n emit('ghost-handover', () => ({\n message: `${what} ${verb} ${describe(target)}, which is ${sealed}`,\n at: edge.declaredAt,\n nodes: [source.id, target.id],\n related: [\n related(targetDocument.statusAt ?? targetDocument.at, `${targetDocument.id} is ${sealed}${statusSuffix(targetDocument)}`),\n ...(source.kind === 'item' ? [related(source.at, `the obligation: ${source.text}`)] : []),\n ],\n hint: HANDOVER_HINT[targetDocument.phase]?.(targetDocument.id) ?? FROZEN_HINT(targetDocument.id),\n }));\n }\n return claimed;\n}\n\n/** How a sealed target is described in a ghost-handover message. */\nconst SEALED_WORD: Readonly<Partial<Record<string, string>>> = {\n retired: 'retired',\n record: 'a historical record',\n};\n\nconst FROZEN_HINT = (id: string): string =>\n `${id} is frozen and cannot take on new work - open an amendment, or close this here`;\n\n/** What to do about work handed into each kind of sealed document. */\nconst HANDOVER_HINT: Readonly<Partial<Record<string, (id: string) => string>>> = {\n retired: (id) => `nothing will be read from ${id} again - re-home this in a live document, or close it here`,\n record: (id) => `${id} is a log of what happened and will never act - re-home this in a live document`,\n};\n\n/** Identity of a relation, for the hand-off between overlapping rules. */\nfunction edgeKey(edge: Edge): string {\n return `${edge.kind} ${edge.from} ${edge.to}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Stale premises */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A live document resting on something that stopped being true.\n *\n * Only load-bearing relations count. A \"See also\" pointing at a superseded ADR\n * is ordinary cross-referencing; `assumes` pointing at the same document means\n * somebody is designing around a constraint that was abolished.\n */\nconst STALE_PREMISE_DOCUMENT: QuerySpec = parseQuery(\n '*[phase!=retired] -depends-on,assumes,amends,blocked-by-> document[phase=retired]',\n);\nconst STALE_PREMISE_ITEM: QuerySpec = parseQuery(\n '*[phase!=retired] -depends-on,assumes,amends,blocked-by-> item[state=obviated]',\n);\n\nfunction stalePremises(graph: SpecGraph, emit: Emit, claimed: ReadonlySet<string>): void {\n const seen = new Set<string>();\n for (const spec of [STALE_PREMISE_DOCUMENT, STALE_PREMISE_ITEM]) {\n for (const match of execute(graph, spec)) {\n const source = match.nodes[0] as SpecNode;\n const target = match.nodes[1] as SpecNode;\n const edge = match.edges[0] as Edge;\n // Already reported as a ghost handover, which says the same thing in the\n // terms the reader needs.\n if (claimed.has(edgeKey(edge))) continue;\n const key = `${source.id}>${target.id}>${edge.kind}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n const targetDocument = graph.owningDocument(target.id);\n const because =\n target.kind === 'item'\n ? 'that question was obviated - its premise no longer holds'\n : `${target.id} is retired${statusSuffix(targetDocument)}`;\n\n emit('stale-premise', () => ({\n message: `${describe(source)} ${EDGE_TRAITS[edge.kind].phrase} ${describe(target)}, which no longer holds`,\n at: edge.declaredAt,\n nodes: [source.id, target.id],\n // Point at the status line when there is one: that is the sentence that\n // makes the premise stale, and repeating the document's first line\n // underneath it adds nothing.\n related: [related(target.kind === 'document' ? (targetDocument?.statusAt ?? target.at) : target.at, because)],\n hint: `re-check this dependency: the constraint it assumes may have been lifted when ${target.kind === 'item' ? 'the question closed' : `${target.id} was retired`}`,\n }));\n }\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Reference integrity */\n/* -------------------------------------------------------------------------- */\n\nfunction brokenReferences(corpus: ResolvedCorpus, emit: Emit): void {\n for (const ref of corpus.dangling) {\n if (ref.reason === 'ambiguous') {\n emit('ambiguous-reference', () => ({\n message: `\"${ref.target}\" matches ${ref.candidates.length} documents`,\n at: ref.declaredAt,\n nodes: [ref.from],\n target: ref.target,\n related: [],\n hint: `disambiguate it - candidates: ${ref.candidates.join(', ')}`,\n }));\n continue;\n }\n\n // A link to a file that exists but was not included is a configuration\n // problem: the graph is not broken, the corpus is just incomplete. Reporting\n // it as an error means a repository whose specifications live somewhere the\n // default patterns miss fails on its first run, for something the user has\n // not done wrong.\n const rule: RuleId = ref.reason === 'not-a-spec' ? 'reference-outside-corpus' : 'broken-reference';\n\n emit(rule, () => ({\n message: brokenMessage(ref),\n at: ref.declaredAt,\n nodes: [ref.from],\n target: ref.target,\n related: [],\n hint: brokenHint(ref),\n }));\n }\n}\n\nfunction brokenMessage(ref: DanglingRef): string {\n switch (ref.reason) {\n case 'unknown-anchor':\n return `\"${ref.target}\" points at an anchor that does not exist`;\n case 'not-a-spec':\n return `\"${ref.target}\" resolves to a file that is not a specification`;\n default:\n return `\"${ref.target}\" does not resolve to any document`;\n }\n}\n\nfunction brokenHint(ref: DanglingRef): string {\n if (ref.candidates.length > 0) return `did you mean ${ref.candidates.slice(0, 3).join(', ')}?`;\n // A wiki link carries no path, only a name, so whether it means a document at\n // all is a repository convention. Naming the flag here is what keeps that a\n // one-run discovery rather than a support question.\n if (ref.raw.startsWith('[[') && ref.reason === 'unknown-target') {\n return `fix the identifier, or - if [[...]] tags a concept here - exclude it: --ignore-ref \"${suggestReferenceGlob(ref.target)}\"`;\n }\n switch (ref.reason) {\n case 'unknown-anchor':\n return 'check the heading or item id it is meant to address';\n case 'not-a-spec':\n // Name the pattern that would include it: \"widen your patterns\" is advice\n // the reader then has to translate, and this is the translation.\n return `include it, for example: spec-graph \"${suggestPattern(ref)}\"`;\n default:\n return 'fix the identifier, or add the document it names';\n }\n}\n\n/**\n * A glob covering the family of concept tags a target belongs to.\n *\n * `trap 55` suggests `trap *` rather than `trap 55`, because these tags come in\n * numbered series and excluding them one at a time is not a fix anybody would\n * accept. A target with no trailing number is suggested verbatim.\n */\nfunction suggestReferenceGlob(target: string): string {\n const trimmed = target.trim();\n const series = /^(.*?)[\\s._-]*\\d+$/.exec(trimmed);\n const stem = series?.[1]?.trim();\n return stem !== undefined && stem.length > 0 ? `${stem} *` : trimmed;\n}\n\n/**\n * The narrowest include pattern that would reach a target the corpus missed.\n *\n * Built from the *resolved* repository-relative path, not the link as written:\n * `../notes.md` cited from `docs/adr/` needs `docs/**\\/*.md`, and suggesting\n * `../**\\/*.md` would be worse than saying nothing.\n */\nfunction suggestPattern(ref: DanglingRef): string {\n const withoutAnchor = (ref.target.split('#')[0] ?? ref.target).trim();\n const resolved = resolveFrom(ref.declaredAt.file, withoutAnchor);\n const directory = dirnamePosix(resolved);\n return directory.length > 0 ? `${directory}/**/*.md` : '**/*.md';\n}\n\n/* -------------------------------------------------------------------------- */\n/* Cycles */\n/* -------------------------------------------------------------------------- */\n\nfunction circularDelegations(graph: SpecGraph, emit: Emit): void {\n const kinds = [...OBLIGATION_EDGES, 'supersedes' as const];\n // Projected onto documents, so a question handed back and forth between two\n // of them is found even though each delegation runs item -> document and the\n // raw graph therefore contains no cycle at all.\n for (const component of graph.cycles(kinds, { byDocument: true })) {\n const members = component\n .map((id) => graph.document(id))\n .filter((node): node is DocumentNode => node !== undefined);\n if (members.length < 2) continue;\n // A cycle is a set, not a subject, so the exemption at `emit` cannot see it.\n // One record in the loop means the loop is partly a report of what was once\n // said, and nothing in it is owed by anybody.\n if (members.some((member) => member.phase === 'record')) continue;\n\n const edges = cycleEdges(graph, component, kinds);\n const onlySupersession = edges.length > 0 && edges.every((edge) => edge.kind === 'supersedes');\n const head = members[0] as DocumentNode;\n\n emit('circular-delegation', () => ({\n message: onlySupersession\n ? `supersession cycle across ${members.length} documents`\n : `delegation cycle across ${members.length} documents: nothing in it can ever land`,\n at: edges[0]?.declaredAt ?? head.at,\n nodes: component,\n related: edges.map((edge) => related(edge.declaredAt, `${edge.from} ${EDGE_TRAITS[edge.kind].phrase} ${edge.to}`)),\n hint: 'break the loop: one of these must own the work outright, or be closed',\n }));\n }\n}\n\n/**\n * The relations that keep a component strongly connected.\n *\n * Membership is by owning document, because the component is a projection: the\n * edge that closes the loop is usually written on an item, and that is the line\n * the report has to point at.\n */\nfunction cycleEdges(graph: SpecGraph, component: readonly string[], kinds: readonly Edge['kind'][]): Edge[] {\n const inside = new Set(component);\n const owner = (id: string): string => graph.owningDocument(id)?.id ?? id;\n const out: Edge[] = [];\n for (const edge of graph.edges) {\n if (!kinds.includes(edge.kind)) continue;\n const from = owner(edge.from);\n const to = owner(edge.to);\n if (from !== to && inside.has(from) && inside.has(to)) out.push(edge);\n }\n return out;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Orphaned obligations */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The black hole itself: a document nobody reads any more, still holding work.\n *\n * Reported once per document rather than once per item. Five findings that all\n * say \"this archived ADR has open questions\" is five times the noise and none\n * of the extra information.\n */\nfunction orphanedObligations(graph: SpecGraph, emit: Emit, maxRelated: number): void {\n for (const document of graph.documents) {\n if (document.phase !== 'retired' && document.phase !== 'frozen') continue;\n const open = graph.itemsOf(document.id).filter((item) => item.openness !== 'closed');\n if (open.length === 0) continue;\n\n const sealed = document.phase === 'retired' ? 'retired' : 'frozen';\n emit('orphaned-obligation', () => ({\n message: `${sealed} document still holds ${open.length} open ${open.length === 1 ? 'obligation' : 'obligations'}`,\n at: document.statusAt ?? document.at,\n nodes: [document.id, ...open.map((item) => item.id)],\n related: open\n .slice(0, maxRelated)\n .map((item) => related(item.at, `${item.disposition}: ${item.text}`)),\n hint: `move each one to a live document or close it - as it stands, ${open.length === 1 ? 'it disappears' : 'they disappear'} with ${document.id}`,\n }));\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Supersession consistency */\n/* -------------------------------------------------------------------------- */\n\nfunction supersessions(graph: SpecGraph, emit: Emit): void {\n for (const edge of graph.edges) {\n if (edge.kind !== 'supersedes' || edge.reflexive) continue;\n const superseded = graph.document(edge.to);\n const superseding = graph.document(edge.from);\n if (!superseded || !superseding) continue;\n // A record narrating \"ADR-0005 replaced ADR-0001\" is reporting a\n // supersession, not declaring one. Both hints below ask somebody to edit\n // the document that made the claim, and a log is the one document nobody\n // can edit - it says what was true when it was written. The exemption at\n // `emit` cannot see this: the subject here is the *other* document.\n if (superseding.phase === 'record') continue;\n\n if (superseded.phase !== 'retired' && superseded.phase !== 'unknown') {\n emit('live-supersession', () => ({\n // The fix belongs in the superseded document, so that is where the\n // finding points - not at the document that made the claim.\n message: `${superseded.id} is superseded by ${superseding.id} but still reads as ${superseded.phase}`,\n at: superseded.statusAt ?? superseded.at,\n nodes: [superseded.id, superseding.id],\n related: [related(edge.declaredAt, `the supersession is declared here`)],\n hint: `mark ${superseded.id} as superseded, or drop the claim in ${superseding.id}`,\n }));\n continue;\n }\n\n if (superseded.phase === 'retired' && !edge.declaredIn.includes(superseded.path)) {\n emit('unreciprocated-supersession', () => ({\n message: `${superseded.id} is retired but never says that ${superseding.id} replaced it`,\n at: superseded.statusAt ?? superseded.at,\n nodes: [superseded.id, superseding.id],\n related: [related(edge.declaredAt, `only ${superseding.id} records the relationship`)],\n hint: `add \"superseded-by: ${superseding.id}\" to ${superseded.path} so a reader who lands there is redirected`,\n }));\n }\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Item state */\n/* -------------------------------------------------------------------------- */\n\nfunction stateConflicts(graph: SpecGraph, emit: Emit): void {\n for (const item of graph.items) {\n if (item.conflicts.length === 0) continue;\n emit('state-conflict', () => ({\n message: `item reads as ${item.disposition} but also carries ${describeSignals(item)}`,\n at: item.evidence.at,\n nodes: [item.id],\n related: item.conflicts.map((signal) =>\n related(signal.at, `${signal.source} says ${signal.disposition}${signal.raw ? `: ${signal.raw}` : ''}`),\n ),\n hint: `make the two agree - spec-graph is treating it as ${item.openness}`,\n }));\n }\n}\n\nfunction describeSignals(item: ItemNode): string {\n const parts = item.conflicts.map((signal) => `a ${signal.source} saying ${signal.disposition}`);\n return parts.length === 1 ? (parts[0] as string) : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Self references */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A document that delegates to, or depends on, itself.\n *\n * Almost always a copied link that was never repointed. Informational, because\n * a self-link in a table of contents is harmless - but a *delegation* to self\n * means an obligation that looks handed off and was not.\n */\nfunction selfReferences(graph: SpecGraph, emit: Emit): void {\n for (const edge of graph.edges) {\n if (!edge.reflexive || edge.kind === 'contains') continue;\n const traits = EDGE_TRAITS[edge.kind];\n if (!traits.transfersObligation && !traits.loadBearing) continue;\n const node = graph.node(edge.from);\n if (!node) continue;\n\n emit('self-reference', () => ({\n message: `${describe(node)} ${traits.phrase} itself`,\n at: edge.declaredAt,\n nodes: [edge.from],\n related: [],\n hint: traits.transfersObligation\n ? 'this obligation looks delegated but never left - point it at another document, or own it here'\n : 'a document cannot depend on itself - repoint or remove the reference',\n }));\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Helpers */\n/* -------------------------------------------------------------------------- */\n\nfunction describe(node: SpecNode): string {\n return node.kind === 'document' ? node.id : `${node.document} \"${truncate(node.text, 48)}\"`;\n}\n\nfunction truncate(text: string, limit: number): string {\n return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;\n}\n\nfunction statusSuffix(document: DocumentNode | undefined): string {\n return document?.rawStatus ? ` (\"${document.rawStatus}\")` : '';\n}\n\nfunction related(at: SourceRef, note: string): RelatedLocation {\n return { at, note };\n}\n\nconst SEVERITY_ORDER: Readonly<Record<Exclude<Severity, 'off'>, number>> = { error: 0, warn: 1, info: 2 };\n\n/**\n * Total ordering, so two runs on the same corpus produce byte-identical output.\n *\n * CI output that reorders between runs cannot be diffed, and output that cannot\n * be diffed stops being read.\n */\nexport function sortDiagnostics(diagnostics: readonly Diagnostic[]): Diagnostic[] {\n return [...diagnostics].sort((a, b) => {\n const bySeverity = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];\n if (bySeverity !== 0) return bySeverity;\n if (a.at.file !== b.at.file) return a.at.file < b.at.file ? -1 : 1;\n if (a.at.span.start.line !== b.at.span.start.line) return a.at.span.start.line - b.at.span.start.line;\n if (a.at.span.start.column !== b.at.span.start.column) return a.at.span.start.column - b.at.span.start.column;\n if (a.rule !== b.rule) return a.rule < b.rule ? -1 : 1;\n return a.message < b.message ? -1 : a.message > b.message ? 1 : 0;\n });\n}\n\n/** The selector equivalent of a rule, for `spec-graph rules --explain`. */\nexport const RULE_QUERIES: Readonly<Partial<Record<RuleId, string>>> = Object.freeze({\n 'ghost-handover': '*[openness!=closed][phase!=retired] -delegates-to,blocked-by-> *[receptivity=sealed]',\n 'stale-premise': '*[phase!=retired] -depends-on,assumes,amends,blocked-by-> document[phase=retired]',\n 'orphaned-obligation': 'document[receptivity=sealed] -contains-> item[openness!=closed]',\n 'self-reference': 'see --format json; reflexive edges are excluded from traversal by default',\n});\n"]}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration: files in, graph and findings out.
|
|
3
|
+
*
|
|
4
|
+
* The only part of spec-graph that touches the filesystem. Everything below it
|
|
5
|
+
* is a pure function of text, which is why the test suite can build pathological
|
|
6
|
+
* corpora in memory and why a build script can feed spec-graph documents that
|
|
7
|
+
* never existed on disk.
|
|
8
|
+
*/
|
|
9
|
+
import { type SpecGraph } from './graph.js';
|
|
10
|
+
import { type ResolvedCorpus } from './resolve.js';
|
|
11
|
+
import { type RuleOptions } from './rules.js';
|
|
12
|
+
import type { AnalysisSummary, Diagnostic, ParseProblem, RuleId, Severity } from './types.js';
|
|
13
|
+
/** Patterns used when the caller names none. */
|
|
14
|
+
export declare const DEFAULT_PATTERNS: readonly string[];
|
|
15
|
+
/** How many files are read at once. */
|
|
16
|
+
export declare const DEFAULT_CONCURRENCY = 16;
|
|
17
|
+
export interface AnalyseOptions {
|
|
18
|
+
/** Absolute or relative directory the patterns are resolved against. */
|
|
19
|
+
readonly root: string;
|
|
20
|
+
readonly patterns?: readonly string[] | undefined;
|
|
21
|
+
readonly ignore?: readonly string[] | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Reference targets to leave unreported when they do not resolve.
|
|
24
|
+
*
|
|
25
|
+
* For repositories where `[[...]]` tags concepts rather than naming files.
|
|
26
|
+
* Suppresses findings only; a reference that resolves is still an edge.
|
|
27
|
+
*/
|
|
28
|
+
readonly ignoreReferences?: readonly string[] | undefined;
|
|
29
|
+
/** Families a bare identifier may name. Unset means any family in the corpus. */
|
|
30
|
+
readonly families?: readonly string[] | undefined;
|
|
31
|
+
/** Families that are never citations, whatever the corpus contains. */
|
|
32
|
+
readonly ignoreFamilies?: readonly string[] | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Files that are logs of what was decided rather than decisions.
|
|
35
|
+
*
|
|
36
|
+
* Journals, changelogs, meeting minutes, sprint summaries. Their references
|
|
37
|
+
* are still checked; their obligations and lifecycle are not. See ADR-0011.
|
|
38
|
+
*/
|
|
39
|
+
readonly historyPatterns?: readonly string[] | undefined;
|
|
40
|
+
readonly severities?: Partial<Record<RuleId, Severity>> | undefined;
|
|
41
|
+
readonly concurrency?: number | undefined;
|
|
42
|
+
readonly maxRelated?: number | undefined;
|
|
43
|
+
readonly followSymlinks?: boolean | undefined;
|
|
44
|
+
}
|
|
45
|
+
export interface AnalysisResult {
|
|
46
|
+
readonly graph: SpecGraph;
|
|
47
|
+
readonly corpus: ResolvedCorpus;
|
|
48
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
49
|
+
readonly problems: readonly ParseProblem[];
|
|
50
|
+
readonly files: readonly string[];
|
|
51
|
+
readonly summary: AnalysisSummary;
|
|
52
|
+
/** True when nothing at error severity was found. */
|
|
53
|
+
readonly ok: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Reads, extracts, resolves, and checks a corpus of specifications. */
|
|
56
|
+
export declare function analyse(options: AnalyseOptions): Promise<AnalysisResult>;
|
|
57
|
+
export interface Source {
|
|
58
|
+
/** Repository-relative POSIX path. */
|
|
59
|
+
readonly path: string;
|
|
60
|
+
readonly text: string;
|
|
61
|
+
}
|
|
62
|
+
export interface AnalyseSourcesOptions extends RuleOptions {
|
|
63
|
+
readonly fileExists?: ((path: string) => boolean) | undefined;
|
|
64
|
+
readonly isIgnoredReference?: ((target: string) => boolean) | undefined;
|
|
65
|
+
readonly isIgnoredFamily?: ((family: string) => boolean) | undefined;
|
|
66
|
+
/** Whether a path holds a historical record rather than a specification. */
|
|
67
|
+
readonly isRecord?: ((path: string) => boolean) | undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Builds the record predicate, or nothing when no patterns were given.
|
|
71
|
+
*
|
|
72
|
+
* Returning `undefined` rather than a predicate that always says no keeps the
|
|
73
|
+
* common case free of a call per file, and makes "no history patterns" visible
|
|
74
|
+
* as an absent option rather than as a function nobody can read.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createHistoryMatcher(patterns: readonly string[] | undefined): ((path: string) => boolean) | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Builds the family predicate from an allowlist and a denylist.
|
|
79
|
+
*
|
|
80
|
+
* An allowlist is the stronger statement - "these are the families this
|
|
81
|
+
* repository has" - and turns every other noun-number construct back into
|
|
82
|
+
* prose. A denylist handles the narrower case where the corpus genuinely owns a
|
|
83
|
+
* family but some of its numbers belong to somebody else, which is exactly
|
|
84
|
+
* `RFC 2119` in a repository of local RFCs.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createFamilyFilter(families: readonly string[] | undefined, ignoreFamilies: readonly string[] | undefined): ((family: string) => boolean) | undefined;
|
|
87
|
+
interface Analysed {
|
|
88
|
+
readonly graph: SpecGraph;
|
|
89
|
+
readonly corpus: ResolvedCorpus;
|
|
90
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The whole engine, without a filesystem.
|
|
94
|
+
*
|
|
95
|
+
* Exported because it is the honest unit of composition: a monorepo tool that
|
|
96
|
+
* already has the file contents in memory should not have to write them out to
|
|
97
|
+
* a temporary directory to use spec-graph.
|
|
98
|
+
*/
|
|
99
|
+
export declare function analyseSources(sources: readonly Source[], options?: AnalyseSourcesOptions): Analysed;
|
|
100
|
+
/**
|
|
101
|
+
* Rebuilds a result around a narrower set of findings.
|
|
102
|
+
*
|
|
103
|
+
* The corpus, the graph and the counts of what is in them are unchanged - a
|
|
104
|
+
* baseline suppresses findings, never facts. Only the tallies that describe the
|
|
105
|
+
* findings, and the verdict that follows from them, move.
|
|
106
|
+
*/
|
|
107
|
+
export declare function withDiagnostics(result: AnalysisResult, diagnostics: readonly Diagnostic[]): AnalysisResult;
|
|
108
|
+
export {};
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration: files in, graph and findings out.
|
|
3
|
+
*
|
|
4
|
+
* The only part of spec-graph that touches the filesystem. Everything below it
|
|
5
|
+
* is a pure function of text, which is why the test suite can build pathological
|
|
6
|
+
* corpora in memory and why a build script can feed spec-graph documents that
|
|
7
|
+
* never existed on disk.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { extractSpecifications } from './extract.js';
|
|
12
|
+
import { createGlobMatcher, createReferenceFilter, walkFiles } from './glob.js';
|
|
13
|
+
import { buildGraph } from './graph.js';
|
|
14
|
+
import { toPosix } from './paths.js';
|
|
15
|
+
import { resolveCorpus } from './resolve.js';
|
|
16
|
+
import { runRules } from './rules.js';
|
|
17
|
+
/** Patterns used when the caller names none. */
|
|
18
|
+
export const DEFAULT_PATTERNS = Object.freeze([
|
|
19
|
+
'docs/**/*.md',
|
|
20
|
+
'doc/**/*.md',
|
|
21
|
+
'adr/**/*.md',
|
|
22
|
+
'rfcs/**/*.md',
|
|
23
|
+
'specs/**/*.md',
|
|
24
|
+
'*.md',
|
|
25
|
+
]);
|
|
26
|
+
/** How many files are read at once. */
|
|
27
|
+
export const DEFAULT_CONCURRENCY = 16;
|
|
28
|
+
/** Reads, extracts, resolves, and checks a corpus of specifications. */
|
|
29
|
+
export async function analyse(options) {
|
|
30
|
+
const started = performance.now();
|
|
31
|
+
const root = toPosix(options.root).replace(/\/+$/, '');
|
|
32
|
+
const patterns = options.patterns && options.patterns.length > 0 ? options.patterns : DEFAULT_PATTERNS;
|
|
33
|
+
const walked = await walkFiles({
|
|
34
|
+
root,
|
|
35
|
+
patterns,
|
|
36
|
+
ignore: options.ignore,
|
|
37
|
+
followSymlinks: options.followSymlinks,
|
|
38
|
+
});
|
|
39
|
+
const sources = await readAll(walked, options.concurrency ?? DEFAULT_CONCURRENCY);
|
|
40
|
+
const present = new Set(walked.map((file) => file.path.toLowerCase()));
|
|
41
|
+
return finish(analyseSources(sources, {
|
|
42
|
+
// Consulted only for references that failed to resolve, so the cost is
|
|
43
|
+
// bounded by the number of findings - and it buys the difference between
|
|
44
|
+
// "that document does not exist" and "that document exists but your
|
|
45
|
+
// include patterns did not reach it", which are different fixes.
|
|
46
|
+
fileExists: (path) => present.has(path.toLowerCase()) || existsSync(`${root}/${path}`),
|
|
47
|
+
isIgnoredReference: createReferenceFilter(options.ignoreReferences ?? []),
|
|
48
|
+
isIgnoredFamily: createFamilyFilter(options.families, options.ignoreFamilies),
|
|
49
|
+
isRecord: createHistoryMatcher(options.historyPatterns),
|
|
50
|
+
...(options.severities !== undefined ? { severities: options.severities } : {}),
|
|
51
|
+
...(options.maxRelated !== undefined ? { maxRelated: options.maxRelated } : {}),
|
|
52
|
+
}), walked.map((file) => file.path), started);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds the record predicate, or nothing when no patterns were given.
|
|
56
|
+
*
|
|
57
|
+
* Returning `undefined` rather than a predicate that always says no keeps the
|
|
58
|
+
* common case free of a call per file, and makes "no history patterns" visible
|
|
59
|
+
* as an absent option rather than as a function nobody can read.
|
|
60
|
+
*/
|
|
61
|
+
export function createHistoryMatcher(patterns) {
|
|
62
|
+
if (patterns === undefined || patterns.length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
return createGlobMatcher(patterns);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Builds the family predicate from an allowlist and a denylist.
|
|
68
|
+
*
|
|
69
|
+
* An allowlist is the stronger statement - "these are the families this
|
|
70
|
+
* repository has" - and turns every other noun-number construct back into
|
|
71
|
+
* prose. A denylist handles the narrower case where the corpus genuinely owns a
|
|
72
|
+
* family but some of its numbers belong to somebody else, which is exactly
|
|
73
|
+
* `RFC 2119` in a repository of local RFCs.
|
|
74
|
+
*/
|
|
75
|
+
export function createFamilyFilter(families, ignoreFamilies) {
|
|
76
|
+
const allowed = families && families.length > 0 ? new Set(families.map((f) => f.trim().toUpperCase())) : null;
|
|
77
|
+
const denied = new Set((ignoreFamilies ?? []).map((f) => f.trim().toUpperCase()));
|
|
78
|
+
if (allowed === null && denied.size === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
return (family) => {
|
|
81
|
+
const key = family.toUpperCase();
|
|
82
|
+
if (denied.has(key))
|
|
83
|
+
return true;
|
|
84
|
+
return allowed !== null && !allowed.has(key);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The whole engine, without a filesystem.
|
|
89
|
+
*
|
|
90
|
+
* Exported because it is the honest unit of composition: a monorepo tool that
|
|
91
|
+
* already has the file contents in memory should not have to write them out to
|
|
92
|
+
* a temporary directory to use spec-graph.
|
|
93
|
+
*/
|
|
94
|
+
export function analyseSources(sources, options = {}) {
|
|
95
|
+
const extracted = [];
|
|
96
|
+
for (const source of sources) {
|
|
97
|
+
extracted.push(...extractSpecifications({
|
|
98
|
+
path: source.path,
|
|
99
|
+
text: source.text,
|
|
100
|
+
record: options.isRecord?.(source.path) === true,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
const corpus = resolveCorpus(extracted, {
|
|
104
|
+
fileExists: options.fileExists,
|
|
105
|
+
isIgnoredReference: options.isIgnoredReference,
|
|
106
|
+
isIgnoredFamily: options.isIgnoredFamily,
|
|
107
|
+
});
|
|
108
|
+
const graph = buildGraph({ nodes: corpus.nodes, edges: corpus.edges });
|
|
109
|
+
const diagnostics = runRules(graph, corpus, options);
|
|
110
|
+
return { graph, corpus, diagnostics };
|
|
111
|
+
}
|
|
112
|
+
function finish(analysed, files, started) {
|
|
113
|
+
const { graph, corpus, diagnostics } = analysed;
|
|
114
|
+
const counts = { error: 0, warn: 0, info: 0 };
|
|
115
|
+
for (const diagnostic of diagnostics)
|
|
116
|
+
counts[diagnostic.severity] += 1;
|
|
117
|
+
const records = new Set(corpus.documents.filter((node) => node.phase === 'record').map((node) => node.id));
|
|
118
|
+
const summary = {
|
|
119
|
+
documents: corpus.documents.length,
|
|
120
|
+
items: corpus.items.length,
|
|
121
|
+
edges: graph.edges.length,
|
|
122
|
+
// A record's unchecked boxes are minutes, not work. Counting them would
|
|
123
|
+
// make the headline number the one thing in the report nobody can act on.
|
|
124
|
+
openObligations: corpus.items.filter((item) => item.openness !== 'closed' && !records.has(item.document)).length,
|
|
125
|
+
errors: counts.error,
|
|
126
|
+
warnings: counts.warn,
|
|
127
|
+
infos: counts.info,
|
|
128
|
+
durationMs: Math.round((performance.now() - started) * 100) / 100,
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
graph,
|
|
132
|
+
corpus,
|
|
133
|
+
diagnostics,
|
|
134
|
+
problems: corpus.problems,
|
|
135
|
+
files,
|
|
136
|
+
summary,
|
|
137
|
+
ok: counts.error === 0,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Reads files with bounded concurrency, skipping ones that cannot be read. */
|
|
141
|
+
async function readAll(files, concurrency) {
|
|
142
|
+
const out = new Array(files.length);
|
|
143
|
+
let cursor = 0;
|
|
144
|
+
let written = 0;
|
|
145
|
+
const worker = async () => {
|
|
146
|
+
for (;;) {
|
|
147
|
+
const index = cursor;
|
|
148
|
+
cursor += 1;
|
|
149
|
+
if (index >= files.length)
|
|
150
|
+
return;
|
|
151
|
+
const file = files[index];
|
|
152
|
+
try {
|
|
153
|
+
const text = await readFile(file.absolute, 'utf8');
|
|
154
|
+
out[written] = { path: file.path, text };
|
|
155
|
+
written += 1;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// A file that vanished or cannot be decoded is skipped, not fatal.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const workers = Array.from({ length: Math.max(1, Math.min(concurrency, files.length || 1)) }, worker);
|
|
163
|
+
await Promise.all(workers);
|
|
164
|
+
out.length = written;
|
|
165
|
+
// Deterministic order regardless of which worker finished first.
|
|
166
|
+
return out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Rebuilds a result around a narrower set of findings.
|
|
170
|
+
*
|
|
171
|
+
* The corpus, the graph and the counts of what is in them are unchanged - a
|
|
172
|
+
* baseline suppresses findings, never facts. Only the tallies that describe the
|
|
173
|
+
* findings, and the verdict that follows from them, move.
|
|
174
|
+
*/
|
|
175
|
+
export function withDiagnostics(result, diagnostics) {
|
|
176
|
+
const counts = { error: 0, warn: 0, info: 0 };
|
|
177
|
+
for (const diagnostic of diagnostics)
|
|
178
|
+
counts[diagnostic.severity] += 1;
|
|
179
|
+
return {
|
|
180
|
+
...result,
|
|
181
|
+
diagnostics,
|
|
182
|
+
summary: { ...result.summary, errors: counts.error, warnings: counts.warn, infos: counts.info },
|
|
183
|
+
ok: counts.error === 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Orchestration: files in, graph and findings out.\n *\n * The only part of spec-graph that touches the filesystem. Everything below it\n * is a pure function of text, which is why the test suite can build pathological\n * corpora in memory and why a build script can feed spec-graph documents that\n * never existed on disk.\n */\n\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\n\nimport { extractSpecifications, type ExtractedDocument } from './extract.js';\nimport { createGlobMatcher, createReferenceFilter, walkFiles, type WalkedFile } from './glob.js';\nimport { buildGraph, type SpecGraph } from './graph.js';\nimport { toPosix } from './paths.js';\nimport { resolveCorpus, type ResolvedCorpus } from './resolve.js';\nimport { runRules, type RuleOptions } from './rules.js';\nimport type { AnalysisSummary, Diagnostic, ParseProblem, RuleId, Severity } from './types.js';\n\n/** Patterns used when the caller names none. */\nexport const DEFAULT_PATTERNS: readonly string[] = Object.freeze([\n 'docs/**/*.md',\n 'doc/**/*.md',\n 'adr/**/*.md',\n 'rfcs/**/*.md',\n 'specs/**/*.md',\n '*.md',\n]);\n\n/** How many files are read at once. */\nexport const DEFAULT_CONCURRENCY = 16;\n\nexport interface AnalyseOptions {\n /** Absolute or relative directory the patterns are resolved against. */\n readonly root: string;\n readonly patterns?: readonly string[] | undefined;\n readonly ignore?: readonly string[] | undefined;\n /**\n * Reference targets to leave unreported when they do not resolve.\n *\n * For repositories where `[[...]]` tags concepts rather than naming files.\n * Suppresses findings only; a reference that resolves is still an edge.\n */\n readonly ignoreReferences?: readonly string[] | undefined;\n /** Families a bare identifier may name. Unset means any family in the corpus. */\n readonly families?: readonly string[] | undefined;\n /** Families that are never citations, whatever the corpus contains. */\n readonly ignoreFamilies?: readonly string[] | undefined;\n /**\n * Files that are logs of what was decided rather than decisions.\n *\n * Journals, changelogs, meeting minutes, sprint summaries. Their references\n * are still checked; their obligations and lifecycle are not. See ADR-0011.\n */\n readonly historyPatterns?: readonly string[] | undefined;\n readonly severities?: Partial<Record<RuleId, Severity>> | undefined;\n readonly concurrency?: number | undefined;\n readonly maxRelated?: number | undefined;\n readonly followSymlinks?: boolean | undefined;\n}\n\nexport interface AnalysisResult {\n readonly graph: SpecGraph;\n readonly corpus: ResolvedCorpus;\n readonly diagnostics: readonly Diagnostic[];\n readonly problems: readonly ParseProblem[];\n readonly files: readonly string[];\n readonly summary: AnalysisSummary;\n /** True when nothing at error severity was found. */\n readonly ok: boolean;\n}\n\n/** Reads, extracts, resolves, and checks a corpus of specifications. */\nexport async function analyse(options: AnalyseOptions): Promise<AnalysisResult> {\n const started = performance.now();\n const root = toPosix(options.root).replace(/\\/+$/, '');\n const patterns = options.patterns && options.patterns.length > 0 ? options.patterns : DEFAULT_PATTERNS;\n\n const walked = await walkFiles({\n root,\n patterns,\n ignore: options.ignore,\n followSymlinks: options.followSymlinks,\n });\n\n const sources = await readAll(walked, options.concurrency ?? DEFAULT_CONCURRENCY);\n const present = new Set(walked.map((file) => file.path.toLowerCase()));\n\n return finish(\n analyseSources(sources, {\n // Consulted only for references that failed to resolve, so the cost is\n // bounded by the number of findings - and it buys the difference between\n // \"that document does not exist\" and \"that document exists but your\n // include patterns did not reach it\", which are different fixes.\n fileExists: (path) => present.has(path.toLowerCase()) || existsSync(`${root}/${path}`),\n isIgnoredReference: createReferenceFilter(options.ignoreReferences ?? []),\n isIgnoredFamily: createFamilyFilter(options.families, options.ignoreFamilies),\n isRecord: createHistoryMatcher(options.historyPatterns),\n ...(options.severities !== undefined ? { severities: options.severities } : {}),\n ...(options.maxRelated !== undefined ? { maxRelated: options.maxRelated } : {}),\n }),\n walked.map((file) => file.path),\n started,\n );\n}\n\nexport interface Source {\n /** Repository-relative POSIX path. */\n readonly path: string;\n readonly text: string;\n}\n\nexport interface AnalyseSourcesOptions extends RuleOptions {\n readonly fileExists?: ((path: string) => boolean) | undefined;\n readonly isIgnoredReference?: ((target: string) => boolean) | undefined;\n readonly isIgnoredFamily?: ((family: string) => boolean) | undefined;\n /** Whether a path holds a historical record rather than a specification. */\n readonly isRecord?: ((path: string) => boolean) | undefined;\n}\n\n/**\n * Builds the record predicate, or nothing when no patterns were given.\n *\n * Returning `undefined` rather than a predicate that always says no keeps the\n * common case free of a call per file, and makes \"no history patterns\" visible\n * as an absent option rather than as a function nobody can read.\n */\nexport function createHistoryMatcher(patterns: readonly string[] | undefined): ((path: string) => boolean) | undefined {\n if (patterns === undefined || patterns.length === 0) return undefined;\n return createGlobMatcher(patterns);\n}\n\n/**\n * Builds the family predicate from an allowlist and a denylist.\n *\n * An allowlist is the stronger statement - \"these are the families this\n * repository has\" - and turns every other noun-number construct back into\n * prose. A denylist handles the narrower case where the corpus genuinely owns a\n * family but some of its numbers belong to somebody else, which is exactly\n * `RFC 2119` in a repository of local RFCs.\n */\nexport function createFamilyFilter(\n families: readonly string[] | undefined,\n ignoreFamilies: readonly string[] | undefined,\n): ((family: string) => boolean) | undefined {\n const allowed = families && families.length > 0 ? new Set(families.map((f) => f.trim().toUpperCase())) : null;\n const denied = new Set((ignoreFamilies ?? []).map((f) => f.trim().toUpperCase()));\n if (allowed === null && denied.size === 0) return undefined;\n return (family: string): boolean => {\n const key = family.toUpperCase();\n if (denied.has(key)) return true;\n return allowed !== null && !allowed.has(key);\n };\n}\n\ninterface Analysed {\n readonly graph: SpecGraph;\n readonly corpus: ResolvedCorpus;\n readonly diagnostics: readonly Diagnostic[];\n}\n\n/**\n * The whole engine, without a filesystem.\n *\n * Exported because it is the honest unit of composition: a monorepo tool that\n * already has the file contents in memory should not have to write them out to\n * a temporary directory to use spec-graph.\n */\nexport function analyseSources(sources: readonly Source[], options: AnalyseSourcesOptions = {}): Analysed {\n const extracted: ExtractedDocument[] = [];\n for (const source of sources) {\n extracted.push(\n ...extractSpecifications({\n path: source.path,\n text: source.text,\n record: options.isRecord?.(source.path) === true,\n }),\n );\n }\n\n const corpus = resolveCorpus(extracted, {\n fileExists: options.fileExists,\n isIgnoredReference: options.isIgnoredReference,\n isIgnoredFamily: options.isIgnoredFamily,\n });\n const graph = buildGraph({ nodes: corpus.nodes, edges: corpus.edges });\n const diagnostics = runRules(graph, corpus, options);\n return { graph, corpus, diagnostics };\n}\n\nfunction finish(analysed: Analysed, files: readonly string[], started: number): AnalysisResult {\n const { graph, corpus, diagnostics } = analysed;\n const counts = { error: 0, warn: 0, info: 0 };\n for (const diagnostic of diagnostics) counts[diagnostic.severity] += 1;\n const records = new Set(corpus.documents.filter((node) => node.phase === 'record').map((node) => node.id));\n\n const summary: AnalysisSummary = {\n documents: corpus.documents.length,\n items: corpus.items.length,\n edges: graph.edges.length,\n // A record's unchecked boxes are minutes, not work. Counting them would\n // make the headline number the one thing in the report nobody can act on.\n openObligations: corpus.items.filter((item) => item.openness !== 'closed' && !records.has(item.document)).length,\n errors: counts.error,\n warnings: counts.warn,\n infos: counts.info,\n durationMs: Math.round((performance.now() - started) * 100) / 100,\n };\n\n return {\n graph,\n corpus,\n diagnostics,\n problems: corpus.problems,\n files,\n summary,\n ok: counts.error === 0,\n };\n}\n\n/** Reads files with bounded concurrency, skipping ones that cannot be read. */\nasync function readAll(files: readonly WalkedFile[], concurrency: number): Promise<Source[]> {\n const out: Source[] = new Array(files.length);\n let cursor = 0;\n let written = 0;\n\n const worker = async (): Promise<void> => {\n for (;;) {\n const index = cursor;\n cursor += 1;\n if (index >= files.length) return;\n const file = files[index] as WalkedFile;\n try {\n const text = await readFile(file.absolute, 'utf8');\n out[written] = { path: file.path, text };\n written += 1;\n } catch {\n // A file that vanished or cannot be decoded is skipped, not fatal.\n }\n }\n };\n\n const workers = Array.from({ length: Math.max(1, Math.min(concurrency, files.length || 1)) }, worker);\n await Promise.all(workers);\n out.length = written;\n // Deterministic order regardless of which worker finished first.\n return out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Rebuilds a result around a narrower set of findings.\n *\n * The corpus, the graph and the counts of what is in them are unchanged - a\n * baseline suppresses findings, never facts. Only the tallies that describe the\n * findings, and the verdict that follows from them, move.\n */\nexport function withDiagnostics(result: AnalysisResult, diagnostics: readonly Diagnostic[]): AnalysisResult {\n const counts = { error: 0, warn: 0, info: 0 };\n for (const diagnostic of diagnostics) counts[diagnostic.severity] += 1;\n return {\n ...result,\n diagnostics,\n summary: { ...result.summary, errors: counts.error, warnings: counts.warn, infos: counts.info },\n ok: counts.error === 0,\n };\n}\n"]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specification regions: the part of a file that one specification occupies.
|
|
3
|
+
*
|
|
4
|
+
* `spec-graph` began by assuming one specification per file, because that is
|
|
5
|
+
* how MADR, KEPs and Rust RFCs are laid out. Plenty of organisations do not
|
|
6
|
+
* work that way. A register, a whitepaper or a living architecture document
|
|
7
|
+
* holds dozens of decisions in one file, each with its own identifier, its own
|
|
8
|
+
* status and its own dependencies - and to an engine that binds a document to a
|
|
9
|
+
* file, every one of them is invisible.
|
|
10
|
+
*
|
|
11
|
+
* The fix is small once stated: **a specification is a region of a file, not a
|
|
12
|
+
* file.** A file yields at least one region - itself - and may yield more.
|
|
13
|
+
* Everything downstream is unchanged, because a region produces exactly the
|
|
14
|
+
* same `DocumentNode` a whole file produced.
|
|
15
|
+
*
|
|
16
|
+
* What this module decides is where those regions begin and end, and it is
|
|
17
|
+
* deliberately hard to convince. A heading has to carry an identifier *and* the
|
|
18
|
+
* section has to declare a status before it counts. One signal is not enough:
|
|
19
|
+
* `## Q3 2026 Roadmap` parses as family `Q`, number 3, and `## v1.2.0` in a
|
|
20
|
+
* changelog parses as family `v`, number 1. Neither is a specification, and
|
|
21
|
+
* neither declares a status.
|
|
22
|
+
*/
|
|
23
|
+
import { type Directive } from './directives.js';
|
|
24
|
+
import type { Heading, Range, ScannedDocument, TableRow } from './markdown.js';
|
|
25
|
+
import type { EdgeKind } from './types.js';
|
|
26
|
+
/** A status as written, and where it was written. */
|
|
27
|
+
export interface RegionStatus {
|
|
28
|
+
readonly text: string;
|
|
29
|
+
readonly start: number;
|
|
30
|
+
readonly end: number;
|
|
31
|
+
}
|
|
32
|
+
/** A relation read from a table column rather than from prose. */
|
|
33
|
+
export interface TableRelation {
|
|
34
|
+
readonly kind: EdgeKind;
|
|
35
|
+
readonly inverted: boolean;
|
|
36
|
+
readonly target: string;
|
|
37
|
+
readonly start: number;
|
|
38
|
+
readonly end: number;
|
|
39
|
+
/** The column header that gave it its meaning, for the diagnostic. */
|
|
40
|
+
readonly column: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SpecificationRegion {
|
|
43
|
+
/** The heading that opens the region, or `null` for the file or a table row. */
|
|
44
|
+
readonly heading: Heading | null;
|
|
45
|
+
/** The table row that is the region, when it is one. */
|
|
46
|
+
readonly row: TableRow | null;
|
|
47
|
+
/** Offset the region starts at. */
|
|
48
|
+
readonly start: number;
|
|
49
|
+
/** Offset just past the region. */
|
|
50
|
+
readonly end: number;
|
|
51
|
+
/** Identifier read from the heading or the identifier column. */
|
|
52
|
+
readonly declaredId: string | null;
|
|
53
|
+
readonly title: string | null;
|
|
54
|
+
readonly status: RegionStatus | null;
|
|
55
|
+
/** A `@spec-node` directive inside the region, when there is one. */
|
|
56
|
+
readonly directive: Directive | null;
|
|
57
|
+
/** Relations declared by column, empty for a heading region. */
|
|
58
|
+
readonly relations: readonly TableRelation[];
|
|
59
|
+
/**
|
|
60
|
+
* Cells whose content is already accounted for as a typed relation.
|
|
61
|
+
*
|
|
62
|
+
* Prose scanning skips these, so a link in a `Depends on` column produces one
|
|
63
|
+
* typed edge rather than a typed edge and a neutral citation beside it.
|
|
64
|
+
*/
|
|
65
|
+
readonly claimed: readonly Range[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Finds every specification in a file.
|
|
69
|
+
*
|
|
70
|
+
* The first entry is always the file itself, so a caller that ignores the rest
|
|
71
|
+
* behaves exactly as it did before regions existed.
|
|
72
|
+
*/
|
|
73
|
+
export declare function findSpecificationRegions(scanned: ScannedDocument, directives: readonly Directive[],
|
|
74
|
+
/** The file's own identifier, so its title heading is not read as a sub-region. */
|
|
75
|
+
fileId: string): SpecificationRegion[];
|
|
76
|
+
/**
|
|
77
|
+
* Reads a table whose columns describe specifications.
|
|
78
|
+
*
|
|
79
|
+
* A table qualifies when it has an identifier column *and* either a status
|
|
80
|
+
* column or at least one relation column. Two signals again, for the same
|
|
81
|
+
* reason: a table of identifiers and prose is a citation list, and promoting
|
|
82
|
+
* every row of it to a specification would invent a register nobody wrote.
|
|
83
|
+
*/
|
|
84
|
+
export declare function findTableRegions(scanned: ScannedDocument): SpecificationRegion[];
|
|
85
|
+
/**
|
|
86
|
+
* Reads a status declared inside a region.
|
|
87
|
+
*
|
|
88
|
+
* Two forms, both common: a labelled line (`**Status:** Accepted`) and a
|
|
89
|
+
* sub-heading (`### Status` with the value beneath it). Only the region is
|
|
90
|
+
* searched, so one decision in a register cannot inherit its neighbour's.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readRegionStatus(scanned: ScannedDocument, start: number, end: number): RegionStatus | null;
|
|
93
|
+
/** The region containing an offset, innermost first. `null` when only the file does. */
|
|
94
|
+
export declare function regionAt(regions: readonly SpecificationRegion[], offset: number): SpecificationRegion | null;
|