@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Writing a new brief: the next free id, a file name, and a body with every\n * section the configuration requires, each holding a hint in a comment.\n * Comments are not content, so a freshly scaffolded brief reads as unwritten\n * until somebody writes it.\n */\n\nimport type { Config, SectionRule } from './config.js';\nimport type { Corpus } from './corpus.js';\nimport { renderScalar } from './frontmatter.js';\nimport { fillTemplate, slugify } from './text.js';\n\nexport interface NewBrief {\n readonly id: string;\n readonly title: string;\n readonly date: string;\n readonly type?: string | undefined;\n readonly wave?: number | undefined;\n readonly dependsOn?: readonly string[] | undefined;\n}\n\n/**\n * The next id: one more than the highest numeric id, live or archived, at the\n * configured width. Ids are never reused, so an archived brief still holds\n * its number. `null` when the corpus has ids but none of them are numbers.\n */\nexport function nextId(corpus: Corpus): string | null {\n const ids = corpus.briefs.map((b) => b.id).filter((id): id is string => id !== null);\n const numeric = ids.filter((id) => /^\\d+$/.test(id)).map(Number);\n if (numeric.length === 0 && ids.length > 0) return null;\n const next = numeric.length === 0 ? 1 : Math.max(...numeric) + 1;\n return String(next).padStart(corpus.config.id.digits, '0');\n}\n\nexport function fileNameFor(config: Config, id: string, title: string): string {\n const slug = slugify(title);\n if (config.id.source === 'frontmatter') return `${slug === '' ? id : slug}.md`;\n return slug === '' ? `${id}.md` : `${id}${config.id.separator}${slug}.md`;\n}\n\nconst HINTS: Readonly<Record<string, string>> = {\n intent: 'The state of the tree when this round is done, and why it matters. One paragraph.',\n 'negative scope': 'What this round must not do, even where it would look helpful.',\n 'not empowered': 'Files, interfaces and decisions this round may not change.',\n invariants: 'Checks that must hold before the round is called done, each one a task item.',\n 'acceptance criteria': 'What a reviewer checks to accept the result, each one a task item.',\n 'the defect, measured': 'How to reproduce the defect, and the measurement that shows it.',\n};\n\nfunction hint(rule: SectionRule): string {\n return HINTS[rule.name.toLowerCase()] ?? `Write the ${rule.name}.`;\n}\n\nfunction frontMatter(config: Config, brief: NewBrief): string[] {\n const lines = ['---'];\n if (config.id.source === 'frontmatter') lines.push(`id: ${renderScalar(brief.id)}`);\n if (config.status.field !== null) {\n lines.push(`${config.status.field}: ${renderScalar(config.status.draft ?? config.status.active)}`);\n }\n lines.push(`date: ${brief.date}`);\n if (brief.type !== undefined) lines.push(`type: ${renderScalar(brief.type)}`);\n if (brief.wave !== undefined) lines.push(`wave: ${brief.wave}`);\n if (brief.dependsOn !== undefined && brief.dependsOn.length > 0) {\n lines.push(`dependsOn: [${brief.dependsOn.map((d) => JSON.stringify(d)).join(', ')}]`);\n }\n lines.push('affectedFiles: []', 'protectedFiles: []', '---');\n return lines;\n}\n\n/** The text of a new brief, from the configured template or from the section list. */\nexport function renderNewBrief(config: Config, brief: NewBrief, template: string | null): string {\n if (template !== null) {\n return fillTemplate(template, {\n id: brief.id,\n title: brief.title,\n date: brief.date,\n type: brief.type ?? '',\n wave: brief.wave === undefined ? '' : String(brief.wave),\n status: config.status.draft ?? config.status.active,\n });\n }\n const typed = brief.type === undefined ? [] : (config.types[brief.type] ?? []);\n const lines = [...frontMatter(config, brief), '', `# ${brief.id} \\u2014 ${brief.title}`];\n for (const rule of [...config.sections, ...typed]) {\n lines.push('', `## ${rule.name}`, '', `<!-- ${hint(rule)} -->`);\n // Text a section must contain is written in, so it is not forgotten.\n if (rule.mustContain.length > 0) lines.push('', ...rule.mustContain.map((text) => `- ${text}`));\n if (rule.checklist) lines.push('', '- [ ] <!-- one check -->');\n }\n return `${lines.join('\\n')}\\n`;\n}\n"]}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A small schema language: one description that both validates a value and
3
+ * renders as JSON Schema, so the published `schema.json` and the checks the
4
+ * loader runs cannot disagree about what a configuration may say.
5
+ */
6
+ interface Described {
7
+ readonly description?: string;
8
+ }
9
+ export type Schema = (Described & {
10
+ readonly type: 'string';
11
+ readonly enum?: readonly string[];
12
+ readonly nullable?: boolean;
13
+ readonly minLength?: number;
14
+ }) | (Described & {
15
+ readonly type: 'boolean';
16
+ }) | (Described & {
17
+ readonly type: 'integer';
18
+ readonly minimum?: number;
19
+ readonly maximum?: number;
20
+ }) | (Described & {
21
+ readonly type: 'array';
22
+ readonly items: Schema;
23
+ }) | (Described & {
24
+ readonly type: 'object';
25
+ readonly properties: Readonly<Record<string, Schema>>;
26
+ readonly required?: readonly string[];
27
+ }) | (Described & {
28
+ readonly type: 'map';
29
+ readonly values: Schema;
30
+ }) | (Described & {
31
+ readonly type: 'anyOf';
32
+ readonly options: readonly Schema[];
33
+ }) | (Described & {
34
+ readonly type: 'any';
35
+ });
36
+ /** The problems with `value`, each naming the path to it. Empty when it conforms. */
37
+ export declare function validate(schema: Schema, value: unknown, path?: string): string[];
38
+ /** The known key nearest to a misspelling, when one is near enough to be what was meant. */
39
+ export declare function closest(word: string, options: readonly string[]): string | undefined;
40
+ type Json = null | boolean | number | string | Json[] | {
41
+ [key: string]: Json;
42
+ };
43
+ /** Renders a schema as JSON Schema (draft 2020-12). */
44
+ export declare function toJsonSchema(schema: Schema): {
45
+ [key: string]: Json;
46
+ };
47
+ export {};
package/dist/schema.js ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * A small schema language: one description that both validates a value and
3
+ * renders as JSON Schema, so the published `schema.json` and the checks the
4
+ * loader runs cannot disagree about what a configuration may say.
5
+ */
6
+ function kindOf(value) {
7
+ if (value === null)
8
+ return 'null';
9
+ if (Array.isArray(value))
10
+ return 'array';
11
+ switch (typeof value) {
12
+ case 'string':
13
+ return 'string';
14
+ case 'boolean':
15
+ return 'boolean';
16
+ case 'number':
17
+ return 'number';
18
+ default:
19
+ return 'object';
20
+ }
21
+ }
22
+ function accepts(schema, kind) {
23
+ switch (schema.type) {
24
+ case 'string':
25
+ return kind === 'string' || (kind === 'null' && schema.nullable === true);
26
+ case 'boolean':
27
+ return kind === 'boolean';
28
+ case 'integer':
29
+ return kind === 'number';
30
+ case 'array':
31
+ return kind === 'array';
32
+ case 'object':
33
+ case 'map':
34
+ return kind === 'object';
35
+ case 'anyOf':
36
+ return schema.options.some((option) => accepts(option, kind));
37
+ case 'any':
38
+ return true;
39
+ }
40
+ }
41
+ function noun(schema) {
42
+ switch (schema.type) {
43
+ case 'string':
44
+ return schema.nullable === true ? 'a string or null' : 'a string';
45
+ case 'boolean':
46
+ return 'true or false';
47
+ case 'integer':
48
+ return 'an integer';
49
+ case 'array':
50
+ return 'a list';
51
+ case 'object':
52
+ case 'map':
53
+ return 'an object';
54
+ case 'anyOf':
55
+ return schema.options.map(noun).join(' or ');
56
+ /* v8 ignore next 2 -- "any" accepts every value, so no message ever names it. */
57
+ case 'any':
58
+ return 'anything';
59
+ }
60
+ }
61
+ function at(path, key) {
62
+ return path === '' ? key : `${path}.${key}`;
63
+ }
64
+ /** The problems with `value`, each naming the path to it. Empty when it conforms. */
65
+ export function validate(schema, value, path = '') {
66
+ const where = path === '' ? 'the configuration' : `"${path}"`;
67
+ if (!accepts(schema, kindOf(value)))
68
+ return [`${where} must be ${noun(schema)}`];
69
+ switch (schema.type) {
70
+ case 'string': {
71
+ if (typeof value !== 'string')
72
+ return [];
73
+ if (schema.enum !== undefined && !schema.enum.includes(value)) {
74
+ return [`${where} must be one of ${schema.enum.map((e) => `"${e}"`).join(', ')}, not "${value}"`];
75
+ }
76
+ if (schema.minLength !== undefined && value.length < schema.minLength)
77
+ return [`${where} must not be empty`];
78
+ return [];
79
+ }
80
+ case 'integer': {
81
+ const number = value;
82
+ if (!Number.isInteger(number))
83
+ return [`${where} must be an integer`];
84
+ if (schema.minimum !== undefined && number < schema.minimum)
85
+ return [`${where} must be at least ${schema.minimum}`];
86
+ if (schema.maximum !== undefined && number > schema.maximum)
87
+ return [`${where} must be at most ${schema.maximum}`];
88
+ return [];
89
+ }
90
+ case 'array':
91
+ return value.flatMap((item, i) => validate(schema.items, item, `${path}[${i}]`));
92
+ case 'object': {
93
+ const record = value;
94
+ const known = Object.keys(schema.properties);
95
+ const problems = [];
96
+ for (const key of Object.keys(record)) {
97
+ const property = schema.properties[key];
98
+ if (property === undefined) {
99
+ const guess = closest(key, known);
100
+ problems.push(`"${at(path, key)}" is not a known key${guess === undefined ? '' : `; did you mean "${guess}"?`}`);
101
+ continue;
102
+ }
103
+ problems.push(...validate(property, record[key], at(path, key)));
104
+ }
105
+ for (const key of schema.required ?? []) {
106
+ if (!(key in record))
107
+ problems.push(`"${at(path, key)}" is required`);
108
+ }
109
+ return problems;
110
+ }
111
+ case 'map':
112
+ return Object.entries(value).flatMap(([key, item]) => validate(schema.values, item, at(path, key)));
113
+ case 'anyOf': {
114
+ const candidates = schema.options.filter((option) => accepts(option, kindOf(value)));
115
+ let first;
116
+ for (const option of candidates) {
117
+ const problems = validate(option, value, path);
118
+ if (problems.length === 0)
119
+ return [];
120
+ first ??= problems;
121
+ }
122
+ return first ?? [];
123
+ }
124
+ case 'boolean':
125
+ case 'any':
126
+ return [];
127
+ }
128
+ }
129
+ /** The known key nearest to a misspelling, when one is near enough to be what was meant. */
130
+ export function closest(word, options) {
131
+ let best;
132
+ let bestDistance = Math.max(2, Math.floor(word.length / 3)) + 1;
133
+ for (const option of options) {
134
+ const d = distance(word.toLowerCase(), option.toLowerCase());
135
+ if (d < bestDistance) {
136
+ best = option;
137
+ bestDistance = d;
138
+ }
139
+ }
140
+ return best;
141
+ }
142
+ function distance(a, b) {
143
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
144
+ for (let i = 1; i <= a.length; i += 1) {
145
+ const row = [i];
146
+ for (let j = 1; j <= b.length; j += 1) {
147
+ const cost = a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1;
148
+ row.push(Math.min(previous[j] + 1, row[j - 1] + 1, previous[j - 1] + cost));
149
+ }
150
+ previous = row;
151
+ }
152
+ return previous[b.length];
153
+ }
154
+ /** Renders a schema as JSON Schema (draft 2020-12). */
155
+ export function toJsonSchema(schema) {
156
+ const described = (body) => schema.description === undefined ? body : { description: schema.description, ...body };
157
+ switch (schema.type) {
158
+ case 'string': {
159
+ const body = { type: schema.nullable === true ? ['string', 'null'] : 'string' };
160
+ if (schema.enum !== undefined)
161
+ body['enum'] = [...schema.enum, ...(schema.nullable === true ? [null] : [])];
162
+ if (schema.minLength !== undefined)
163
+ body['minLength'] = schema.minLength;
164
+ return described(body);
165
+ }
166
+ case 'boolean':
167
+ return described({ type: 'boolean' });
168
+ case 'integer': {
169
+ const body = { type: 'integer' };
170
+ if (schema.minimum !== undefined)
171
+ body['minimum'] = schema.minimum;
172
+ if (schema.maximum !== undefined)
173
+ body['maximum'] = schema.maximum;
174
+ return described(body);
175
+ }
176
+ case 'array':
177
+ return described({ type: 'array', items: toJsonSchema(schema.items) });
178
+ case 'object': {
179
+ const properties = {};
180
+ for (const [key, value] of Object.entries(schema.properties))
181
+ properties[key] = toJsonSchema(value);
182
+ const body = { type: 'object', properties, additionalProperties: false };
183
+ if (schema.required !== undefined && schema.required.length > 0)
184
+ body['required'] = [...schema.required];
185
+ return described(body);
186
+ }
187
+ case 'map':
188
+ return described({ type: 'object', additionalProperties: toJsonSchema(schema.values) });
189
+ case 'anyOf':
190
+ return described({ anyOf: schema.options.map(toJsonSchema) });
191
+ case 'any':
192
+ return described({});
193
+ }
194
+ }
195
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A small schema language: one description that both validates a value and\n * renders as JSON Schema, so the published `schema.json` and the checks the\n * loader runs cannot disagree about what a configuration may say.\n */\n\ninterface Described {\n readonly description?: string;\n}\n\nexport type Schema =\n | (Described & {\n readonly type: 'string';\n readonly enum?: readonly string[];\n readonly nullable?: boolean;\n readonly minLength?: number;\n })\n | (Described & { readonly type: 'boolean' })\n | (Described & { readonly type: 'integer'; readonly minimum?: number; readonly maximum?: number })\n | (Described & { readonly type: 'array'; readonly items: Schema })\n | (Described & {\n readonly type: 'object';\n readonly properties: Readonly<Record<string, Schema>>;\n readonly required?: readonly string[];\n })\n | (Described & { readonly type: 'map'; readonly values: Schema })\n | (Described & { readonly type: 'anyOf'; readonly options: readonly Schema[] })\n | (Described & { readonly type: 'any' });\n\ntype Kind = 'null' | 'string' | 'boolean' | 'number' | 'array' | 'object';\n\nfunction kindOf(value: unknown): Kind {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n switch (typeof value) {\n case 'string':\n return 'string';\n case 'boolean':\n return 'boolean';\n case 'number':\n return 'number';\n default:\n return 'object';\n }\n}\n\nfunction accepts(schema: Schema, kind: Kind): boolean {\n switch (schema.type) {\n case 'string':\n return kind === 'string' || (kind === 'null' && schema.nullable === true);\n case 'boolean':\n return kind === 'boolean';\n case 'integer':\n return kind === 'number';\n case 'array':\n return kind === 'array';\n case 'object':\n case 'map':\n return kind === 'object';\n case 'anyOf':\n return schema.options.some((option) => accepts(option, kind));\n case 'any':\n return true;\n }\n}\n\nfunction noun(schema: Schema): string {\n switch (schema.type) {\n case 'string':\n return schema.nullable === true ? 'a string or null' : 'a string';\n case 'boolean':\n return 'true or false';\n case 'integer':\n return 'an integer';\n case 'array':\n return 'a list';\n case 'object':\n case 'map':\n return 'an object';\n case 'anyOf':\n return schema.options.map(noun).join(' or ');\n /* v8 ignore next 2 -- \"any\" accepts every value, so no message ever names it. */\n case 'any':\n return 'anything';\n }\n}\n\nfunction at(path: string, key: string): string {\n return path === '' ? key : `${path}.${key}`;\n}\n\n/** The problems with `value`, each naming the path to it. Empty when it conforms. */\nexport function validate(schema: Schema, value: unknown, path = ''): string[] {\n const where = path === '' ? 'the configuration' : `\"${path}\"`;\n if (!accepts(schema, kindOf(value))) return [`${where} must be ${noun(schema)}`];\n switch (schema.type) {\n case 'string': {\n if (typeof value !== 'string') return [];\n if (schema.enum !== undefined && !schema.enum.includes(value)) {\n return [`${where} must be one of ${schema.enum.map((e) => `\"${e}\"`).join(', ')}, not \"${value}\"`];\n }\n if (schema.minLength !== undefined && value.length < schema.minLength) return [`${where} must not be empty`];\n return [];\n }\n case 'integer': {\n const number = value as number;\n if (!Number.isInteger(number)) return [`${where} must be an integer`];\n if (schema.minimum !== undefined && number < schema.minimum) return [`${where} must be at least ${schema.minimum}`];\n if (schema.maximum !== undefined && number > schema.maximum) return [`${where} must be at most ${schema.maximum}`];\n return [];\n }\n case 'array':\n return (value as unknown[]).flatMap((item, i) => validate(schema.items, item, `${path}[${i}]`));\n case 'object': {\n const record = value as Record<string, unknown>;\n const known = Object.keys(schema.properties);\n const problems: string[] = [];\n for (const key of Object.keys(record)) {\n const property = schema.properties[key];\n if (property === undefined) {\n const guess = closest(key, known);\n problems.push(`\"${at(path, key)}\" is not a known key${guess === undefined ? '' : `; did you mean \"${guess}\"?`}`);\n continue;\n }\n problems.push(...validate(property, record[key], at(path, key)));\n }\n for (const key of schema.required ?? []) {\n if (!(key in record)) problems.push(`\"${at(path, key)}\" is required`);\n }\n return problems;\n }\n case 'map':\n return Object.entries(value as Record<string, unknown>).flatMap(([key, item]) =>\n validate(schema.values, item, at(path, key)),\n );\n case 'anyOf': {\n const candidates = schema.options.filter((option) => accepts(option, kindOf(value)));\n let first: string[] | undefined;\n for (const option of candidates) {\n const problems = validate(option, value, path);\n if (problems.length === 0) return [];\n first ??= problems;\n }\n return first ?? [];\n }\n case 'boolean':\n case 'any':\n return [];\n }\n}\n\n/** The known key nearest to a misspelling, when one is near enough to be what was meant. */\nexport function closest(word: string, options: readonly string[]): string | undefined {\n let best: string | undefined;\n let bestDistance = Math.max(2, Math.floor(word.length / 3)) + 1;\n for (const option of options) {\n const d = distance(word.toLowerCase(), option.toLowerCase());\n if (d < bestDistance) {\n best = option;\n bestDistance = d;\n }\n }\n return best;\n}\n\nfunction distance(a: string, b: string): number {\n let previous = Array.from({ length: b.length + 1 }, (_, j) => j);\n for (let i = 1; i <= a.length; i += 1) {\n const row = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const cost = a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1;\n row.push(Math.min((previous[j] as number) + 1, (row[j - 1] as number) + 1, (previous[j - 1] as number) + cost));\n }\n previous = row;\n }\n return previous[b.length] as number;\n}\n\ntype Json = null | boolean | number | string | Json[] | { [key: string]: Json };\n\n/** Renders a schema as JSON Schema (draft 2020-12). */\nexport function toJsonSchema(schema: Schema): { [key: string]: Json } {\n const described = (body: { [key: string]: Json }): { [key: string]: Json } =>\n schema.description === undefined ? body : { description: schema.description, ...body };\n switch (schema.type) {\n case 'string': {\n const body: { [key: string]: Json } = { type: schema.nullable === true ? ['string', 'null'] : 'string' };\n if (schema.enum !== undefined) body['enum'] = [...schema.enum, ...(schema.nullable === true ? [null] : [])];\n if (schema.minLength !== undefined) body['minLength'] = schema.minLength;\n return described(body);\n }\n case 'boolean':\n return described({ type: 'boolean' });\n case 'integer': {\n const body: { [key: string]: Json } = { type: 'integer' };\n if (schema.minimum !== undefined) body['minimum'] = schema.minimum;\n if (schema.maximum !== undefined) body['maximum'] = schema.maximum;\n return described(body);\n }\n case 'array':\n return described({ type: 'array', items: toJsonSchema(schema.items) });\n case 'object': {\n const properties: { [key: string]: Json } = {};\n for (const [key, value] of Object.entries(schema.properties)) properties[key] = toJsonSchema(value);\n const body: { [key: string]: Json } = { type: 'object', properties, additionalProperties: false };\n if (schema.required !== undefined && schema.required.length > 0) body['required'] = [...schema.required];\n return described(body);\n }\n case 'map':\n return described({ type: 'object', additionalProperties: toJsonSchema(schema.values) });\n case 'anyOf':\n return described({ anyOf: schema.options.map(toJsonSchema) });\n case 'any':\n return described({});\n }\n}\n"]}
package/dist/text.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Line handling and the normalisations every other module agrees on.
3
+ */
4
+ /** Splits text into lines without their terminators. A final newline adds no line. */
5
+ export declare function splitLines(text: string): string[];
6
+ /**
7
+ * The line ending a file already uses, so a rewrite does not flip it. Decided by
8
+ * the first terminator; a file with none is written with LF.
9
+ */
10
+ export declare function lineEnding(text: string): '\n' | '\r\n';
11
+ /** Joins lines with the given ending and a final newline. */
12
+ export declare function joinLines(lines: readonly string[], eol: '\n' | '\r\n'): string;
13
+ export declare function stripBom(text: string): string;
14
+ /** CRLF and a byte-order mark are storage details, not content. */
15
+ export declare function canonicalText(text: string): string;
16
+ /**
17
+ * The form in which two section names are compared.
18
+ *
19
+ * Headings are typed by people and pasted from documents, so the comparison
20
+ * forgives what does not change meaning: typographic apostrophes and quotes,
21
+ * emphasis and code markers, a leading number, a trailing colon, case and runs
22
+ * of whitespace. `## 2. Commander’s Intent:` and `Commander's Intent` compare
23
+ * equal. Nothing else is folded: a different word is a different section.
24
+ */
25
+ export declare function normaliseLabel(label: string): string;
26
+ /**
27
+ * Whether a heading names a section: equal after normalisation, or the section
28
+ * name followed by a separator and a qualifier, as in `Invariants (must hold)`
29
+ * or `Deliverables — round two`.
30
+ */
31
+ export declare function labelMatches(heading: string, name: string): boolean;
32
+ /**
33
+ * A file-name slug: lower case, ASCII letters and digits, single hyphens.
34
+ * Letters with diacritics lose them; anything else becomes a separator.
35
+ */
36
+ export declare function slugify(title: string, maxLength?: number): string;
37
+ /** Fills `{name}` holes. An unknown name is left as written. */
38
+ export declare function fillTemplate(template: string, values: Readonly<Record<string, string>>): string;
39
+ /** The `{name}` holes a template line refers to. */
40
+ export declare function templateHoles(template: string): string[];
package/dist/text.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Line handling and the normalisations every other module agrees on.
3
+ */
4
+ /** Splits text into lines without their terminators. A final newline adds no line. */
5
+ export function splitLines(text) {
6
+ const lines = text.split(/\r?\n/);
7
+ if (lines.length > 1 && lines[lines.length - 1] === '')
8
+ lines.pop();
9
+ return lines;
10
+ }
11
+ /**
12
+ * The line ending a file already uses, so a rewrite does not flip it. Decided by
13
+ * the first terminator; a file with none is written with LF.
14
+ */
15
+ export function lineEnding(text) {
16
+ return text.charAt(text.indexOf('\n') - 1) === '\r' ? '\r\n' : '\n';
17
+ }
18
+ /** Joins lines with the given ending and a final newline. */
19
+ export function joinLines(lines, eol) {
20
+ return lines.length === 0 ? '' : `${lines.join(eol)}${eol}`;
21
+ }
22
+ export function stripBom(text) {
23
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
24
+ }
25
+ /** CRLF and a byte-order mark are storage details, not content. */
26
+ export function canonicalText(text) {
27
+ return stripBom(text).replace(/\r\n/g, '\n');
28
+ }
29
+ /**
30
+ * The form in which two section names are compared.
31
+ *
32
+ * Headings are typed by people and pasted from documents, so the comparison
33
+ * forgives what does not change meaning: typographic apostrophes and quotes,
34
+ * emphasis and code markers, a leading number, a trailing colon, case and runs
35
+ * of whitespace. `## 2. Commander’s Intent:` and `Commander's Intent` compare
36
+ * equal. Nothing else is folded: a different word is a different section.
37
+ */
38
+ export function normaliseLabel(label) {
39
+ return label
40
+ .normalize('NFKC')
41
+ .replace(/[\u2018\u2019\u201b\u2032\u00b4]/g, "'")
42
+ .replace(/[\u201c\u201d\u201f\u2033]/g, '"')
43
+ .replace(/[*_`]/g, '')
44
+ .replace(/^\s*\d+(?:\.\d+)*[.)]?\s+/, '')
45
+ .replace(/\s*:\s*$/, '')
46
+ .replace(/\s+/g, ' ')
47
+ .trim()
48
+ .toLowerCase();
49
+ }
50
+ /**
51
+ * Whether a heading names a section: equal after normalisation, or the section
52
+ * name followed by a separator and a qualifier, as in `Invariants (must hold)`
53
+ * or `Deliverables — round two`.
54
+ */
55
+ export function labelMatches(heading, name) {
56
+ const h = normaliseLabel(heading);
57
+ const n = normaliseLabel(name);
58
+ if (n.length === 0)
59
+ return false;
60
+ if (h === n)
61
+ return true;
62
+ if (!h.startsWith(n))
63
+ return false;
64
+ const rest = h.slice(n.length);
65
+ return /^(?:\s*[:(\u2014\u2013]|\s+-\s)/.test(rest);
66
+ }
67
+ /**
68
+ * A file-name slug: lower case, ASCII letters and digits, single hyphens.
69
+ * Letters with diacritics lose them; anything else becomes a separator.
70
+ */
71
+ export function slugify(title, maxLength = 80) {
72
+ const slug = title
73
+ .normalize('NFKD')
74
+ .replace(/[\u0300-\u036f]/g, '')
75
+ .toLowerCase()
76
+ .replace(/[^a-z0-9]+/g, '-')
77
+ .replace(/^-+|-+$/g, '');
78
+ if (slug.length <= maxLength)
79
+ return slug;
80
+ const cut = slug.slice(0, maxLength);
81
+ // A cut that lands on a break keeps its last word whole.
82
+ if (slug.charAt(maxLength) === '-')
83
+ return cut;
84
+ const lastBreak = cut.lastIndexOf('-');
85
+ return lastBreak > 0 ? cut.slice(0, lastBreak) : cut;
86
+ }
87
+ /** Fills `{name}` holes. An unknown name is left as written. */
88
+ export function fillTemplate(template, values) {
89
+ return template.replace(/\{([A-Za-z]+)\}/g, (hole, name) => values[name] ?? hole);
90
+ }
91
+ /** The `{name}` holes a template line refers to. */
92
+ export function templateHoles(template) {
93
+ return [...template.matchAll(/\{([A-Za-z]+)\}/g)].map((match) => match[1]);
94
+ }
95
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Line handling and the normalisations every other module agrees on.\n */\n\n/** Splits text into lines without their terminators. A final newline adds no line. */\nexport function splitLines(text: string): string[] {\n const lines = text.split(/\\r?\\n/);\n if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();\n return lines;\n}\n\n/**\n * The line ending a file already uses, so a rewrite does not flip it. Decided by\n * the first terminator; a file with none is written with LF.\n */\nexport function lineEnding(text: string): '\\n' | '\\r\\n' {\n return text.charAt(text.indexOf('\\n') - 1) === '\\r' ? '\\r\\n' : '\\n';\n}\n\n/** Joins lines with the given ending and a final newline. */\nexport function joinLines(lines: readonly string[], eol: '\\n' | '\\r\\n'): string {\n return lines.length === 0 ? '' : `${lines.join(eol)}${eol}`;\n}\n\nexport function stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\n/** CRLF and a byte-order mark are storage details, not content. */\nexport function canonicalText(text: string): string {\n return stripBom(text).replace(/\\r\\n/g, '\\n');\n}\n\n/**\n * The form in which two section names are compared.\n *\n * Headings are typed by people and pasted from documents, so the comparison\n * forgives what does not change meaning: typographic apostrophes and quotes,\n * emphasis and code markers, a leading number, a trailing colon, case and runs\n * of whitespace. `## 2. Commander’s Intent:` and `Commander's Intent` compare\n * equal. Nothing else is folded: a different word is a different section.\n */\nexport function normaliseLabel(label: string): string {\n return label\n .normalize('NFKC')\n .replace(/[\\u2018\\u2019\\u201b\\u2032\\u00b4]/g, \"'\")\n .replace(/[\\u201c\\u201d\\u201f\\u2033]/g, '\"')\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+(?:\\.\\d+)*[.)]?\\s+/, '')\n .replace(/\\s*:\\s*$/, '')\n .replace(/\\s+/g, ' ')\n .trim()\n .toLowerCase();\n}\n\n/**\n * Whether a heading names a section: equal after normalisation, or the section\n * name followed by a separator and a qualifier, as in `Invariants (must hold)`\n * or `Deliverables — round two`.\n */\nexport function labelMatches(heading: string, name: string): boolean {\n const h = normaliseLabel(heading);\n const n = normaliseLabel(name);\n if (n.length === 0) return false;\n if (h === n) return true;\n if (!h.startsWith(n)) return false;\n const rest = h.slice(n.length);\n return /^(?:\\s*[:(\\u2014\\u2013]|\\s+-\\s)/.test(rest);\n}\n\n/**\n * A file-name slug: lower case, ASCII letters and digits, single hyphens.\n * Letters with diacritics lose them; anything else becomes a separator.\n */\nexport function slugify(title: string, maxLength = 80): string {\n const slug = title\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n if (slug.length <= maxLength) return slug;\n const cut = slug.slice(0, maxLength);\n // A cut that lands on a break keeps its last word whole.\n if (slug.charAt(maxLength) === '-') return cut;\n const lastBreak = cut.lastIndexOf('-');\n return lastBreak > 0 ? cut.slice(0, lastBreak) : cut;\n}\n\n/** Fills `{name}` holes. An unknown name is left as written. */\nexport function fillTemplate(template: string, values: Readonly<Record<string, string>>): string {\n return template.replace(/\\{([A-Za-z]+)\\}/g, (hole, name: string) => values[name] ?? hole);\n}\n\n/** The `{name}` holes a template line refers to. */\nexport function templateHoles(template: string): string[] {\n return [...template.matchAll(/\\{([A-Za-z]+)\\}/g)].map((match) => match[1] as string);\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Types shared across the pipeline. Type-only: this module compiles to nothing.
3
+ */
4
+ export type Severity = 'error' | 'warning' | 'note';
5
+ /** A severity as configuration spells it, where `off` silences a rule. */
6
+ export type SeveritySetting = Severity | 'off';
7
+ /** Where a brief lives. Location is the authority on whether a brief is archived. */
8
+ export type Phase = 'live' | 'archived';
9
+ /**
10
+ * The lifecycle a brief moves through. `draft` is optional: a repository that
11
+ * never writes one maps no word to it. There is no persisted "archiving" state;
12
+ * archival is a transaction, and a transaction that stops half-way is rolled
13
+ * back rather than recorded.
14
+ */
15
+ export type Status = 'draft' | 'active' | 'archived';
16
+ /**
17
+ * One problem, located. Lines are 1-based; a finding about a file as a whole
18
+ * points at line 1.
19
+ */
20
+ export interface Finding {
21
+ readonly rule: string;
22
+ readonly severity: Severity;
23
+ readonly message: string;
24
+ /** Repository-relative POSIX path. */
25
+ readonly file: string;
26
+ readonly line: number;
27
+ readonly brief?: string | undefined;
28
+ /** The concrete next action, where there is one. */
29
+ readonly hint?: string | undefined;
30
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Types shared across the pipeline. Type-only: this module compiles to nothing.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * Types shared across the pipeline. Type-only: this module compiles to nothing.\n */\n\nexport type Severity = 'error' | 'warning' | 'note';\n\n/** A severity as configuration spells it, where `off` silences a rule. */\nexport type SeveritySetting = Severity | 'off';\n\n/** Where a brief lives. Location is the authority on whether a brief is archived. */\nexport type Phase = 'live' | 'archived';\n\n/**\n * The lifecycle a brief moves through. `draft` is optional: a repository that\n * never writes one maps no word to it. There is no persisted \"archiving\" state;\n * archival is a transaction, and a transaction that stops half-way is rolled\n * back rather than recorded.\n */\nexport type Status = 'draft' | 'active' | 'archived';\n\n/**\n * One problem, located. Lines are 1-based; a finding about a file as a whole\n * points at line 1.\n */\nexport interface Finding {\n readonly rule: string;\n readonly severity: Severity;\n readonly message: string;\n /** Repository-relative POSIX path. */\n readonly file: string;\n readonly line: number;\n readonly brief?: string | undefined;\n /** The concrete next action, where there is one. */\n readonly hint?: string | undefined;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@descent-vtt/spec-brief",
3
+ "version": "0.1.0",
4
+ "description": "Lifecycle management for task briefs: lint the contract, map parallel collisions, and archive a round in one atomic command.",
5
+ "keywords": [
6
+ "agents",
7
+ "archive",
8
+ "brief",
9
+ "collision",
10
+ "lifecycle",
11
+ "lint",
12
+ "markdown",
13
+ "spec",
14
+ "task",
15
+ "wave"
16
+ ],
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "bin": {
23
+ "spec-brief": "bin/spec-brief.js"
24
+ },
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./schema.json": "./schema.json",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "CHANGELOG.md",
37
+ "LICENSE",
38
+ "README.md",
39
+ "bin",
40
+ "dist",
41
+ "schema.json"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.build.json",
48
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
49
+ "lint": "tsc -p tsconfig.json --noEmit",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "test:coverage": "vitest run --coverage",
53
+ "test:mutation": "stryker run stryker.core.config.mjs",
54
+ "test:mutation:full": "stryker run",
55
+ "schema": "npm run build && node scripts/write-schema.mjs",
56
+ "selfcheck": "node ./bin/spec-brief.js lint",
57
+ "prepublishOnly": "npm run clean && npm run build"
58
+ },
59
+ "dependencies": {},
60
+ "devDependencies": {
61
+ "@stryker-mutator/core": "^10.0.0",
62
+ "@stryker-mutator/vitest-runner": "^10.0.0",
63
+ "@types/node": "^22.20.4",
64
+ "@vitest/coverage-v8": "^4.1.11",
65
+ "typescript": "^7.0.2",
66
+ "vitest": "^4.1.11"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/DescentVTT/spec-brief.git"
71
+ },
72
+ "bugs": {
73
+ "url": "https://github.com/DescentVTT/spec-brief/issues"
74
+ },
75
+ "homepage": "https://github.com/DescentVTT/spec-brief#readme"
76
+ }