@descent-vtt/spec-brief 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +56 -6
  3. package/dist/archive.js.map +1 -1
  4. package/dist/brief.js.map +1 -1
  5. package/dist/cli.d.ts +18 -1
  6. package/dist/cli.js +273 -55
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.js +3 -2
  9. package/dist/config.js.map +1 -1
  10. package/dist/corpus.js +1 -0
  11. package/dist/corpus.js.map +1 -1
  12. package/dist/engine.d.ts +43 -0
  13. package/dist/engine.js +74 -12
  14. package/dist/engine.js.map +1 -1
  15. package/dist/fs.d.ts +15 -1
  16. package/dist/fs.js +21 -8
  17. package/dist/fs.js.map +1 -1
  18. package/dist/git.d.ts +11 -0
  19. package/dist/git.js +38 -4
  20. package/dist/git.js.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/lint.js.map +1 -1
  24. package/dist/literal.d.ts +11 -0
  25. package/dist/literal.js +14 -0
  26. package/dist/literal.js.map +1 -0
  27. package/dist/markdown.js.map +1 -1
  28. package/dist/plugins.js +2 -0
  29. package/dist/plugins.js.map +1 -1
  30. package/dist/report.js +3 -2
  31. package/dist/report.js.map +1 -1
  32. package/dist/rules.js +2 -1
  33. package/dist/rules.js.map +1 -1
  34. package/dist/schedule.js.map +1 -1
  35. package/dist/schema.js.map +1 -1
  36. package/dist/scope.js.map +1 -1
  37. package/dist/text.js.map +1 -1
  38. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  39. package/dist/vendor/spec-core/markdown/headings.js.map +1 -1
  40. package/dist/vendor/spec-core/markdown/links.js.map +1 -1
  41. package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
  42. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
  43. package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
  44. package/dist/vendor/spec-core/pattern/regex.js.map +1 -1
  45. package/dist/vendor/spec-core/text/lines.js.map +1 -1
  46. package/dist/vendor/spec-core/text/width.js.map +1 -1
  47. package/package.json +1 -1
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACnG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAElE,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AAgBvD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC;IAC9D,MAAM;IACN,KAAK;IACL,MAAM;IACN,cAAc;IACd,MAAM;IACN,OAAO;IACP,UAAU;IACV,QAAQ;IACR,OAAO;IACP,aAAa;IACb,cAAc;CACf,CAAC,CAAC;AAEH,sFAAsF;AACtF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AACxD,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,KAAK,UAAU,KAAK,CAAC,EAAU;IAC7B,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,OAAO,cAAc;IAChB,IAAI,CAAS;IAEtB,YAAY,IAAY;QACtB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAEO,IAAI,CAAC,IAAY;QACvB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YACpE,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,OAAe;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,qFAAqF;QACrF,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAC7F,0EAA0E;QAC1E,uEAAuE;QACvE,MAAM,SAAS,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;oBAChC,OAAO;gBACT,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,kDAAkD;oBAClD,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAc,CAAC;oBAC7D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,eAAe;wBAAE,MAAM,KAAK,CAAC;oBACpE,mEAAmE;oBACnE,MAAM,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,4FAA4F;YAC5F,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACrC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,SAAiB;QAC1B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7E,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;YACnD,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC;YACzD,iEAAiE;YACjE,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,KAAK,EAAE,SAAiB,EAAiB,EAAE;YACvD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAClE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBACzC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;oBACxB,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;wBAAE,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC9D,CAAC;qBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;oBAC1B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC7D,CAAC;YACH,CAAC;QACH,CAAC,CAAC;QACF,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AAID;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IAClB,KAAK,CAAsB;IACpC,IAAI,CAAoE;IAExE,YAAY,KAAK,GAAqC,EAAE;QACtD,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAChG,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;IACxB,CAAC;IAED,IAAI,CAAC,IAAY;QACf,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,OAAe;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,IAAI;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,MAAM,EAAE,CAAC,CAAC,CAAC;QAClH,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,IAAY;QACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,IAAI;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,MAAM,EAAE,CAAC,CAAC,CAAC;QACpH,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,MAAM,iBAAiB,CAAC,CAAC,CAAC;QAC7F,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,IAAI,CAAC,SAAiB;QACpB,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,WAAW,GAAG,MAAM,KAAK,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YACrC,IAAI,MAAM,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC;gBAAE,SAAS;YAC9D,WAAW,GAAG,IAAI,CAAC;YACnB,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAClE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI;QACF,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtH,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,CAAC,IAAY;QACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,OAAO,CAAC,OAAO,CACpB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAC1G,CAAC;IACJ,CAAC;CACF","sourcesContent":["/**\n * The filesystem, behind an interface.\n *\n * Every path crossing this boundary is repository-relative and POSIX; the\n * Node implementation is the only place a host path exists. The in-memory\n * implementation is not a test double bolted on afterwards: it is how a\n * harness asks \"what would archiving this do\" against files that were never\n * written, and how the transaction's rollback is exercised with a failure on\n * exactly the operation that matters.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative, resolve, sep } from 'node:path';\n\nimport { normalisePath as repoPath } from './links.js';\n\nexport interface FileSystem {\n /** Contents, or `null` when there is no such file. */\n read(path: string): Promise<string | null>;\n /** Writes a file, creating its directory. Replaces an existing file atomically where the platform allows. */\n write(path: string, content: string): Promise<void>;\n /** Removes a file. Removing a file that is absent is an error. */\n remove(path: string): Promise<void>;\n /** Names of the files directly inside a directory, or `null` when it does not exist. */\n list(directory: string): Promise<string[] | null>;\n /** Every file beneath the root, as sorted repository-relative paths, skipping {@link IGNORED_DIRECTORIES}. */\n walk(): Promise<string[]>;\n exists(path: string): Promise<boolean>;\n}\n\n/**\n * The path as the filesystem names it: links and junctions followed, and on\n * Windows short names such as `RUNNER~1` expanded. Git reports the top of a\n * work tree this way, so a root compared with it has to be named the same.\n * A path that does not exist is returned as given, for the caller to report.\n */\nexport async function canonicalPath(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/** Directories a walk never enters: build output and other tools' state. */\nexport const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set([\n '.git',\n '.hg',\n '.svn',\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'target',\n '.venv',\n '__pycache__',\n '.stryker-tmp',\n]);\n\n/** Errors a rename on Windows raises while another process briefly holds the file. */\nconst TRANSIENT = new Set(['EPERM', 'EACCES', 'EBUSY']);\nconst RENAME_ATTEMPTS = 6;\n\nasync function pause(ms: number): Promise<void> {\n await new Promise((done) => setTimeout(done, ms));\n}\n\nexport class NodeFileSystem implements FileSystem {\n readonly root: string;\n\n constructor(root: string) {\n this.root = resolve(root);\n }\n\n private host(path: string): string {\n return join(this.root, ...repoPath(path).split('/'));\n }\n\n async read(path: string): Promise<string | null> {\n try {\n return await readFile(this.host(path), 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n }\n\n async write(path: string, content: string): Promise<void> {\n const target = this.host(path);\n await mkdir(dirname(target), { recursive: true });\n // Written beside the target and renamed over it, so a reader never sees half a file.\n const temporary = join(dirname(target), `.${randomBytes(6).toString('hex')}.spec-brief.tmp`);\n // \"wx\" refuses a name that is taken rather than write over it. Six random\n // bytes are not drawn twice in a test, so none shows the flag at work.\n await writeFile(temporary, content, { flag: 'wx' });\n try {\n for (let attempt = 1; ; attempt += 1) {\n try {\n await rename(temporary, target);\n return;\n } catch (error) {\n // An error with no code is not in the set either.\n const code = (error as NodeJS.ErrnoException).code as string;\n if (!TRANSIENT.has(code) || attempt >= RENAME_ATTEMPTS) throw error;\n // How long it waits changes when the answer comes, not what it is.\n await pause(15 * attempt);\n }\n }\n } catch (error) {\n // Forced, so that a temporary file something else removed does not hide the rename's error.\n await rm(temporary, { force: true });\n throw error;\n }\n }\n\n async remove(path: string): Promise<void> {\n await rm(this.host(path));\n }\n\n async list(directory: string): Promise<string[] | null> {\n try {\n const entries = await readdir(this.host(directory), { withFileTypes: true });\n return entries.filter((e) => e.isFile()).map((e) => e.name);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR') return null;\n // A directory that may not be read must not read as \"no briefs\".\n throw error;\n }\n }\n\n async walk(): Promise<string[]> {\n const found: string[] = [];\n const visit = async (directory: string): Promise<void> => {\n const entries = await readdir(directory, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(directory, entry.name);\n if (entry.isDirectory()) {\n if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(full);\n } else if (entry.isFile()) {\n found.push(relative(this.root, full).split(sep).join('/'));\n }\n }\n };\n await visit(this.root);\n return found.sort();\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await stat(this.host(path));\n return true;\n } catch {\n return false;\n }\n }\n}\n\nexport type FileOperation = 'write' | 'remove';\n\n/**\n * Files in a `Map`. `fail` lets a test make one chosen operation throw, which\n * is the only honest way to prove a rollback restores what it claims to.\n */\nexport class MemoryFileSystem implements FileSystem {\n readonly files: Map<string, string>;\n fail: ((operation: FileOperation, path: string) => boolean) | undefined;\n\n constructor(files: Readonly<Record<string, string>> = {}) {\n this.files = new Map(Object.entries(files).map(([path, content]) => [repoPath(path), content]));\n this.fail = undefined;\n }\n\n read(path: string): Promise<string | null> {\n return Promise.resolve(this.files.get(repoPath(path)) ?? null);\n }\n\n write(path: string, content: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('write', normal) === true) return Promise.reject(new Error(`injected failure writing ${normal}`));\n this.files.set(normal, content);\n return Promise.resolve();\n }\n\n remove(path: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('remove', normal) === true) return Promise.reject(new Error(`injected failure removing ${normal}`));\n if (!this.files.delete(normal)) return Promise.reject(new Error(`${normal} does not exist`));\n return Promise.resolve();\n }\n\n list(directory: string): Promise<string[] | null> {\n const prefix = repoPath(directory);\n const names: string[] = [];\n let isDirectory = prefix === '';\n for (const path of this.files.keys()) {\n if (prefix !== '' && !path.startsWith(`${prefix}/`)) continue;\n isDirectory = true;\n const rest = prefix === '' ? path : path.slice(prefix.length + 1);\n if (!rest.includes('/')) names.push(rest);\n }\n return Promise.resolve(isDirectory ? names.sort() : null);\n }\n\n walk(): Promise<string[]> {\n const paths = [...this.files.keys()].filter((path) => !path.split('/').some((part) => IGNORED_DIRECTORIES.has(part)));\n return Promise.resolve(paths.sort());\n }\n\n exists(path: string): Promise<boolean> {\n const normal = repoPath(path);\n return Promise.resolve(\n this.files.has(normal) || [...this.files.keys()].some((p) => normal === '' || p.startsWith(`${normal}/`)),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The filesystem, behind an interface.\n *\n * Every path crossing this boundary is repository-relative and POSIX; the\n * Node implementation is the only place a host path exists. The in-memory\n * implementation is not a test double bolted on afterwards: it is how a\n * harness asks \"what would archiving this do\" against files that were never\n * written, and how the transaction's rollback is exercised with a failure on\n * exactly the operation that matters.\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative, resolve, sep } from 'node:path';\n\nimport { normalisePath as repoPath } from './links.js';\n\nexport interface FileSystem {\n /** Contents, or `null` when there is no such file. */\n read(path: string): Promise<string | null>;\n /** Writes a file, creating its directory. Replaces an existing file atomically where the platform allows. */\n write(path: string, content: string): Promise<void>;\n /** Removes a file. Removing a file that is absent is an error. */\n remove(path: string): Promise<void>;\n /** Names of the files directly inside a directory, or `null` when it does not exist. */\n list(directory: string): Promise<string[] | null>;\n /** Every file beneath the root, as sorted repository-relative paths, skipping {@link IGNORED_DIRECTORIES}. */\n walk(): Promise<string[]>;\n exists(path: string): Promise<boolean>;\n}\n\n/**\n * What a run read from the disk, by repository-relative path: each file with\n * the SHA-256 of its bytes, or `null` where it was looked for and was not\n * there, and each directory with the names of the files it listed, or\n * `null` where there was none. An answer that names every input it was read\n * from is one another tool may give again without starting this one\n * (ADR-0002, amended 2026-10-19).\n */\nexport interface Reads {\n readonly files: Map<string, string | null>;\n readonly directories: Map<string, readonly string[] | null>;\n}\n\n/**\n * The path as the filesystem names it: links and junctions followed, and on\n * Windows short names such as `RUNNER~1` expanded. Git reports the top of a\n * work tree this way, so a root compared with it has to be named the same.\n * A path that does not exist is returned as given, for the caller to report.\n */\nexport async function canonicalPath(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/** Directories a walk never enters: build output and other tools' state. */\nexport const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set([\n '.git',\n '.hg',\n '.svn',\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'target',\n '.venv',\n '__pycache__',\n '.stryker-tmp',\n]);\n\n/** Errors a rename on Windows raises while another process briefly holds the file. */\nconst TRANSIENT = new Set(['EPERM', 'EACCES', 'EBUSY']);\nconst RENAME_ATTEMPTS = 6;\n\nasync function pause(ms: number): Promise<void> {\n await new Promise((done) => setTimeout(done, ms));\n}\n\nexport class NodeFileSystem implements FileSystem {\n readonly root: string;\n private readonly reads: Reads | undefined;\n\n /** `reads`, where it is given, is told every file read and every directory listed. */\n constructor(root: string, reads?: Reads) {\n this.root = resolve(root);\n this.reads = reads;\n }\n\n private host(path: string): string {\n return join(this.root, ...repoPath(path).split('/'));\n }\n\n async read(path: string): Promise<string | null> {\n let bytes: Buffer;\n try {\n bytes = await readFile(this.host(path));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n this.reads?.files.set(repoPath(path), null);\n return null;\n }\n // The digest is of the bytes the text is decoded from, so that nothing\n // written between a read and a digest of it can go unseen.\n this.reads?.files.set(repoPath(path), createHash('sha256').update(bytes).digest('hex'));\n return bytes.toString('utf8');\n }\n\n async write(path: string, content: string): Promise<void> {\n const target = this.host(path);\n await mkdir(dirname(target), { recursive: true });\n // Written beside the target and renamed over it, so a reader never sees half a file.\n const temporary = join(dirname(target), `.${randomBytes(6).toString('hex')}.spec-brief.tmp`);\n // \"wx\" refuses a name that is taken rather than write over it. Six random\n // bytes are not drawn twice in a test, so none shows the flag at work.\n await writeFile(temporary, content, { flag: 'wx' });\n try {\n for (let attempt = 1; ; attempt += 1) {\n try {\n await rename(temporary, target);\n return;\n } catch (error) {\n // An error with no code is not in the set either.\n const code = (error as NodeJS.ErrnoException).code as string;\n if (!TRANSIENT.has(code) || attempt >= RENAME_ATTEMPTS) throw error;\n // How long it waits changes when the answer comes, not what it is.\n await pause(15 * attempt);\n }\n }\n } catch (error) {\n // Forced, so that a temporary file something else removed does not hide the rename's error.\n await rm(temporary, { force: true });\n throw error;\n }\n }\n\n async remove(path: string): Promise<void> {\n await rm(this.host(path));\n }\n\n async list(directory: string): Promise<string[] | null> {\n try {\n const entries = await readdir(this.host(directory), { withFileTypes: true });\n const names = entries.filter((e) => e.isFile()).map((e) => e.name);\n this.reads?.directories.set(repoPath(directory), names);\n return names;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR') {\n this.reads?.directories.set(repoPath(directory), null);\n return null;\n }\n // A directory that may not be read must not read as \"no briefs\".\n throw error;\n }\n }\n\n async walk(): Promise<string[]> {\n const found: string[] = [];\n const visit = async (directory: string): Promise<void> => {\n const entries = await readdir(directory, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(directory, entry.name);\n if (entry.isDirectory()) {\n if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(full);\n } else if (entry.isFile()) {\n found.push(relative(this.root, full).split(sep).join('/'));\n }\n }\n };\n await visit(this.root);\n return found.sort();\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await stat(this.host(path));\n return true;\n } catch {\n return false;\n }\n }\n}\n\nexport type FileOperation = 'write' | 'remove';\n\n/**\n * Files in a `Map`. `fail` lets a test make one chosen operation throw, which\n * is the only honest way to prove a rollback restores what it claims to.\n */\nexport class MemoryFileSystem implements FileSystem {\n readonly files: Map<string, string>;\n fail: ((operation: FileOperation, path: string) => boolean) | undefined;\n\n constructor(files: Readonly<Record<string, string>> = {}) {\n this.files = new Map(Object.entries(files).map(([path, content]) => [repoPath(path), content]));\n this.fail = undefined;\n }\n\n read(path: string): Promise<string | null> {\n return Promise.resolve(this.files.get(repoPath(path)) ?? null);\n }\n\n write(path: string, content: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('write', normal) === true) return Promise.reject(new Error(`injected failure writing ${normal}`));\n this.files.set(normal, content);\n return Promise.resolve();\n }\n\n remove(path: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('remove', normal) === true) return Promise.reject(new Error(`injected failure removing ${normal}`));\n if (!this.files.delete(normal)) return Promise.reject(new Error(`${normal} does not exist`));\n return Promise.resolve();\n }\n\n list(directory: string): Promise<string[] | null> {\n const prefix = repoPath(directory);\n const names: string[] = [];\n let isDirectory = prefix === '';\n for (const path of this.files.keys()) {\n if (prefix !== '' && !path.startsWith(`${prefix}/`)) continue;\n isDirectory = true;\n const rest = prefix === '' ? path : path.slice(prefix.length + 1);\n if (!rest.includes('/')) names.push(rest);\n }\n return Promise.resolve(isDirectory ? names.sort() : null);\n }\n\n walk(): Promise<string[]> {\n const paths = [...this.files.keys()].filter((path) => !path.split('/').some((part) => IGNORED_DIRECTORIES.has(part)));\n return Promise.resolve(paths.sort());\n }\n\n exists(path: string): Promise<boolean> {\n const normal = repoPath(path);\n return Promise.resolve(\n this.files.has(normal) || [...this.files.keys()].some((p) => normal === '' || p.startsWith(`${normal}/`)),\n );\n }\n}\n"]}
package/dist/git.d.ts CHANGED
@@ -22,6 +22,11 @@ export interface FileChange {
22
22
  export interface Git {
23
23
  /** The commit a revision names, or `null` when it names none. */
24
24
  commit(revision: string): Promise<CommitInfo | null>;
25
+ /**
26
+ * The hash of the commit each revision names, or `null` for one that names
27
+ * none, asked once. A git without it is one that cannot say.
28
+ */
29
+ commits?(revisions: readonly string[]): Promise<(string | null)[]>;
25
30
  mergeBase(a: string, b: string): Promise<string | null>;
26
31
  /** Files changed from `from` to `to`; with no `from`, the files `to` changed against its first parent. */
27
32
  changes(from: string | null, to: string): Promise<FileChange[]>;
@@ -57,6 +62,12 @@ export declare class NodeGit implements Git {
57
62
  /** The working tree's top directory, canonical, or `null` outside a repository. */
58
63
  static toplevel(cwd: string): Promise<string | null>;
59
64
  commit(revision: string): Promise<CommitInfo | null>;
65
+ /**
66
+ * One process for every revision: `cat-file --batch-check` reads one a
67
+ * line, as `rev-parse --verify` reads one, and answers each with its
68
+ * object's name, or with the revision and why it names none.
69
+ */
70
+ commits(revisions: readonly string[]): Promise<(string | null)[]>;
60
71
  mergeBase(a: string, b: string): Promise<string | null>;
61
72
  changes(from: string | null, to: string): Promise<FileChange[]>;
62
73
  dirty(): Promise<string[]>;
package/dist/git.js CHANGED
@@ -22,11 +22,21 @@ export class GitError extends Error {
22
22
  this.name = 'GitError';
23
23
  }
24
24
  }
25
- function run(cwd, args) {
25
+ /** A git command, handed `input` on its standard input, and an end of it there either way. */
26
+ function run(cwd, args, input) {
26
27
  return new Promise((done) => {
27
- execFile('git', ['-c', 'core.quotepath=off', ...args],
28
+ const child = execFile('git', ['-c', 'core.quotepath=off', ...args],
28
29
  // windowsHide keeps a console window from flashing up on Windows; no other host has one to hide.
29
30
  { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => done({ ok: error === null, stdout, stderr }));
31
+ // A git that ends before it reads what it is handed, as one outside a
32
+ // repository does, closes the pipe under the write, and the EPIPE went
33
+ // uncaught and ended the process. What git printed is its answer, so the
34
+ // error is taken and nothing is done with it; the mutant that gives the
35
+ // handler a body of `undefined` does nothing either, and is equivalent.
36
+ child.stdin.on('error', () => { });
37
+ // A command that reads its input reads to the end of it, which only this
38
+ // gives it; the others read none.
39
+ child.stdin.end(input);
30
40
  });
31
41
  }
32
42
  /** A revision that starts with "-" would be read as an option. */
@@ -42,7 +52,9 @@ function records(output) {
42
52
  export function parseNumstat(output) {
43
53
  // `show --format=` leaves the empty header's newline in front of the first record.
44
54
  return records(output.replace(/^\n+/, '')).map((record) => {
45
- const [added, deleted, ...path] = record.split('\t');
55
+ const fields = record.split('\t');
56
+ // Cast on the name: what stands under an `as` is not mutated (ADR-0009).
57
+ const [added, deleted, ...path] = fields;
46
58
  return {
47
59
  path: path.join('\t'),
48
60
  insertions: added === '-' ? null : Number(added),
@@ -91,9 +103,31 @@ export class NodeGit {
91
103
  return null;
92
104
  const hash = sha.stdout.trim();
93
105
  const show = await run(this.cwd, ['show', '-s', '--format=%an%x00%aI', hash]);
94
- const [author, date] = show.stdout.trim().split('\0');
106
+ const fields = show.stdout.trim().split('\0');
107
+ // Cast on the name, as in `parseNumstat`.
108
+ const [author, date] = fields;
95
109
  return { sha: hash, author, date };
96
110
  }
111
+ /**
112
+ * One process for every revision: `cat-file --batch-check` reads one a
113
+ * line, as `rev-parse --verify` reads one, and answers each with its
114
+ * object's name, or with the revision and why it names none.
115
+ */
116
+ async commits(revisions) {
117
+ // A line break would make one revision two questions, and its answer
118
+ // another's. On a line of its own a revision is never read as an option,
119
+ // so one that starts with "-" is asked, and names none.
120
+ if (revisions.some((revision) => revision.includes('\n')))
121
+ return revisions.map(() => null);
122
+ const asked = revisions.map((revision) => `${revision}^{commit}\n`).join('');
123
+ const lines = (await run(this.cwd, ['cat-file', '--batch-check=%(objectname)'], asked)).stdout.split('\n');
124
+ return revisions.map((_, index) => {
125
+ const line = lines[index];
126
+ // An object's name holds no space, and the answer for a revision that
127
+ // names none does; a git that answered nothing printed no line for it.
128
+ return line !== undefined && line !== '' && !line.includes(' ') ? line : null;
129
+ });
130
+ }
97
131
  async mergeBase(a, b) {
98
132
  const result = await run(this.cwd, ['merge-base', safeRevision(a), safeRevision(b)]);
99
133
  return result.ok ? result.stdout.trim() : null;
package/dist/git.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Git, read-only.\n *\n * spec-brief never stages, commits or rewrites history. It reads the commit a\n * round landed as, the files that commit touched, and whether the working tree\n * holds work the commit does not. What goes into a commit stays the decision\n * of whoever makes it, and archival is atomic over the files it writes rather\n * than over a repository state it would have to reverse.\n */\n\nimport { execFile } from 'node:child_process';\nimport { resolve } from 'node:path';\n\nimport { canonicalPath } from './fs.js';\nimport { relativePath } from './links.js';\n\nexport interface CommitInfo {\n readonly sha: string;\n readonly author: string;\n /** ISO 8601 author date. */\n readonly date: string;\n}\n\nexport interface FileChange {\n readonly path: string;\n /** `null` for a binary file, which has no line counts. */\n readonly insertions: number | null;\n readonly deletions: number | null;\n}\n\nexport interface Git {\n /** The commit a revision names, or `null` when it names none. */\n commit(revision: string): Promise<CommitInfo | null>;\n mergeBase(a: string, b: string): Promise<string | null>;\n /** Files changed from `from` to `to`; with no `from`, the files `to` changed against its first parent. */\n changes(from: string | null, to: string): Promise<FileChange[]>;\n /** Paths with uncommitted changes, untracked files included. */\n dirty(): Promise<string[]>;\n /** Every path git sees: tracked, or untracked and not ignored. A file a round just created counts. */\n files(): Promise<string[]>;\n remoteUrl(name: string): Promise<string | null>;\n}\n\n/**\n * A revision git cannot be handed, or a command of git's that failed: the\n * message is the whole report, since what failed is git or what it was given,\n * and no line of spec-brief's.\n */\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\ninterface Run {\n readonly ok: boolean;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nfunction run(cwd: string, args: readonly string[]): Promise<Run> {\n return new Promise((done) => {\n execFile(\n 'git',\n ['-c', 'core.quotepath=off', ...args],\n // windowsHide keeps a console window from flashing up on Windows; no other host has one to hide.\n { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => done({ ok: error === null, stdout, stderr }),\n );\n });\n}\n\n/** A revision that starts with \"-\" would be read as an option. */\nfunction safeRevision(revision: string): string {\n if (revision.startsWith('-') || revision.trim() === '') throw new GitError(`\"${revision}\" is not a revision`);\n return revision;\n}\n\nfunction records(output: string): string[] {\n return output.split('\\0').filter((r) => r !== '');\n}\n\n/** Parses `--numstat -z` output with renames off: `added<TAB>deleted<TAB>path<NUL>`. */\nexport function parseNumstat(output: string): FileChange[] {\n // `show --format=` leaves the empty header's newline in front of the first record.\n return records(output.replace(/^\\n+/, '')).map((record) => {\n const [added, deleted, ...path] = record.split('\\t') as [string, string, ...string[]];\n return {\n path: path.join('\\t'),\n insertions: added === '-' ? null : Number(added),\n deletions: deleted === '-' ? null : Number(deleted),\n };\n });\n}\n\n/** Parses `status --porcelain=v1 -z`, where a rename's original path is a record of its own. */\nexport function parsePorcelain(output: string): string[] {\n const fields = records(output);\n const paths: string[] = [];\n for (let i = 0; i < fields.length; i += 1) {\n const field = fields[i] as string;\n paths.push(field.slice(3));\n const x = field.charAt(0);\n if (x === 'R' || x === 'C') i += 1;\n }\n return paths;\n}\n\n/**\n * Git reports paths from the top of the work tree; spec-brief's paths start at\n * the root, which may be a directory below it. Every path that crosses this\n * boundary is made relative to the root.\n */\nexport class NodeGit implements Git {\n readonly cwd: string;\n private prefix: Promise<string> | undefined;\n\n constructor(cwd: string) {\n this.cwd = cwd;\n }\n\n /** The root's path from the top of the work tree, `sub/dir/` or empty. */\n private async rootPrefix(): Promise<string> {\n // Asked only after a status that succeeded, so the repository exists.\n this.prefix ??= run(this.cwd, ['rev-parse', '--show-prefix']).then((r) => r.stdout.trim());\n return this.prefix;\n }\n\n /** The working tree's top directory, canonical, or `null` outside a repository. */\n static async toplevel(cwd: string): Promise<string | null> {\n const result = await run(cwd, ['rev-parse', '--show-toplevel']);\n return result.ok ? canonicalPath(resolve(result.stdout.trim())) : null;\n }\n\n async commit(revision: string): Promise<CommitInfo | null> {\n const sha = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${safeRevision(revision)}^{commit}`]);\n if (!sha.ok) return null;\n const hash = sha.stdout.trim();\n const show = await run(this.cwd, ['show', '-s', '--format=%an%x00%aI', hash]);\n const [author, date] = show.stdout.trim().split('\\0') as [string, string];\n return { sha: hash, author, date };\n }\n\n async mergeBase(a: string, b: string): Promise<string | null> {\n const result = await run(this.cwd, ['merge-base', safeRevision(a), safeRevision(b)]);\n return result.ok ? result.stdout.trim() : null;\n }\n\n async changes(from: string | null, to: string): Promise<FileChange[]> {\n const target = safeRevision(to);\n let result: Run;\n if (from !== null) {\n result = await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', safeRevision(from), target]);\n } else {\n const parent = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${target}^`]);\n result = parent.ok\n ? await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', `${target}^`, target])\n : await run(this.cwd, ['show', '--numstat', '-z', '--no-renames', '--relative', '--format=', target]);\n }\n if (!result.ok) throw new GitError(`git could not diff ${from ?? `${to}^`}..${to}: ${result.stderr.trim()}`);\n return parseNumstat(result.stdout);\n }\n\n async dirty(): Promise<string[]> {\n const result = await run(this.cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);\n if (!result.ok) throw new GitError(`git status failed: ${result.stderr.trim()}`);\n const prefix = await this.rootPrefix();\n return parsePorcelain(result.stdout).map((path) => relativePath(prefix, path));\n }\n\n async files(): Promise<string[]> {\n const result = await run(this.cwd, ['ls-files', '-z', '--cached', '--others', '--exclude-standard']);\n if (!result.ok) throw new GitError(`git ls-files failed: ${result.stderr.trim()}`);\n return records(result.stdout).sort();\n }\n\n async remoteUrl(name: string): Promise<string | null> {\n const result = await run(this.cwd, ['remote', 'get-url', safeRevision(name)]);\n return result.ok ? result.stdout.trim() : null;\n }\n}\n\n/** The web address of a pull request, for remotes on github.com. */\nexport function pullRequestUrl(remote: string | null, number: number): string | null {\n // Said for the reader and the compiler: no remote reads as the text \"null\", which is no address either.\n if (remote === null) return null;\n const match = /github\\.com[:/]([^/\\s]+)\\/([^/\\s]+?)(?:\\.git)?\\/?$/.exec(remote);\n if (!match) return null;\n return `https://github.com/${match[1] as string}/${match[2] as string}/pull/${number}`;\n}\n"]}
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Git, read-only.\n *\n * spec-brief never stages, commits or rewrites history. It reads the commit a\n * round landed as, the files that commit touched, and whether the working tree\n * holds work the commit does not. What goes into a commit stays the decision\n * of whoever makes it, and archival is atomic over the files it writes rather\n * than over a repository state it would have to reverse.\n */\n\nimport { execFile } from 'node:child_process';\nimport { resolve } from 'node:path';\n\nimport { canonicalPath } from './fs.js';\nimport { relativePath } from './links.js';\n\nexport interface CommitInfo {\n readonly sha: string;\n readonly author: string;\n /** ISO 8601 author date. */\n readonly date: string;\n}\n\nexport interface FileChange {\n readonly path: string;\n /** `null` for a binary file, which has no line counts. */\n readonly insertions: number | null;\n readonly deletions: number | null;\n}\n\nexport interface Git {\n /** The commit a revision names, or `null` when it names none. */\n commit(revision: string): Promise<CommitInfo | null>;\n /**\n * The hash of the commit each revision names, or `null` for one that names\n * none, asked once. A git without it is one that cannot say.\n */\n commits?(revisions: readonly string[]): Promise<(string | null)[]>;\n mergeBase(a: string, b: string): Promise<string | null>;\n /** Files changed from `from` to `to`; with no `from`, the files `to` changed against its first parent. */\n changes(from: string | null, to: string): Promise<FileChange[]>;\n /** Paths with uncommitted changes, untracked files included. */\n dirty(): Promise<string[]>;\n /** Every path git sees: tracked, or untracked and not ignored. A file a round just created counts. */\n files(): Promise<string[]>;\n remoteUrl(name: string): Promise<string | null>;\n}\n\n/**\n * A revision git cannot be handed, or a command of git's that failed: the\n * message is the whole report, since what failed is git or what it was given,\n * and no line of spec-brief's.\n */\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\ninterface Run {\n readonly ok: boolean;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** A git command, handed `input` on its standard input, and an end of it there either way. */\nfunction run(cwd: string, args: readonly string[], input?: string): Promise<Run> {\n return new Promise((done) => {\n const child = execFile(\n 'git',\n ['-c', 'core.quotepath=off', ...args],\n // windowsHide keeps a console window from flashing up on Windows; no other host has one to hide.\n { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => done({ ok: error === null, stdout, stderr }),\n );\n // A git that ends before it reads what it is handed, as one outside a\n // repository does, closes the pipe under the write, and the EPIPE went\n // uncaught and ended the process. What git printed is its answer, so the\n // error is taken and nothing is done with it; the mutant that gives the\n // handler a body of `undefined` does nothing either, and is equivalent.\n child.stdin!.on('error', () => {});\n // A command that reads its input reads to the end of it, which only this\n // gives it; the others read none.\n child.stdin!.end(input);\n });\n}\n\n/** A revision that starts with \"-\" would be read as an option. */\nfunction safeRevision(revision: string): string {\n if (revision.startsWith('-') || revision.trim() === '') throw new GitError(`\"${revision}\" is not a revision`);\n return revision;\n}\n\nfunction records(output: string): string[] {\n return output.split('\\0').filter((r) => r !== '');\n}\n\n/** Parses `--numstat -z` output with renames off: `added<TAB>deleted<TAB>path<NUL>`. */\nexport function parseNumstat(output: string): FileChange[] {\n // `show --format=` leaves the empty header's newline in front of the first record.\n return records(output.replace(/^\\n+/, '')).map((record) => {\n const fields = record.split('\\t');\n // Cast on the name: what stands under an `as` is not mutated (ADR-0009).\n const [added, deleted, ...path] = fields as [string, string, ...string[]];\n return {\n path: path.join('\\t'),\n insertions: added === '-' ? null : Number(added),\n deletions: deleted === '-' ? null : Number(deleted),\n };\n });\n}\n\n/** Parses `status --porcelain=v1 -z`, where a rename's original path is a record of its own. */\nexport function parsePorcelain(output: string): string[] {\n const fields = records(output);\n const paths: string[] = [];\n for (let i = 0; i < fields.length; i += 1) {\n const field = fields[i] as string;\n paths.push(field.slice(3));\n const x = field.charAt(0);\n if (x === 'R' || x === 'C') i += 1;\n }\n return paths;\n}\n\n/**\n * Git reports paths from the top of the work tree; spec-brief's paths start at\n * the root, which may be a directory below it. Every path that crosses this\n * boundary is made relative to the root.\n */\nexport class NodeGit implements Git {\n readonly cwd: string;\n private prefix: Promise<string> | undefined;\n\n constructor(cwd: string) {\n this.cwd = cwd;\n }\n\n /** The root's path from the top of the work tree, `sub/dir/` or empty. */\n private async rootPrefix(): Promise<string> {\n // Asked only after a status that succeeded, so the repository exists.\n this.prefix ??= run(this.cwd, ['rev-parse', '--show-prefix']).then((r) => r.stdout.trim());\n return this.prefix;\n }\n\n /** The working tree's top directory, canonical, or `null` outside a repository. */\n static async toplevel(cwd: string): Promise<string | null> {\n const result = await run(cwd, ['rev-parse', '--show-toplevel']);\n return result.ok ? canonicalPath(resolve(result.stdout.trim())) : null;\n }\n\n async commit(revision: string): Promise<CommitInfo | null> {\n const sha = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${safeRevision(revision)}^{commit}`]);\n if (!sha.ok) return null;\n const hash = sha.stdout.trim();\n const show = await run(this.cwd, ['show', '-s', '--format=%an%x00%aI', hash]);\n const fields = show.stdout.trim().split('\\0');\n // Cast on the name, as in `parseNumstat`.\n const [author, date] = fields as [string, string];\n return { sha: hash, author, date };\n }\n\n /**\n * One process for every revision: `cat-file --batch-check` reads one a\n * line, as `rev-parse --verify` reads one, and answers each with its\n * object's name, or with the revision and why it names none.\n */\n async commits(revisions: readonly string[]): Promise<(string | null)[]> {\n // A line break would make one revision two questions, and its answer\n // another's. On a line of its own a revision is never read as an option,\n // so one that starts with \"-\" is asked, and names none.\n if (revisions.some((revision) => revision.includes('\\n'))) return revisions.map(() => null);\n const asked = revisions.map((revision) => `${revision}^{commit}\\n`).join('');\n const lines = (await run(this.cwd, ['cat-file', '--batch-check=%(objectname)'], asked)).stdout.split('\\n');\n return revisions.map((_, index) => {\n const line = lines[index];\n // An object's name holds no space, and the answer for a revision that\n // names none does; a git that answered nothing printed no line for it.\n return line !== undefined && line !== '' && !line.includes(' ') ? line : null;\n });\n }\n\n async mergeBase(a: string, b: string): Promise<string | null> {\n const result = await run(this.cwd, ['merge-base', safeRevision(a), safeRevision(b)]);\n return result.ok ? result.stdout.trim() : null;\n }\n\n async changes(from: string | null, to: string): Promise<FileChange[]> {\n const target = safeRevision(to);\n let result: Run;\n if (from !== null) {\n result = await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', safeRevision(from), target]);\n } else {\n const parent = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${target}^`]);\n result = parent.ok\n ? await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', `${target}^`, target])\n : await run(this.cwd, ['show', '--numstat', '-z', '--no-renames', '--relative', '--format=', target]);\n }\n if (!result.ok) throw new GitError(`git could not diff ${from ?? `${to}^`}..${to}: ${result.stderr.trim()}`);\n return parseNumstat(result.stdout);\n }\n\n async dirty(): Promise<string[]> {\n const result = await run(this.cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);\n if (!result.ok) throw new GitError(`git status failed: ${result.stderr.trim()}`);\n const prefix = await this.rootPrefix();\n return parsePorcelain(result.stdout).map((path) => relativePath(prefix, path));\n }\n\n async files(): Promise<string[]> {\n const result = await run(this.cwd, ['ls-files', '-z', '--cached', '--others', '--exclude-standard']);\n if (!result.ok) throw new GitError(`git ls-files failed: ${result.stderr.trim()}`);\n return records(result.stdout).sort();\n }\n\n async remoteUrl(name: string): Promise<string | null> {\n const result = await run(this.cwd, ['remote', 'get-url', safeRevision(name)]);\n return result.ok ? result.stdout.trim() : null;\n }\n}\n\n/** The web address of a pull request, for remotes on github.com. */\nexport function pullRequestUrl(remote: string | null, number: number): string | null {\n // Said for the reader and the compiler: no remote reads as the text \"null\", which is no address either.\n if (remote === null) return null;\n const match = /github\\.com[:/]([^/\\s]+)\\/([^/\\s]+?)(?:\\.git)?\\/?$/.exec(remote);\n if (!match) return null;\n return `https://github.com/${match[1] as string}/${match[2] as string}/pull/${number}`;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -9,8 +9,8 @@ export { main, type CliIO, EXIT_ERROR, EXIT_FAILED, EXIT_OK, HELP, UsageError }
9
9
  export { type Collision, collisionFindings, type CollisionOptions, type CollisionReport, collisions, type Overlap, type SharedDirectory, type UndecidedPair, type WaveMatrix, } from './collisions.js';
10
10
  export { BANNER_PLACEHOLDERS, type Config, CONFIG_FILES, CONFIG_SCHEMA, ConfigError, configJsonSchema, DEFAULT_CONFIG, parseConfig, type PluginReference, resolveConfig, type SectionRule, } from './config.js';
11
11
  export { buildCorpus, type Corpus, dependencyCycles, findBriefs, isReady, pendingDependencies, resolveDependency, type SourceFile, type Waiting, waitingOnDeferred, } from './corpus.js';
12
- export { type ArchiveOptions, BriefEngine, EngineError, type EngineSetup, type NewOptions, type OpenOptions, today } from './engine.js';
13
- export { type FileSystem, MemoryFileSystem, NodeFileSystem } from './fs.js';
12
+ export { type ArchiveOptions, BriefEngine, EngineError, type EngineSetup, type Inputs, type NewOptions, type OpenOptions, today } from './engine.js';
13
+ export { type FileSystem, MemoryFileSystem, NodeFileSystem, type Reads } from './fs.js';
14
14
  export { type CommitInfo, type FileChange, type Git, GitError, NodeGit } from './git.js';
15
15
  export { type Glob, globBase, globBases, globCovers, globWitness, intersectGlobs, type Literal, type LiteralReading, matchGlob, parseGlob, type ParseOptions, readingIn, WITNESS_BUDGET, type Witness, } from './glob.js';
16
16
  export { integrityOf } from './integrity.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EACL,YAAY,EAGZ,SAAS,EAET,WAAW,EACX,aAAa,EAEb,YAAY,EAGZ,QAAQ,GAET,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAc,eAAe,EAAqB,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC/H,OAAO,EAAE,IAAI,EAAc,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAChG,OAAO,EAEL,iBAAiB,EAGjB,UAAU,GAKX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,mBAAmB,EAEnB,YAAY,EACZ,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,WAAW,EAEX,aAAa,GAEd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EAEX,gBAAgB,EAChB,UAAU,EACV,OAAO,EACP,mBAAmB,EACnB,iBAAiB,EAGjB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,WAAW,EAAE,WAAW,EAAuD,KAAK,EAAE,MAAM,aAAa,CAAC;AACxI,OAAO,EAAmB,gBAAgB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EAA8C,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACzF,OAAO,EAEL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,WAAW,EACX,cAAc,EAGd,SAAS,EACT,SAAS,EAET,SAAS,EACT,cAAc,GAEf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAiC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAqB,MAAM,WAAW,CAAC;AAC9H,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAe,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1J,OAAO,EAAE,aAAa,EAAE,eAAe,EAA+D,KAAK,EAAE,MAAM,YAAY,CAAC;AAChI,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAEL,aAAa,EACb,KAAK,EACL,KAAK,EAGL,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,gBAAgB,GAIjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,IAAI,EAA4B,OAAO,EAAqB,MAAM,YAAY,CAAC;AAGxG,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAuC,MAAS;IAC1E,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * The library. Everything the CLI does is reachable from here, and the\n * CLI is built from nothing else.\n */\n\nexport { applyPlan, ConflictError, TransactionError } from './apply.js';\nexport {\n applyWaivers,\n type ArchiveRequest,\n type FileOp,\n openTasks,\n type Plan,\n planArchive,\n planUnarchive,\n type PullRequest,\n renderBanner,\n type UnarchiveRequest,\n type Unmeasured,\n WAIVABLE,\n type Waiver,\n} from './archive.js';\nexport { BANNER_CLOSE, BANNER_OPEN, type Brief, BUILT_IN_FIELDS, type FieldProblem, idFromName, parseBrief } from './brief.js';\nexport { main, type CliIO, EXIT_ERROR, EXIT_FAILED, EXIT_OK, HELP, UsageError } from './cli.js';\nexport {\n type Collision,\n collisionFindings,\n type CollisionOptions,\n type CollisionReport,\n collisions,\n type Overlap,\n type SharedDirectory,\n type UndecidedPair,\n type WaveMatrix,\n} from './collisions.js';\nexport {\n BANNER_PLACEHOLDERS,\n type Config,\n CONFIG_FILES,\n CONFIG_SCHEMA,\n ConfigError,\n configJsonSchema,\n DEFAULT_CONFIG,\n parseConfig,\n type PluginReference,\n resolveConfig,\n type SectionRule,\n} from './config.js';\nexport {\n buildCorpus,\n type Corpus,\n dependencyCycles,\n findBriefs,\n isReady,\n pendingDependencies,\n resolveDependency,\n type SourceFile,\n type Waiting,\n waitingOnDeferred,\n} from './corpus.js';\nexport { type ArchiveOptions, BriefEngine, EngineError, type EngineSetup, type NewOptions, type OpenOptions, today } from './engine.js';\nexport { type FileSystem, MemoryFileSystem, NodeFileSystem } from './fs.js';\nexport { type CommitInfo, type FileChange, type Git, GitError, NodeGit } from './git.js';\nexport {\n type Glob,\n globBase,\n globBases,\n globCovers,\n globWitness,\n intersectGlobs,\n type Literal,\n type LiteralReading,\n matchGlob,\n parseGlob,\n type ParseOptions,\n readingIn,\n WITNESS_BUDGET,\n type Witness,\n} from './glob.js';\nexport { integrityOf } from './integrity.js';\nexport { failing, lint, type LintOptions, type Plugin, ruleIds, sortFindings, summarise, type WaiveContext } from './lint.js';\nexport { asPlugin, asWaivers, loadPlugins } from './plugins.js';\nexport { type Format, FORMATS, githubCommands, gitlabCodeQuality, JSON_SCHEMA_VERSION, matrixJson, sarif, scheduleJson, sectionsJson } from './report.js';\nexport { ARCHIVE_RULES, COLLISION_RULES, type Rule, type RuleContext, type RuleInfo, type RuleResult, RULES } from './rules.js';\nexport { nextId, renderNewBrief } from './scaffold.js';\nexport {\n type Breach,\n breachReasons,\n holds,\n moves,\n type Passed,\n type Placement,\n planWaves,\n reasons,\n type Schedule,\n schedule,\n scheduleFindings,\n type ScheduleOptions,\n type Unplaced,\n type WavePlan,\n} from './schedule.js';\nexport { contradictions, meet, type Meeting, type Scope, scopeOf, type ScopePattern } from './scope.js';\nexport type { Finding, Phase, Severity, SeveritySetting, Status } from './types.js';\n\n/** Types a plugin author writes against, with inference for the rule list. */\nexport function definePlugin<T extends import('./lint.js').Plugin>(plugin: T): T {\n return plugin;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EACL,YAAY,EAGZ,SAAS,EAET,WAAW,EACX,aAAa,EAEb,YAAY,EAGZ,QAAQ,GAET,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAc,eAAe,EAAqB,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC/H,OAAO,EAAE,IAAI,EAAc,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAChG,OAAO,EAEL,iBAAiB,EAGjB,UAAU,GAKX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,mBAAmB,EAEnB,YAAY,EACZ,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,WAAW,EAEX,aAAa,GAEd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EAEX,gBAAgB,EAChB,UAAU,EACV,OAAO,EACP,mBAAmB,EACnB,iBAAiB,EAGjB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,WAAW,EAAE,WAAW,EAAoE,KAAK,EAAE,MAAM,aAAa,CAAC;AACrJ,OAAO,EAAmB,gBAAgB,EAAE,cAAc,EAAc,MAAM,SAAS,CAAC;AACxF,OAAO,EAA8C,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACzF,OAAO,EAEL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,WAAW,EACX,cAAc,EAGd,SAAS,EACT,SAAS,EAET,SAAS,EACT,cAAc,GAEf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAiC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAqB,MAAM,WAAW,CAAC;AAC9H,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAe,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1J,OAAO,EAAE,aAAa,EAAE,eAAe,EAA+D,KAAK,EAAE,MAAM,YAAY,CAAC;AAChI,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAEL,aAAa,EACb,KAAK,EACL,KAAK,EAGL,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,gBAAgB,GAIjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,IAAI,EAA4B,OAAO,EAAqB,MAAM,YAAY,CAAC;AAGxG,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAuC,MAAS;IAC1E,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * The library. Everything the CLI does is reachable from here, and the\n * CLI is built from nothing else.\n */\n\nexport { applyPlan, ConflictError, TransactionError } from './apply.js';\nexport {\n applyWaivers,\n type ArchiveRequest,\n type FileOp,\n openTasks,\n type Plan,\n planArchive,\n planUnarchive,\n type PullRequest,\n renderBanner,\n type UnarchiveRequest,\n type Unmeasured,\n WAIVABLE,\n type Waiver,\n} from './archive.js';\nexport { BANNER_CLOSE, BANNER_OPEN, type Brief, BUILT_IN_FIELDS, type FieldProblem, idFromName, parseBrief } from './brief.js';\nexport { main, type CliIO, EXIT_ERROR, EXIT_FAILED, EXIT_OK, HELP, UsageError } from './cli.js';\nexport {\n type Collision,\n collisionFindings,\n type CollisionOptions,\n type CollisionReport,\n collisions,\n type Overlap,\n type SharedDirectory,\n type UndecidedPair,\n type WaveMatrix,\n} from './collisions.js';\nexport {\n BANNER_PLACEHOLDERS,\n type Config,\n CONFIG_FILES,\n CONFIG_SCHEMA,\n ConfigError,\n configJsonSchema,\n DEFAULT_CONFIG,\n parseConfig,\n type PluginReference,\n resolveConfig,\n type SectionRule,\n} from './config.js';\nexport {\n buildCorpus,\n type Corpus,\n dependencyCycles,\n findBriefs,\n isReady,\n pendingDependencies,\n resolveDependency,\n type SourceFile,\n type Waiting,\n waitingOnDeferred,\n} from './corpus.js';\nexport { type ArchiveOptions, BriefEngine, EngineError, type EngineSetup, type Inputs, type NewOptions, type OpenOptions, today } from './engine.js';\nexport { type FileSystem, MemoryFileSystem, NodeFileSystem, type Reads } from './fs.js';\nexport { type CommitInfo, type FileChange, type Git, GitError, NodeGit } from './git.js';\nexport {\n type Glob,\n globBase,\n globBases,\n globCovers,\n globWitness,\n intersectGlobs,\n type Literal,\n type LiteralReading,\n matchGlob,\n parseGlob,\n type ParseOptions,\n readingIn,\n WITNESS_BUDGET,\n type Witness,\n} from './glob.js';\nexport { integrityOf } from './integrity.js';\nexport { failing, lint, type LintOptions, type Plugin, ruleIds, sortFindings, summarise, type WaiveContext } from './lint.js';\nexport { asPlugin, asWaivers, loadPlugins } from './plugins.js';\nexport { type Format, FORMATS, githubCommands, gitlabCodeQuality, JSON_SCHEMA_VERSION, matrixJson, sarif, scheduleJson, sectionsJson } from './report.js';\nexport { ARCHIVE_RULES, COLLISION_RULES, type Rule, type RuleContext, type RuleInfo, type RuleResult, RULES } from './rules.js';\nexport { nextId, renderNewBrief } from './scaffold.js';\nexport {\n type Breach,\n breachReasons,\n holds,\n moves,\n type Passed,\n type Placement,\n planWaves,\n reasons,\n type Schedule,\n schedule,\n scheduleFindings,\n type ScheduleOptions,\n type Unplaced,\n type WavePlan,\n} from './schedule.js';\nexport { contradictions, meet, type Meeting, type Scope, scopeOf, type ScopePattern } from './scope.js';\nexport type { Finding, Phase, Severity, SeveritySetting, Status } from './types.js';\n\n/** Types a plugin author writes against, with inference for the rule list. */\nexport function definePlugin<T extends import('./lint.js').Plugin>(plugin: T): T {\n return plugin;\n}\n"]}
package/dist/lint.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Running rules: severities from the configuration, plugins beside the\n * built-in rules, findings in a stable order.\n */\n\nimport type { Waiver } from './archive.js';\nimport type { Brief } from './brief.js';\nimport { ConfigError } from './config.js';\nimport type { Corpus } from './corpus.js';\nimport { analyse, ARCHIVE_RULES, COLLISION_RULES, type Rule, type RuleInfo, RULES } from './rules.js';\nimport type { Finding, Severity, SeveritySetting } from './types.js';\n\n/** What a plugin's `waive` hook is asked about: one archival, refused. */\nexport interface WaiveContext {\n /** The absolute root: the directory holding the configuration. */\n readonly root: string;\n readonly brief: { readonly id: string | null; readonly file: string; readonly text: string };\n /**\n * Every finding that refuses the archival, as a frozen copy: writing to it\n * throws, and the waivers are matched against the plan's own. A\n * `protected-file` finding names its `path`; an `out-of-scope` one lists its\n * `paths`. Only those two can be waived.\n */\n readonly findings: readonly Finding[];\n /** The base the round is measured from, as it was given; `null` when none was. */\n readonly base: string | null;\n /** The commit the round landed as; `null` when none was read. */\n readonly commit: string | null;\n}\n\nexport interface Plugin {\n /** Prefixes the plugin's rule ids: `<name>/<rule>`. */\n readonly name: string;\n readonly rules: readonly Rule[];\n /** The plugin's entry in the configuration, handed to each rule. */\n readonly options?: unknown;\n /**\n * Lifts refusals of an archival that the plugin's own check allows - a\n * protected file a verified ruling covers - by rule and path. Asked only\n * when the plan has a refusal it could lift. Returning nothing waives\n * nothing.\n */\n readonly waive?: ((context: WaiveContext) => readonly Waiver[] | void | Promise<readonly Waiver[] | void>) | undefined;\n}\n\nexport interface LintOptions {\n readonly plugins?: readonly Plugin[];\n /** Tracked files, for the rules that read the tree. */\n readonly repoFiles?: readonly string[] | null;\n /** Limit the findings to these briefs. The whole corpus is still read. */\n readonly only?: readonly Brief[];\n}\n\nconst RANK: Readonly<Record<Severity, number>> = { note: 0, warning: 1, error: 2 };\n\nfunction lower(a: Severity, b: Severity): Severity {\n // Equal ranks are one severity, so either is the answer.\n return RANK[a] <= RANK[b] ? a : b;\n}\n\ninterface ActiveRule {\n readonly id: string;\n readonly rule: Rule;\n readonly severity: SeveritySetting;\n readonly options: unknown;\n}\n\n/** Every rule id a run knows: built-in, collision, archive and plugin. */\nexport function ruleIds(plugins: readonly Plugin[] = []): string[] {\n return [\n ...RULES.map((r) => r.id),\n ...COLLISION_RULES.map((r) => r.id),\n ...ARCHIVE_RULES.map((r) => r.id),\n ...plugins.flatMap((p) => p.rules.map((r) => `${p.name}/${r.id}`)),\n ];\n}\n\n/** The severity configuration assigns a rule, or its own. */\nexport function severityOf(corpus: Corpus, id: string, fallback: SeveritySetting): SeveritySetting {\n return corpus.config.rules[id] ?? fallback;\n}\n\n/** The severity of a built-in rule as configured, or `null` when it is off. */\nexport function configuredSeverity(corpus: Corpus, id: string): Severity | null {\n const rule = [...RULES, ...COLLISION_RULES, ...ARCHIVE_RULES].find((r) => r.id === id) as RuleInfo;\n const setting = severityOf(corpus, id, rule.severity);\n return setting === 'off' ? null : setting;\n}\n\n/** Refuses a configuration that names a rule nobody defines: a typo there silences nothing. */\nexport function checkRuleIds(corpus: Corpus, plugins: readonly Plugin[] = []): void {\n const known = new Set(ruleIds(plugins));\n const unknown = Object.keys(corpus.config.rules).filter((id) => !known.has(id));\n if (unknown.length > 0) {\n throw new ConfigError('configuration', unknown.map((id) => `\"rules.${id}\" names no rule`));\n }\n}\n\nexport async function lint(corpus: Corpus, options: LintOptions = {}): Promise<Finding[]> {\n const plugins = options.plugins ?? [];\n checkRuleIds(corpus, plugins);\n const active: ActiveRule[] = [\n ...RULES.map((rule) => ({ id: rule.id, rule, severity: severityOf(corpus, rule.id, rule.severity), options: undefined })),\n ...plugins.flatMap((plugin) =>\n plugin.rules.map((rule) => {\n const id = `${plugin.name}/${rule.id}`;\n return { id, rule, severity: severityOf(corpus, id, rule.severity), options: plugin.options };\n }),\n ),\n ].filter((r) => r.severity !== 'off');\n\n const shared = analyse(corpus);\n const targets = options.only ?? corpus.briefs;\n const findings: Finding[] = [];\n for (const brief of targets) {\n for (const { id, rule, severity, options: ruleOptions } of active) {\n const results = await rule.check({\n brief,\n corpus,\n config: corpus.config,\n repoFiles: options.repoFiles ?? null,\n shared,\n options: ruleOptions,\n });\n for (const result of results) {\n const configured = severity as Severity;\n findings.push({\n rule: id,\n severity: lower(result.severity ?? configured, configured),\n message: result.message,\n file: brief.file,\n line: Math.max(1, Math.trunc(result.line)),\n brief: brief.id ?? undefined,\n hint: result.hint,\n subject: result.subject,\n });\n }\n }\n }\n return sortFindings(findings);\n}\n\n/** Strings by code unit, which is the same order on every host and in every locale. */\nfunction order(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nexport function sortFindings(findings: readonly Finding[]): Finding[] {\n return [...findings].sort(\n (a, b) => order(a.file, b.file) || a.line - b.line || order(a.rule, b.rule) || order(a.message, b.message),\n );\n}\n\nexport interface Summary {\n readonly errors: number;\n readonly warnings: number;\n readonly notes: number;\n}\n\nexport function summarise(findings: readonly Finding[]): Summary {\n return {\n errors: findings.filter((f) => f.severity === 'error').length,\n warnings: findings.filter((f) => f.severity === 'warning').length,\n notes: findings.filter((f) => f.severity === 'note').length,\n };\n}\n\n/** Whether findings fail a run: any error, or any warning under `--strict`. */\nexport function failing(findings: readonly Finding[], strict: boolean): boolean {\n return findings.some((f) => f.severity === 'error' || (strict && f.severity === 'warning'));\n}\n"]}
1
+ {"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Running rules: severities from the configuration, plugins beside the\n * built-in rules, findings in a stable order.\n */\n\nimport type { Waiver } from './archive.js';\nimport type { Brief } from './brief.js';\nimport { ConfigError } from './config.js';\nimport type { Corpus } from './corpus.js';\nimport { analyse, ARCHIVE_RULES, COLLISION_RULES, type Rule, RULES } from './rules.js';\nimport type { Finding, Severity, SeveritySetting } from './types.js';\n\n/** What a plugin's `waive` hook is asked about: one archival, refused. */\nexport interface WaiveContext {\n /** The absolute root: the directory holding the configuration. */\n readonly root: string;\n readonly brief: { readonly id: string | null; readonly file: string; readonly text: string };\n /**\n * Every finding that refuses the archival, as a frozen copy: writing to it\n * throws, and the waivers are matched against the plan's own. A\n * `protected-file` finding names its `path`; an `out-of-scope` one lists its\n * `paths`. Only those two can be waived.\n */\n readonly findings: readonly Finding[];\n /** The base the round is measured from, as it was given; `null` when none was. */\n readonly base: string | null;\n /** The commit the round landed as; `null` when none was read. */\n readonly commit: string | null;\n}\n\nexport interface Plugin {\n /** Prefixes the plugin's rule ids: `<name>/<rule>`. */\n readonly name: string;\n readonly rules: readonly Rule[];\n /** The plugin's entry in the configuration, handed to each rule. */\n readonly options?: unknown;\n /**\n * Lifts refusals of an archival that the plugin's own check allows - a\n * protected file a verified ruling covers - by rule and path. Asked only\n * when the plan has a refusal it could lift. Returning nothing waives\n * nothing.\n */\n readonly waive?: ((context: WaiveContext) => readonly Waiver[] | void | Promise<readonly Waiver[] | void>) | undefined;\n}\n\nexport interface LintOptions {\n readonly plugins?: readonly Plugin[];\n /** Tracked files, for the rules that read the tree. */\n readonly repoFiles?: readonly string[] | null;\n /** Limit the findings to these briefs. The whole corpus is still read. */\n readonly only?: readonly Brief[];\n}\n\nconst RANK: Readonly<Record<Severity, number>> = { note: 0, warning: 1, error: 2 };\n\nfunction lower(a: Severity, b: Severity): Severity {\n // Equal ranks are one severity, so either is the answer.\n return RANK[a] <= RANK[b] ? a : b;\n}\n\ninterface ActiveRule {\n readonly id: string;\n readonly rule: Rule;\n readonly severity: SeveritySetting;\n readonly options: unknown;\n}\n\n/** Every rule id a run knows: built-in, collision, archive and plugin. */\nexport function ruleIds(plugins: readonly Plugin[] = []): string[] {\n return [\n ...RULES.map((r) => r.id),\n ...COLLISION_RULES.map((r) => r.id),\n ...ARCHIVE_RULES.map((r) => r.id),\n ...plugins.flatMap((p) => p.rules.map((r) => `${p.name}/${r.id}`)),\n ];\n}\n\n/** The severity configuration assigns a rule, or its own. */\nexport function severityOf(corpus: Corpus, id: string, fallback: SeveritySetting): SeveritySetting {\n return corpus.config.rules[id] ?? fallback;\n}\n\n/** The severity of a built-in rule as configured, or `null` when it is off. */\nexport function configuredSeverity(corpus: Corpus, id: string): Severity | null {\n const rule = [...RULES, ...COLLISION_RULES, ...ARCHIVE_RULES].find((r) => r.id === id)!;\n const setting = severityOf(corpus, id, rule.severity);\n return setting === 'off' ? null : setting;\n}\n\n/** Refuses a configuration that names a rule nobody defines: a typo there silences nothing. */\nexport function checkRuleIds(corpus: Corpus, plugins: readonly Plugin[] = []): void {\n const known = new Set(ruleIds(plugins));\n const unknown = Object.keys(corpus.config.rules).filter((id) => !known.has(id));\n if (unknown.length > 0) {\n throw new ConfigError('configuration', unknown.map((id) => `\"rules.${id}\" names no rule`));\n }\n}\n\nexport async function lint(corpus: Corpus, options: LintOptions = {}): Promise<Finding[]> {\n const plugins = options.plugins ?? [];\n checkRuleIds(corpus, plugins);\n const active: ActiveRule[] = [\n ...RULES.map((rule) => ({ id: rule.id, rule, severity: severityOf(corpus, rule.id, rule.severity), options: undefined })),\n ...plugins.flatMap((plugin) =>\n plugin.rules.map((rule) => {\n const id = `${plugin.name}/${rule.id}`;\n return { id, rule, severity: severityOf(corpus, id, rule.severity), options: plugin.options };\n }),\n ),\n ].filter((r) => r.severity !== 'off');\n\n const shared = analyse(corpus);\n const targets = options.only ?? corpus.briefs;\n const findings: Finding[] = [];\n for (const brief of targets) {\n for (const { id, rule, severity, options: ruleOptions } of active) {\n const results = await rule.check({\n brief,\n corpus,\n config: corpus.config,\n repoFiles: options.repoFiles ?? null,\n shared,\n options: ruleOptions,\n });\n for (const result of results) {\n const configured = severity as Severity;\n findings.push({\n rule: id,\n severity: lower(result.severity ?? configured, configured),\n message: result.message,\n file: brief.file,\n line: Math.max(1, Math.trunc(result.line)),\n brief: brief.id ?? undefined,\n hint: result.hint,\n subject: result.subject,\n });\n }\n }\n }\n return sortFindings(findings);\n}\n\n/** Strings by code unit, which is the same order on every host and in every locale. */\nfunction order(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nexport function sortFindings(findings: readonly Finding[]): Finding[] {\n return [...findings].sort(\n (a, b) => order(a.file, b.file) || a.line - b.line || order(a.rule, b.rule) || order(a.message, b.message),\n );\n}\n\nexport interface Summary {\n readonly errors: number;\n readonly warnings: number;\n readonly notes: number;\n}\n\nexport function summarise(findings: readonly Finding[]): Summary {\n return {\n errors: findings.filter((f) => f.severity === 'error').length,\n warnings: findings.filter((f) => f.severity === 'warning').length,\n notes: findings.filter((f) => f.severity === 'note').length,\n };\n}\n\n/** Whether findings fail a run: any error, or any warning under `--strict`. */\nexport function failing(findings: readonly Finding[], strict: boolean): boolean {\n return findings.some((f) => f.severity === 'error' || (strict && f.severity === 'warning'));\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A table's literal types, without `as const`.
3
+ *
4
+ * Stryker makes no mutant of anything that stands under a TypeScript `as`
5
+ * expression, `as const` among them: a table written `{ ... } as const` is
6
+ * read by no mutator, and no test is held to what it says. A `const` type
7
+ * parameter infers the same readonly literal types from its argument, and an
8
+ * argument is mutated as any other expression is (ADR-0009, amended
9
+ * 2026-10-12).
10
+ */
11
+ export declare function literal<const T>(table: T): T;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A table's literal types, without `as const`.
3
+ *
4
+ * Stryker makes no mutant of anything that stands under a TypeScript `as`
5
+ * expression, `as const` among them: a table written `{ ... } as const` is
6
+ * read by no mutator, and no test is held to what it says. A `const` type
7
+ * parameter infers the same readonly literal types from its argument, and an
8
+ * argument is mutated as any other expression is (ADR-0009, amended
9
+ * 2026-10-12).
10
+ */
11
+ export function literal(table) {
12
+ return table;
13
+ }
14
+ //# sourceMappingURL=literal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"literal.js","sourceRoot":"","sources":["../src/literal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,OAAO,CAAU,KAAQ;IACvC,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * A table's literal types, without `as const`.\n *\n * Stryker makes no mutant of anything that stands under a TypeScript `as`\n * expression, `as const` among them: a table written `{ ... } as const` is\n * read by no mutator, and no test is held to what it says. A `const` type\n * parameter infers the same readonly literal types from its argument, and an\n * argument is mutated as any other expression is (ADR-0009, amended\n * 2026-10-12).\n */\nexport function literal<const T>(table: T): T {\n return table;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A brief's Markdown: spec-core's scanner, copied into `src/vendor/spec-core/`,\n * read in spec-brief's dialect.\n *\n * The scanner is exact about what is code and what is a comment, as CommonMark\n * has it, and reports every heading, list item and link it finds. What of that\n * is a brief's structure is decided here (ADR-0001, amended 2026-09-26):\n *\n * - A heading is an ATX heading outside a block quote. Prose over a `---` is a\n * setext heading to a renderer, and a brief that separates its parts that way\n * must not grow a section from it; a heading in a block quote is quoted.\n * - A task is a list item outside a block quote with a box GFM renders: a\n * space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,\n * which a brief does not write.\n * - A link destination is one written in the link or in a definition: the\n * inline form and the definition, images included, an image inside a link's\n * text, and a link or an image in an image's alt text or a wiki link's text\n * too, each once, but no definition there, where CommonMark reads none. A\n * link inside a link's text is the link, as CommonMark reads it, and the\n * brackets around it are text. A reference is rewritten through its\n * definition, once.\n *\n * Everything here is in 0-based lines of the brief's `lines`, and columns\n * within them.\n */\n\nimport { type Link, linesOf, type MarkdownScan, scanMarkdown, type ScannedLine } from './vendor/spec-core/markdown/index.js';\nimport { textOfLines } from './text.js';\n\nexport interface Heading {\n /** 0-based line. */\n readonly line: number;\n readonly level: number;\n /** As a reader sees it: without a closing sequence or a comment. */\n readonly text: string;\n}\n\nexport interface Section {\n readonly heading: Heading;\n /** 0-based line after the section's last line. The body starts on the line after the heading. */\n readonly end: number;\n}\n\nexport interface TaskItem {\n /** 0-based line of the box. */\n readonly line: number;\n /** 0-based line after the item's last line: its continuations and what is nested under it. */\n readonly end: number;\n readonly checked: boolean;\n readonly text: string;\n}\n\nexport interface LinkDestination {\n /** 0-based line. */\n readonly line: number;\n /** Columns of the destination within the line, end exclusive. */\n readonly start: number;\n readonly end: number;\n readonly target: string;\n}\n\nexport interface Scan {\n readonly lines: readonly string[];\n /** Lines with front matter and comments blanked, and code kept: what counts as content. */\n readonly prose: readonly string[];\n /** Lines with front matter, code, raw-text HTML and comments blanked: what counts as structure. */\n readonly masked: readonly string[];\n readonly headings: readonly Heading[];\n readonly tasks: readonly TaskItem[];\n /** Destinations outside code and comments, each once, in the order of their lines and columns. */\n readonly links: readonly LinkDestination[];\n}\n\n/** Whether a checkbox is one a task is written with. */\nfunction isBox(checkbox: string | null): boolean {\n return checkbox === ' ' || checkbox === 'x' || checkbox === 'X';\n}\n\n/**\n * The links that write their destination - inline links and images, and\n * definitions - with offsets into the text scanned `at` that offset, added to\n * `found`.\n *\n * spec-core reads a link's text as CommonMark does: a link inside it is the\n * link, so `[a [b](b.md)](c.md)` gives `b.md` alone, and an image inside it,\n * `[![diagram](d.png)](d.md)`, is listed after the link. It reads nothing in\n * an image's alt text or a wiki link's text, where a destination still moves\n * with the brief, so those two are read again here, as `inText`. Each is\n * shorter than the text it came from, so the reading ends. Nor does spec-core\n * list a definition inside one, since a bracket pairs across no definition's\n * line, so each destination is found once, and no two rewrites meet on the\n * same columns.\n *\n * Read on its own, such a text is a document, where `[r]: r.md` at its start\n * is a definition. CommonMark reads none inside a paragraph's text, and a\n * renderer shows `![[r]: r.md](z.png)` with that alt text as written, so a\n * definition found there writes no destination.\n */\nfunction written(core: MarkdownScan, at: number, inText: boolean, found: Link[]): Link[] {\n for (const link of core.links) {\n if (link.image || link.form === 'wiki') written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), true, found);\n if (link.form === 'inline' || (link.form === 'definition' && !inText)) {\n found.push({ ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });\n }\n }\n return found;\n}\n\n/** Scans a brief's lines, which carry no byte-order mark. */\nexport function scan(lines: readonly string[]): Scan {\n const core = scanMarkdown(textOfLines(lines));\n const unquoted = (line: number): boolean => (core.lines[line - 1] as ScannedLine).quoteDepth === 0;\n return {\n lines,\n prose: linesOf(core, 'prose'),\n masked: linesOf(core, 'structure'),\n headings: core.headings\n .filter((h) => h.form === 'atx' && unquoted(h.line))\n .map((h) => ({ line: h.line - 1, level: h.level, text: h.text })),\n tasks: core.listItems\n .filter((item) => isBox(item.checkbox) && item.quoteDepth === 0)\n .map((item) => ({ line: item.line - 1, end: item.endLine, checked: item.checkbox !== ' ', text: item.firstLine })),\n links: written(core, 0, false, [])\n .map((link) => {\n // A link's text may run over lines; its destination is on the last.\n const at = core.index.positionAt(link.targetStart);\n const start = at.column - 1;\n return { line: at.line - 1, start, end: start + link.targetEnd - link.targetStart, target: link.target };\n })\n .sort((a, b) => a.line - b.line || a.start - b.start),\n };\n}\n\n/** Sections: every heading below the title, running to the next heading at its level or above. */\nexport function sectionsOf(result: Scan): Section[] {\n const sections: Section[] = [];\n const { headings } = result;\n for (let h = 0; h < headings.length; h += 1) {\n const heading = headings[h] as Heading;\n if (heading.level < 2) continue;\n let end = result.lines.length;\n for (let k = h + 1; k < headings.length; k += 1) {\n const next = headings[k] as Heading;\n if (next.level <= heading.level) {\n end = next.line;\n break;\n }\n }\n sections.push({ heading, end });\n }\n return sections;\n}\n\n/** The first level-one heading, which is what a reader takes as the title. */\nexport function titleOf(result: Scan): Heading | undefined {\n return result.headings.find((h) => h.level === 1);\n}\n\n/** Whether a line carries anything once comments are removed. */\nexport function hasContent(proseLine: string): boolean {\n return proseLine.trim().length > 0;\n}\n"]}
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A brief's Markdown: spec-core's scanner, copied into `src/vendor/spec-core/`,\n * read in spec-brief's dialect.\n *\n * The scanner is exact about what is code and what is a comment, as CommonMark\n * has it, and reports every heading, list item and link it finds. What of that\n * is a brief's structure is decided here (ADR-0001, amended 2026-09-26):\n *\n * - A heading is an ATX heading outside a block quote. Prose over a `---` is a\n * setext heading to a renderer, and a brief that separates its parts that way\n * must not grow a section from it; a heading in a block quote is quoted.\n * - A task is a list item outside a block quote with a box GFM renders: a\n * space, `x` or `X`. The scanner reads `[~]`, `[?]` and others as well,\n * which a brief does not write.\n * - A link destination is one written in the link or in a definition: the\n * inline form and the definition, images included, an image inside a link's\n * text, and a link or an image in an image's alt text or a wiki link's text\n * too, each once, but no definition there, where CommonMark reads none. A\n * link inside a link's text is the link, as CommonMark reads it, and the\n * brackets around it are text. A reference is rewritten through its\n * definition, once.\n *\n * Everything here is in 0-based lines of the brief's `lines`, and columns\n * within them.\n */\n\nimport { type Link, linesOf, type MarkdownScan, scanMarkdown } from './vendor/spec-core/markdown/index.js';\nimport { textOfLines } from './text.js';\n\nexport interface Heading {\n /** 0-based line. */\n readonly line: number;\n readonly level: number;\n /** As a reader sees it: without a closing sequence or a comment. */\n readonly text: string;\n}\n\nexport interface Section {\n readonly heading: Heading;\n /** 0-based line after the section's last line. The body starts on the line after the heading. */\n readonly end: number;\n}\n\nexport interface TaskItem {\n /** 0-based line of the box. */\n readonly line: number;\n /** 0-based line after the item's last line: its continuations and what is nested under it. */\n readonly end: number;\n readonly checked: boolean;\n readonly text: string;\n}\n\nexport interface LinkDestination {\n /** 0-based line. */\n readonly line: number;\n /** Columns of the destination within the line, end exclusive. */\n readonly start: number;\n readonly end: number;\n readonly target: string;\n}\n\nexport interface Scan {\n readonly lines: readonly string[];\n /** Lines with front matter and comments blanked, and code kept: what counts as content. */\n readonly prose: readonly string[];\n /** Lines with front matter, code, raw-text HTML and comments blanked: what counts as structure. */\n readonly masked: readonly string[];\n readonly headings: readonly Heading[];\n readonly tasks: readonly TaskItem[];\n /** Destinations outside code and comments, each once, in the order of their lines and columns. */\n readonly links: readonly LinkDestination[];\n}\n\n/** Whether a checkbox is one a task is written with. */\nfunction isBox(checkbox: string | null): boolean {\n return checkbox === ' ' || checkbox === 'x' || checkbox === 'X';\n}\n\n/**\n * The links that write their destination - inline links and images, and\n * definitions - with offsets into the text scanned `at` that offset, added to\n * `found`.\n *\n * spec-core reads a link's text as CommonMark does: a link inside it is the\n * link, so `[a [b](b.md)](c.md)` gives `b.md` alone, and an image inside it,\n * `[![diagram](d.png)](d.md)`, is listed after the link. It reads nothing in\n * an image's alt text or a wiki link's text, where a destination still moves\n * with the brief, so those two are read again here, as `inText`. Each is\n * shorter than the text it came from, so the reading ends. Nor does spec-core\n * list a definition inside one, since a bracket pairs across no definition's\n * line, so each destination is found once, and no two rewrites meet on the\n * same columns.\n *\n * Read on its own, such a text is a document, where `[r]: r.md` at its start\n * is a definition. CommonMark reads none inside a paragraph's text, and a\n * renderer shows `![[r]: r.md](z.png)` with that alt text as written, so a\n * definition found there writes no destination.\n */\nfunction written(core: MarkdownScan, at: number, inText: boolean, found: Link[]): Link[] {\n for (const link of core.links) {\n if (link.image || link.form === 'wiki') written(scanMarkdown(link.text), at + core.text.indexOf(link.text, link.start), true, found);\n if (link.form === 'inline' || (link.form === 'definition' && !inText)) {\n found.push({ ...link, targetStart: at + link.targetStart, targetEnd: at + link.targetEnd });\n }\n }\n return found;\n}\n\n/** Scans a brief's lines, which carry no byte-order mark. */\nexport function scan(lines: readonly string[]): Scan {\n const core = scanMarkdown(textOfLines(lines));\n const unquoted = (line: number): boolean => core.lines[line - 1]!.quoteDepth === 0;\n return {\n lines,\n prose: linesOf(core, 'prose'),\n masked: linesOf(core, 'structure'),\n headings: core.headings\n .filter((h) => h.form === 'atx' && unquoted(h.line))\n .map((h) => ({ line: h.line - 1, level: h.level, text: h.text })),\n tasks: core.listItems\n .filter((item) => isBox(item.checkbox) && item.quoteDepth === 0)\n .map((item) => ({ line: item.line - 1, end: item.endLine, checked: item.checkbox !== ' ', text: item.firstLine })),\n links: written(core, 0, false, [])\n .map((link) => {\n // A link's text may run over lines; its destination is on the last.\n const at = core.index.positionAt(link.targetStart);\n const start = at.column - 1;\n return { line: at.line - 1, start, end: start + link.targetEnd - link.targetStart, target: link.target };\n })\n .sort((a, b) => a.line - b.line || a.start - b.start),\n };\n}\n\n/** Sections: every heading below the title, running to the next heading at its level or above. */\nexport function sectionsOf(result: Scan): Section[] {\n const sections: Section[] = [];\n const { headings } = result;\n for (let h = 0; h < headings.length; h += 1) {\n const heading = headings[h] as Heading;\n if (heading.level < 2) continue;\n let end = result.lines.length;\n for (let k = h + 1; k < headings.length; k += 1) {\n const next = headings[k] as Heading;\n if (next.level <= heading.level) {\n end = next.line;\n break;\n }\n }\n sections.push({ heading, end });\n }\n return sections;\n}\n\n/** The first level-one heading, which is what a reader takes as the title. */\nexport function titleOf(result: Scan): Heading | undefined {\n return result.headings.find((h) => h.level === 1);\n}\n\n/** Whether a line carries anything once comments are removed. */\nexport function hasContent(proseLine: string): boolean {\n return proseLine.trim().length > 0;\n}\n"]}
package/dist/plugins.js CHANGED
@@ -14,6 +14,7 @@ import { createRequire } from 'node:module';
14
14
  import { dirname, isAbsolute, join, resolve } from 'node:path';
15
15
  import { pathToFileURL } from 'node:url';
16
16
  import { ConfigError } from './config.js';
17
+ /** Asked about whatever a module exported as a severity, so it is a set of anything. */
17
18
  const SEVERITIES = new Set(['off', 'note', 'warning', 'error']);
18
19
  /** The conditions Node matches in "exports" when it imports a module. */
19
20
  const IMPORT_CONDITIONS = new Set(['node', 'import', 'module-sync', 'default']);
@@ -89,6 +90,7 @@ export function exportsTarget(exports, subpath) {
89
90
  /** A package.json, `undefined` when there is none to read. */
90
91
  async function manifest(path) {
91
92
  try {
93
+ // The encoding decides nothing here either, for the reason `version` in cli.ts has.
92
94
  return JSON.parse(await readFile(path, 'utf8'));
93
95
  }
94
96
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Loading rule plugins named by the configuration.\n *\n * spec-brief does not know how another tool signs a departure, records a\n * reproducer or measures a blast radius, and should not: the tool that\n * defines a format is the one that can check it. A plugin is a module whose\n * default export is a plugin object, or a function of the configured options\n * that returns one. Loading one runs its code, exactly as loading an ESLint\n * configuration does; the configuration belongs to the repository, and so\n * does that trust.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport type { Waiver } from './archive.js';\nimport type { PluginReference } from './config.js';\nimport { ConfigError } from './config.js';\nimport type { Plugin } from './lint.js';\nimport type { Rule } from './rules.js';\n\nconst SEVERITIES = new Set(['off', 'note', 'warning', 'error']);\n\n/** The conditions Node matches in \"exports\" when it imports a module. */\nconst IMPORT_CONDITIONS = new Set(['node', 'import', 'module-sync', 'default']);\n\n/**\n * A target of \"exports\": a path inside the package, `null` where the package\n * refuses the subpath, `undefined` where no condition matched and a fallback\n * may still. A target that is not a \"./\" path, or that climbs out of the\n * package or into another, is refused, as Node refuses it: its segments are\n * split on a backslash too, since `join` reads one as a separator on Windows,\n * where \"pkg/..\\..\\outside\" would otherwise climb out of node_modules.\n */\nfunction exportTarget(value: unknown, match: string | null): string | null | undefined {\n if (typeof value === 'string') {\n if (!value.startsWith('./')) return null;\n const path = match === null ? value : value.replaceAll('*', match);\n const parts = path.slice(2).split(/[\\\\/]/);\n return parts.some((part) => part === '..' || part === '.' || part.toLowerCase() === 'node_modules') ? null : path;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n const path = exportTarget(item, match);\n if (typeof path === 'string') return path;\n }\n return null;\n }\n if (typeof value === 'object' && value !== null) {\n for (const [condition, item] of Object.entries(value)) {\n if (!IMPORT_CONDITIONS.has(condition)) continue;\n const path = exportTarget(item, match);\n if (path !== undefined) return path;\n }\n return undefined;\n }\n return null;\n}\n\n/**\n * The file a package's \"exports\" gives a subpath (`.` or `./x`) under the\n * conditions of an import, or `null` when it gives none: Node's\n * PACKAGE_EXPORTS_RESOLVE. An exact key wins; then the \"*\" pattern with the\n * longest prefix, and of those the longest key.\n */\nexport function exportsTarget(exports: unknown, subpath: string): string | null {\n // Only a map of subpaths has a key that starts with a dot: a path's and a list's keys are their indices.\n const keys = Object.keys(Object(exports) as object);\n const dotted = keys.filter((key) => key.startsWith('.'));\n if (dotted.length === 0) return subpath === '.' ? (exportTarget(exports, null) ?? null) : null;\n // Subpaths beside conditions is a package Node refuses to read.\n if (dotted.length !== keys.length) return null;\n const map = exports as Record<string, unknown>;\n if (!subpath.includes('*') && Object.hasOwn(map, subpath)) return exportTarget(map[subpath], null) ?? null;\n let best: { readonly key: string; readonly prefix: number; readonly match: string } | undefined;\n for (const key of dotted) {\n const star = key.indexOf('*');\n if (star === -1 || key.includes('*', star + 1)) continue;\n const suffix = key.slice(star + 1);\n if (subpath.length < key.length || !subpath.startsWith(key.slice(0, star)) || !subpath.endsWith(suffix)) continue;\n // Two keys of one prefix and one length that both match are one key, so the lengths never tie.\n if (best === undefined || star > best.prefix || (star === best.prefix && key.length > best.key.length)) {\n best = { key, prefix: star, match: subpath.slice(star, subpath.length - suffix.length) };\n }\n }\n return best === undefined ? null : (exportTarget(map[best.key], best.match) ?? null);\n}\n\n/** A package.json, `undefined` when there is none to read. */\nasync function manifest(path: string): Promise<Record<string, unknown> | undefined> {\n try {\n return JSON.parse(await readFile(path, 'utf8')) as Record<string, unknown>;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT' || (error as NodeJS.ErrnoException).code === 'ENOTDIR') return undefined;\n throw error;\n }\n}\n\n/**\n * The file a package specifier names, found from the root the way an import\n * there would find it. `require.resolve` reads \"exports\" under the require\n * conditions, so a package that exports only for \"import\" - an ESM-only\n * plugin - could not be found; Node's `import.meta.resolve` resolves only\n * from the module calling it, not from the root. So the nearest\n * node_modules/<name> is found here and its \"exports\" read under the import\n * conditions; a package with no \"exports\" resolves as it always has.\n */\nasync function resolvePackage(specifier: string, root: string): Promise<string> {\n const slash = specifier.indexOf('/', specifier.startsWith('@') ? specifier.indexOf('/') + 1 : 0);\n const name = slash === -1 ? specifier : specifier.slice(0, slash);\n const subpath = slash === -1 ? '.' : `.${specifier.slice(slash)}`;\n let directory = root;\n // Bounded by the depth of the path: each pass moves one directory up, and\n // the search stops at the top, so no path reaches the bound and one pass\n // more or fewer of it is the same search.\n for (let depth = 0; depth < 256; depth += 1) {\n const packageDirectory = join(directory, 'node_modules', name);\n const found = await manifest(join(packageDirectory, 'package.json'));\n if (found !== undefined) {\n if (found['exports'] === undefined || found['exports'] === null) break;\n const target = exportsTarget(found['exports'], subpath);\n if (target === null) throw new Error(`${name} exports no \"${subpath}\" for import`);\n return join(packageDirectory, target);\n }\n const parent = dirname(directory);\n // Stopping at the top saves asking it again until the bound: the answer is the same.\n if (parent === directory) break;\n directory = parent;\n }\n return createRequire(join(root, 'package.json')).resolve(specifier);\n}\n\nasync function specifierUrl(specifier: string, root: string): Promise<string> {\n if (specifier.startsWith('.') || isAbsolute(specifier)) return pathToFileURL(resolve(root, specifier)).href;\n return pathToFileURL(await resolvePackage(specifier, root)).href;\n}\n\nfunction isRule(value: unknown): value is Rule {\n if (typeof value !== 'object' || value === null) return false;\n const rule = value as Record<string, unknown>;\n return (\n typeof rule['id'] === 'string' &&\n /^[a-z0-9][a-z0-9-]*$/.test(rule['id']) &&\n typeof rule['description'] === 'string' &&\n SEVERITIES.has(rule['severity'] as string) &&\n typeof rule['check'] === 'function'\n );\n}\n\n/** Checks the shape a module exported, since nothing typed it on the way in. */\nexport function asPlugin(value: unknown, module: string, options: unknown): Plugin {\n const problems: string[] = [];\n if (typeof value !== 'object' || value === null) {\n throw new ConfigError(module, ['does not export a plugin: expected an object with \"name\" and \"rules\"']);\n }\n const candidate = value as Record<string, unknown>;\n const name = candidate['name'];\n const rules = candidate['rules'];\n const waive = candidate['waive'];\n if (typeof name !== 'string' || !/^[a-z0-9@][a-z0-9@/._-]*$/i.test(name)) problems.push('\"name\" must be a plain identifier');\n if (!Array.isArray(rules)) problems.push('\"rules\" must be a list');\n else {\n rules.forEach((rule, i) => {\n if (!isRule(rule)) problems.push(`rules[${i}] needs a lower-case \"id\", a \"description\", a \"severity\" and a \"check\" function`);\n });\n }\n if (waive !== undefined && typeof waive !== 'function') problems.push('\"waive\" must be a function');\n if (problems.length > 0) throw new ConfigError(module, problems);\n const plugin: Plugin = { name: name as string, rules: rules as Rule[], options };\n return waive === undefined ? plugin : { ...plugin, waive: waive as NonNullable<Plugin['waive']> };\n}\n\n/**\n * Checks what a `waive` hook returned: a list of `{ rule, path, reason }`,\n * each a string. A hook that returns nothing waives nothing: a function that\n * found no ruling and fell off its end has answered, not failed.\n */\nexport function asWaivers(value: unknown, plugin: string): Waiver[] {\n const isWaiver = (item: unknown): item is Waiver => {\n if (typeof item !== 'object' || item === null) return false;\n const w = item as Record<string, unknown>;\n return typeof w['rule'] === 'string' && typeof w['path'] === 'string' && typeof w['reason'] === 'string';\n };\n if (value === undefined) return [];\n if (!Array.isArray(value) || !value.every(isWaiver)) {\n throw new ConfigError(`plugin \"${plugin}\"`, ['\"waive\" must return a list of { rule, path, reason }, each a string']);\n }\n return value.map((w) => ({ rule: w.rule, path: w.path, reason: w.reason }));\n}\n\nexport async function loadPlugins(references: readonly PluginReference[], root: string): Promise<Plugin[]> {\n const plugins: Plugin[] = [];\n for (const reference of references) {\n let loaded: Record<string, unknown>;\n try {\n loaded = (await import(await specifierUrl(reference.module, root))) as Record<string, unknown>;\n } catch (error) {\n throw new ConfigError(reference.module, [`could not be loaded: ${(error as Error).message}`]);\n }\n const exported = loaded['default'] ?? loaded['plugin'];\n const value = typeof exported === 'function' ? await (exported as (o: unknown) => unknown)(reference.options) : exported;\n plugins.push(asPlugin(value, reference.module, reference.options));\n }\n const names = plugins.map((p) => p.name);\n const repeated = names.filter((n, i) => names.indexOf(n) !== i);\n if (repeated.length > 0) throw new ConfigError('plugins', [`two plugins are named \"${repeated[0] as string}\"`]);\n return plugins;\n}\n"]}
1
+ {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Loading rule plugins named by the configuration.\n *\n * spec-brief does not know how another tool signs a departure, records a\n * reproducer or measures a blast radius, and should not: the tool that\n * defines a format is the one that can check it. A plugin is a module whose\n * default export is a plugin object, or a function of the configured options\n * that returns one. Loading one runs its code, exactly as loading an ESLint\n * configuration does; the configuration belongs to the repository, and so\n * does that trust.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport type { Waiver } from './archive.js';\nimport type { PluginReference } from './config.js';\nimport { ConfigError } from './config.js';\nimport type { Plugin } from './lint.js';\nimport type { Rule } from './rules.js';\n\n/** Asked about whatever a module exported as a severity, so it is a set of anything. */\nconst SEVERITIES: ReadonlySet<unknown> = new Set(['off', 'note', 'warning', 'error']);\n\n/** The conditions Node matches in \"exports\" when it imports a module. */\nconst IMPORT_CONDITIONS = new Set(['node', 'import', 'module-sync', 'default']);\n\n/**\n * A target of \"exports\": a path inside the package, `null` where the package\n * refuses the subpath, `undefined` where no condition matched and a fallback\n * may still. A target that is not a \"./\" path, or that climbs out of the\n * package or into another, is refused, as Node refuses it: its segments are\n * split on a backslash too, since `join` reads one as a separator on Windows,\n * where \"pkg/..\\..\\outside\" would otherwise climb out of node_modules.\n */\nfunction exportTarget(value: unknown, match: string | null): string | null | undefined {\n if (typeof value === 'string') {\n if (!value.startsWith('./')) return null;\n const path = match === null ? value : value.replaceAll('*', match);\n const parts = path.slice(2).split(/[\\\\/]/);\n return parts.some((part) => part === '..' || part === '.' || part.toLowerCase() === 'node_modules') ? null : path;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n const path = exportTarget(item, match);\n if (typeof path === 'string') return path;\n }\n return null;\n }\n if (typeof value === 'object' && value !== null) {\n for (const [condition, item] of Object.entries(value)) {\n if (!IMPORT_CONDITIONS.has(condition)) continue;\n const path = exportTarget(item, match);\n if (path !== undefined) return path;\n }\n return undefined;\n }\n return null;\n}\n\n/**\n * The file a package's \"exports\" gives a subpath (`.` or `./x`) under the\n * conditions of an import, or `null` when it gives none: Node's\n * PACKAGE_EXPORTS_RESOLVE. An exact key wins; then the \"*\" pattern with the\n * longest prefix, and of those the longest key.\n */\nexport function exportsTarget(exports: unknown, subpath: string): string | null {\n // Only a map of subpaths has a key that starts with a dot: a path's and a list's keys are their indices.\n const keys = Object.keys(Object(exports) as object);\n const dotted = keys.filter((key) => key.startsWith('.'));\n if (dotted.length === 0) return subpath === '.' ? (exportTarget(exports, null) ?? null) : null;\n // Subpaths beside conditions is a package Node refuses to read.\n if (dotted.length !== keys.length) return null;\n const map = exports as Record<string, unknown>;\n if (!subpath.includes('*') && Object.hasOwn(map, subpath)) return exportTarget(map[subpath], null) ?? null;\n let best: { readonly key: string; readonly prefix: number; readonly match: string } | undefined;\n for (const key of dotted) {\n const star = key.indexOf('*');\n if (star === -1 || key.includes('*', star + 1)) continue;\n const suffix = key.slice(star + 1);\n if (subpath.length < key.length || !subpath.startsWith(key.slice(0, star)) || !subpath.endsWith(suffix)) continue;\n // Two keys of one prefix and one length that both match are one key, so the lengths never tie.\n if (best === undefined || star > best.prefix || (star === best.prefix && key.length > best.key.length)) {\n best = { key, prefix: star, match: subpath.slice(star, subpath.length - suffix.length) };\n }\n }\n return best === undefined ? null : (exportTarget(map[best.key], best.match) ?? null);\n}\n\n/** A package.json, `undefined` when there is none to read. */\nasync function manifest(path: string): Promise<Record<string, unknown> | undefined> {\n try {\n // The encoding decides nothing here either, for the reason `version` in cli.ts has.\n return JSON.parse(await readFile(path, 'utf8'));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT' || (error as NodeJS.ErrnoException).code === 'ENOTDIR') return undefined;\n throw error;\n }\n}\n\n/**\n * The file a package specifier names, found from the root the way an import\n * there would find it. `require.resolve` reads \"exports\" under the require\n * conditions, so a package that exports only for \"import\" - an ESM-only\n * plugin - could not be found; Node's `import.meta.resolve` resolves only\n * from the module calling it, not from the root. So the nearest\n * node_modules/<name> is found here and its \"exports\" read under the import\n * conditions; a package with no \"exports\" resolves as it always has.\n */\nasync function resolvePackage(specifier: string, root: string): Promise<string> {\n const slash = specifier.indexOf('/', specifier.startsWith('@') ? specifier.indexOf('/') + 1 : 0);\n const name = slash === -1 ? specifier : specifier.slice(0, slash);\n const subpath = slash === -1 ? '.' : `.${specifier.slice(slash)}`;\n let directory = root;\n // Bounded by the depth of the path: each pass moves one directory up, and\n // the search stops at the top, so no path reaches the bound and one pass\n // more or fewer of it is the same search.\n for (let depth = 0; depth < 256; depth += 1) {\n const packageDirectory = join(directory, 'node_modules', name);\n const found = await manifest(join(packageDirectory, 'package.json'));\n if (found !== undefined) {\n if (found['exports'] === undefined || found['exports'] === null) break;\n const target = exportsTarget(found['exports'], subpath);\n if (target === null) throw new Error(`${name} exports no \"${subpath}\" for import`);\n return join(packageDirectory, target);\n }\n const parent = dirname(directory);\n // Stopping at the top saves asking it again until the bound: the answer is the same.\n if (parent === directory) break;\n directory = parent;\n }\n return createRequire(join(root, 'package.json')).resolve(specifier);\n}\n\nasync function specifierUrl(specifier: string, root: string): Promise<string> {\n if (specifier.startsWith('.') || isAbsolute(specifier)) return pathToFileURL(resolve(root, specifier)).href;\n return pathToFileURL(await resolvePackage(specifier, root)).href;\n}\n\nfunction isRule(value: unknown): value is Rule {\n if (typeof value !== 'object' || value === null) return false;\n const rule = value as Record<string, unknown>;\n return (\n typeof rule['id'] === 'string' &&\n /^[a-z0-9][a-z0-9-]*$/.test(rule['id']) &&\n typeof rule['description'] === 'string' &&\n SEVERITIES.has(rule['severity']) &&\n typeof rule['check'] === 'function'\n );\n}\n\n/** Checks the shape a module exported, since nothing typed it on the way in. */\nexport function asPlugin(value: unknown, module: string, options: unknown): Plugin {\n const problems: string[] = [];\n if (typeof value !== 'object' || value === null) {\n throw new ConfigError(module, ['does not export a plugin: expected an object with \"name\" and \"rules\"']);\n }\n const candidate = value as Record<string, unknown>;\n const name = candidate['name'];\n const rules = candidate['rules'];\n const waive = candidate['waive'];\n if (typeof name !== 'string' || !/^[a-z0-9@][a-z0-9@/._-]*$/i.test(name)) problems.push('\"name\" must be a plain identifier');\n if (!Array.isArray(rules)) problems.push('\"rules\" must be a list');\n else {\n rules.forEach((rule, i) => {\n if (!isRule(rule)) problems.push(`rules[${i}] needs a lower-case \"id\", a \"description\", a \"severity\" and a \"check\" function`);\n });\n }\n if (waive !== undefined && typeof waive !== 'function') problems.push('\"waive\" must be a function');\n if (problems.length > 0) throw new ConfigError(module, problems);\n const plugin: Plugin = { name: name as string, rules: rules as Rule[], options };\n return waive === undefined ? plugin : { ...plugin, waive: waive as NonNullable<Plugin['waive']> };\n}\n\n/**\n * Checks what a `waive` hook returned: a list of `{ rule, path, reason }`,\n * each a string. A hook that returns nothing waives nothing: a function that\n * found no ruling and fell off its end has answered, not failed.\n */\nexport function asWaivers(value: unknown, plugin: string): Waiver[] {\n const isWaiver = (item: unknown): item is Waiver => {\n if (typeof item !== 'object' || item === null) return false;\n const w = item as Record<string, unknown>;\n return typeof w['rule'] === 'string' && typeof w['path'] === 'string' && typeof w['reason'] === 'string';\n };\n if (value === undefined) return [];\n if (!Array.isArray(value) || !value.every(isWaiver)) {\n throw new ConfigError(`plugin \"${plugin}\"`, ['\"waive\" must return a list of { rule, path, reason }, each a string']);\n }\n return value.map((w) => ({ rule: w.rule, path: w.path, reason: w.reason }));\n}\n\nexport async function loadPlugins(references: readonly PluginReference[], root: string): Promise<Plugin[]> {\n const plugins: Plugin[] = [];\n for (const reference of references) {\n let loaded: Record<string, unknown>;\n try {\n loaded = (await import(await specifierUrl(reference.module, root))) as Record<string, unknown>;\n } catch (error) {\n throw new ConfigError(reference.module, [`could not be loaded: ${(error as Error).message}`]);\n }\n const exported = loaded['default'] ?? loaded['plugin'];\n const value = typeof exported === 'function' ? await (exported as (o: unknown) => unknown)(reference.options) : exported;\n plugins.push(asPlugin(value, reference.module, reference.options));\n }\n const names = plugins.map((p) => p.name);\n const repeated = names.filter((n, i) => names.indexOf(n) !== i);\n if (repeated.length > 0) throw new ConfigError('plugins', [`two plugins are named \"${repeated[0] as string}\"`]);\n return plugins;\n}\n"]}
package/dist/report.js CHANGED
@@ -11,6 +11,7 @@
11
11
  import { createHash } from 'node:crypto';
12
12
  import { idKey } from './corpus.js';
13
13
  import { summarise } from './lint.js';
14
+ import { literal } from './literal.js';
14
15
  import { ARCHIVE_RULES, COLLISION_RULES, RULES } from './rules.js';
15
16
  import { breachReasons, holds, moves, reasons } from './schedule.js';
16
17
  import { witnessText } from './scope.js';
@@ -27,13 +28,13 @@ export const FORMATS = ['pretty', 'json', 'sarif', 'github', 'gitlab'];
27
28
  */
28
29
  export const JSON_SCHEMA_VERSION = 2;
29
30
  /** The SGR code that turns each colour on, and the one that turns it off again. */
30
- const CODES = {
31
+ const CODES = literal({
31
32
  red: [31, 39],
32
33
  yellow: [33, 39],
33
34
  cyan: [36, 39],
34
35
  dim: [2, 22],
35
36
  bold: [1, 22],
36
- };
37
+ });
37
38
  export function paint(style, color, text) {
38
39
  const [on, off] = CODES[color];
39
40
  return style.color ? `\u001b[${on}m${text}\u001b[${off}m` : text;