@descent-vtt/spec-brief 0.1.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 (78) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +269 -0
  4. package/bin/spec-brief.js +19 -0
  5. package/dist/apply.d.ts +26 -0
  6. package/dist/apply.js +71 -0
  7. package/dist/apply.js.map +1 -0
  8. package/dist/archive.d.ts +81 -0
  9. package/dist/archive.js +333 -0
  10. package/dist/archive.js.map +1 -0
  11. package/dist/brief.d.ts +60 -0
  12. package/dist/brief.js +152 -0
  13. package/dist/brief.js.map +1 -0
  14. package/dist/cli.d.ts +35 -0
  15. package/dist/cli.js +411 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/collisions.d.ts +50 -0
  18. package/dist/collisions.js +127 -0
  19. package/dist/collisions.js.map +1 -0
  20. package/dist/config.d.ts +94 -0
  21. package/dist/config.js +353 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/corpus.d.ts +41 -0
  24. package/dist/corpus.js +154 -0
  25. package/dist/corpus.js.map +1 -0
  26. package/dist/engine.d.ts +121 -0
  27. package/dist/engine.js +276 -0
  28. package/dist/engine.js.map +1 -0
  29. package/dist/frontmatter.d.ts +68 -0
  30. package/dist/frontmatter.js +311 -0
  31. package/dist/frontmatter.js.map +1 -0
  32. package/dist/fs.d.ts +59 -0
  33. package/dist/fs.js +189 -0
  34. package/dist/fs.js.map +1 -0
  35. package/dist/git.d.ts +59 -0
  36. package/dist/git.js +131 -0
  37. package/dist/git.js.map +1 -0
  38. package/dist/glob.d.ts +79 -0
  39. package/dist/glob.js +465 -0
  40. package/dist/glob.js.map +1 -0
  41. package/dist/index.d.ts +24 -0
  42. package/dist/index.js +26 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/integrity.d.ts +11 -0
  45. package/dist/integrity.js +20 -0
  46. package/dist/integrity.js.map +1 -0
  47. package/dist/links.d.ts +38 -0
  48. package/dist/links.js +142 -0
  49. package/dist/links.js.map +1 -0
  50. package/dist/lint.d.ts +38 -0
  51. package/dist/lint.js +90 -0
  52. package/dist/lint.js.map +1 -0
  53. package/dist/markdown.d.ts +65 -0
  54. package/dist/markdown.js +274 -0
  55. package/dist/markdown.js.map +1 -0
  56. package/dist/plugins.d.ts +16 -0
  57. package/dist/plugins.js +77 -0
  58. package/dist/plugins.js.map +1 -0
  59. package/dist/report.d.ts +38 -0
  60. package/dist/report.js +244 -0
  61. package/dist/report.js.map +1 -0
  62. package/dist/rules.d.ts +58 -0
  63. package/dist/rules.js +448 -0
  64. package/dist/rules.js.map +1 -0
  65. package/dist/scaffold.d.ts +25 -0
  66. package/dist/scaffold.js +81 -0
  67. package/dist/scaffold.js.map +1 -0
  68. package/dist/schema.d.ts +47 -0
  69. package/dist/schema.js +195 -0
  70. package/dist/schema.js.map +1 -0
  71. package/dist/text.d.ts +40 -0
  72. package/dist/text.js +95 -0
  73. package/dist/text.js.map +1 -0
  74. package/dist/types.d.ts +30 -0
  75. package/dist/types.js +5 -0
  76. package/dist/types.js.map +1 -0
  77. package/package.json +76 -0
  78. package/schema.json +321 -0
package/dist/git.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Git, read-only.
3
+ *
4
+ * spec-brief never stages, commits or rewrites history. It reads the commit a
5
+ * round landed as, the files that commit touched, and whether the working tree
6
+ * holds work the commit does not. What goes into a commit stays the decision
7
+ * of whoever makes it, and archival is atomic over the files it writes rather
8
+ * than over a repository state it would have to reverse.
9
+ */
10
+ export interface CommitInfo {
11
+ readonly sha: string;
12
+ readonly author: string;
13
+ /** ISO 8601 author date. */
14
+ readonly date: string;
15
+ }
16
+ export interface FileChange {
17
+ readonly path: string;
18
+ /** `null` for a binary file, which has no line counts. */
19
+ readonly insertions: number | null;
20
+ readonly deletions: number | null;
21
+ }
22
+ export interface Git {
23
+ /** The commit a revision names, or `null` when it names none. */
24
+ commit(revision: string): Promise<CommitInfo | null>;
25
+ mergeBase(a: string, b: string): Promise<string | null>;
26
+ /** Files changed from `from` to `to`; with no `from`, the files `to` changed against its first parent. */
27
+ changes(from: string | null, to: string): Promise<FileChange[]>;
28
+ /** Paths with uncommitted changes, untracked files included. */
29
+ dirty(): Promise<string[]>;
30
+ /** Every path git sees: tracked, or untracked and not ignored. A file a round just created counts. */
31
+ files(): Promise<string[]>;
32
+ remoteUrl(name: string): Promise<string | null>;
33
+ }
34
+ /** Parses `--numstat -z` output with renames off: `added<TAB>deleted<TAB>path<NUL>`. */
35
+ export declare function parseNumstat(output: string): FileChange[];
36
+ /** Parses `status --porcelain=v1 -z`, where a rename's original path is a record of its own. */
37
+ export declare function parsePorcelain(output: string): string[];
38
+ /**
39
+ * Git reports paths from the top of the work tree; spec-brief's paths start at
40
+ * the root, which may be a directory below it. Every path that crosses this
41
+ * boundary is made relative to the root.
42
+ */
43
+ export declare class NodeGit implements Git {
44
+ readonly cwd: string;
45
+ private prefix;
46
+ constructor(cwd: string);
47
+ /** The root's path from the top of the work tree, `sub/dir/` or empty. */
48
+ private rootPrefix;
49
+ /** The working tree's top directory, canonical, or `null` outside a repository. */
50
+ static toplevel(cwd: string): Promise<string | null>;
51
+ commit(revision: string): Promise<CommitInfo | null>;
52
+ mergeBase(a: string, b: string): Promise<string | null>;
53
+ changes(from: string | null, to: string): Promise<FileChange[]>;
54
+ dirty(): Promise<string[]>;
55
+ files(): Promise<string[]>;
56
+ remoteUrl(name: string): Promise<string | null>;
57
+ }
58
+ /** The web address of a pull request, for remotes on github.com. */
59
+ export declare function pullRequestUrl(remote: string | null, number: number): string | null;
package/dist/git.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Git, read-only.
3
+ *
4
+ * spec-brief never stages, commits or rewrites history. It reads the commit a
5
+ * round landed as, the files that commit touched, and whether the working tree
6
+ * holds work the commit does not. What goes into a commit stays the decision
7
+ * of whoever makes it, and archival is atomic over the files it writes rather
8
+ * than over a repository state it would have to reverse.
9
+ */
10
+ import { execFile } from 'node:child_process';
11
+ import { resolve } from 'node:path';
12
+ import { canonicalPath } from './fs.js';
13
+ import { relativePath } from './links.js';
14
+ function run(cwd, args) {
15
+ return new Promise((done) => {
16
+ execFile('git', ['-c', 'core.quotepath=off', ...args], { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => done({ ok: error === null, stdout, stderr }));
17
+ });
18
+ }
19
+ /** A revision that starts with "-" would be read as an option. */
20
+ function safeRevision(revision) {
21
+ if (revision.startsWith('-') || revision.trim() === '')
22
+ throw new Error(`"${revision}" is not a revision`);
23
+ return revision;
24
+ }
25
+ function records(output) {
26
+ return output.split('\0').filter((r) => r !== '');
27
+ }
28
+ /** Parses `--numstat -z` output with renames off: `added<TAB>deleted<TAB>path<NUL>`. */
29
+ export function parseNumstat(output) {
30
+ // `show --format=` leaves the empty header's newline in front of the first record.
31
+ return records(output.replace(/^\n+/, '')).map((record) => {
32
+ const [added = '-', deleted = '-', ...path] = record.split('\t');
33
+ return {
34
+ path: path.join('\t'),
35
+ insertions: added === '-' ? null : Number(added),
36
+ deletions: deleted === '-' ? null : Number(deleted),
37
+ };
38
+ });
39
+ }
40
+ /** Parses `status --porcelain=v1 -z`, where a rename's original path is a record of its own. */
41
+ export function parsePorcelain(output) {
42
+ const fields = records(output);
43
+ const paths = [];
44
+ for (let i = 0; i < fields.length; i += 1) {
45
+ const field = fields[i];
46
+ paths.push(field.slice(3));
47
+ const x = field.charAt(0);
48
+ if (x === 'R' || x === 'C')
49
+ i += 1;
50
+ }
51
+ return paths;
52
+ }
53
+ /**
54
+ * Git reports paths from the top of the work tree; spec-brief's paths start at
55
+ * the root, which may be a directory below it. Every path that crosses this
56
+ * boundary is made relative to the root.
57
+ */
58
+ export class NodeGit {
59
+ cwd;
60
+ prefix;
61
+ constructor(cwd) {
62
+ this.cwd = cwd;
63
+ }
64
+ /** The root's path from the top of the work tree, `sub/dir/` or empty. */
65
+ async rootPrefix() {
66
+ // Asked only after a status that succeeded, so the repository exists.
67
+ this.prefix ??= run(this.cwd, ['rev-parse', '--show-prefix']).then((r) => r.stdout.trim());
68
+ return this.prefix;
69
+ }
70
+ /** The working tree's top directory, canonical, or `null` outside a repository. */
71
+ static async toplevel(cwd) {
72
+ const result = await run(cwd, ['rev-parse', '--show-toplevel']);
73
+ return result.ok ? canonicalPath(resolve(result.stdout.trim())) : null;
74
+ }
75
+ async commit(revision) {
76
+ const sha = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${safeRevision(revision)}^{commit}`]);
77
+ if (!sha.ok)
78
+ return null;
79
+ const hash = sha.stdout.trim();
80
+ const show = await run(this.cwd, ['show', '-s', '--format=%an%x00%aI', hash]);
81
+ const [author = '', date = ''] = show.stdout.trim().split('\0');
82
+ return { sha: hash, author, date };
83
+ }
84
+ async mergeBase(a, b) {
85
+ const result = await run(this.cwd, ['merge-base', safeRevision(a), safeRevision(b)]);
86
+ return result.ok ? result.stdout.trim() : null;
87
+ }
88
+ async changes(from, to) {
89
+ const target = safeRevision(to);
90
+ let result;
91
+ if (from !== null) {
92
+ result = await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', safeRevision(from), target]);
93
+ }
94
+ else {
95
+ const parent = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${target}^`]);
96
+ result = parent.ok
97
+ ? await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', `${target}^`, target])
98
+ : await run(this.cwd, ['show', '--numstat', '-z', '--no-renames', '--relative', '--format=', target]);
99
+ }
100
+ if (!result.ok)
101
+ throw new Error(`git could not diff ${from ?? `${to}^`}..${to}: ${result.stderr.trim()}`);
102
+ return parseNumstat(result.stdout);
103
+ }
104
+ async dirty() {
105
+ const result = await run(this.cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
106
+ if (!result.ok)
107
+ throw new Error(`git status failed: ${result.stderr.trim()}`);
108
+ const prefix = await this.rootPrefix();
109
+ return parsePorcelain(result.stdout).map((path) => (prefix === '' ? path : relativePath(prefix, path)));
110
+ }
111
+ async files() {
112
+ const result = await run(this.cwd, ['ls-files', '-z', '--cached', '--others', '--exclude-standard']);
113
+ if (!result.ok)
114
+ throw new Error(`git ls-files failed: ${result.stderr.trim()}`);
115
+ return records(result.stdout).sort();
116
+ }
117
+ async remoteUrl(name) {
118
+ const result = await run(this.cwd, ['remote', 'get-url', safeRevision(name)]);
119
+ return result.ok ? result.stdout.trim() : null;
120
+ }
121
+ }
122
+ /** The web address of a pull request, for remotes on github.com. */
123
+ export function pullRequestUrl(remote, number) {
124
+ if (remote === null)
125
+ return null;
126
+ const match = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(remote);
127
+ if (!match)
128
+ return null;
129
+ return `https://github.com/${match[1]}/${match[2]}/pull/${number}`;
130
+ }
131
+ //# sourceMappingURL=git.js.map
@@ -0,0 +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\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 { 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 Error(`\"${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');\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');\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 Error(`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 Error(`git status failed: ${result.stderr.trim()}`);\n const prefix = await this.rootPrefix();\n return parsePorcelain(result.stdout).map((path) => (prefix === '' ? 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 Error(`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 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/glob.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Globs: parsing, matching, and deciding whether two globs can name the same file.
3
+ *
4
+ * The dialect is the one people type: `**`, `*`, `?`, `[abc]`, `[!a-z]`,
5
+ * `{a,b}` and `\` to escape. A pattern with no glob syntax at all names a
6
+ * directory and everything beneath it, so `src/auth` covers
7
+ * `src/auth/login.ts` - unless it names a file, one the tree holds or one with
8
+ * an extension, which matches only itself. A trailing `/` always means a
9
+ * directory. Paths
10
+ * are repository-relative and POSIX, and matching is case-sensitive on every
11
+ * host, because git's paths are and a result should not depend on who ran it.
12
+ * `*` matches a leading dot; a scope that forgot its dotfiles is not a scope
13
+ * that excludes them.
14
+ *
15
+ * Nothing here compiles to a `RegExp`. Matching and intersection are dynamic
16
+ * programmes over the pattern and the subject, so a pattern of `*a*a*a*a*b`
17
+ * against a long name costs the product of their lengths and cannot backtrack
18
+ * into an exponent. The same property is why the sibling tools match their own
19
+ * globs this way.
20
+ */
21
+ export type CharToken = {
22
+ readonly kind: 'literal';
23
+ readonly char: string;
24
+ } | {
25
+ readonly kind: 'any';
26
+ } | {
27
+ readonly kind: 'star';
28
+ } | {
29
+ readonly kind: 'class';
30
+ readonly negated: boolean;
31
+ readonly ranges: readonly (readonly [number, number])[];
32
+ };
33
+ export type Segment = {
34
+ readonly kind: 'globstar';
35
+ } | {
36
+ readonly kind: 'pattern';
37
+ readonly tokens: readonly CharToken[];
38
+ readonly literal: boolean;
39
+ };
40
+ export interface Glob {
41
+ readonly source: string;
42
+ /** One sequence of segments per brace alternative; a path matches when any does. */
43
+ readonly alternatives: readonly (readonly Segment[])[];
44
+ }
45
+ export type GlobParse = {
46
+ readonly ok: true;
47
+ readonly glob: Glob;
48
+ } | {
49
+ readonly ok: false;
50
+ readonly error: string;
51
+ };
52
+ /** More alternatives than this is a pattern nobody meant. */
53
+ export declare const MAX_ALTERNATIVES = 256;
54
+ export interface ParseOptions {
55
+ /** Whether a literal path is a file, from the tree when it is known. */
56
+ readonly isFile?: ((path: string) => boolean) | undefined;
57
+ }
58
+ export declare function parseGlob(source: string, options?: ParseOptions): GlobParse;
59
+ export declare function matchGlob(glob: Glob, path: string): boolean;
60
+ /**
61
+ * A path both globs match, or `null` when there is none. The answer is exact
62
+ * for the dialect above: a `null` means no file can be in both scopes, and a
63
+ * path is a witness a reader can check by eye.
64
+ */
65
+ export declare function intersectGlobs(a: Glob, b: Glob): string | null;
66
+ /**
67
+ * A non-empty string both token sequences match, or `null`.
68
+ *
69
+ * Complete by a shortest-witness argument: in a shortest common string, no
70
+ * character is absorbed by a star on both sides at once (dropping it would
71
+ * leave a shorter one), so every character advances at least one sequence,
72
+ * and the five moves below are all the ways that can happen.
73
+ */
74
+ export declare function intersectTokens(x: readonly CharToken[], y: readonly CharToken[]): string | null;
75
+ /**
76
+ * The directory a glob is rooted in: its leading literal segments. A pattern
77
+ * that names a file is rooted in that file's directory.
78
+ */
79
+ export declare function globBase(glob: Glob): string;