@descent-vtt/spec-brief 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +4 -4
- package/dist/config.js +1 -2
- package/dist/config.js.map +1 -1
- package/dist/glob.d.ts +8 -0
- package/dist/glob.js +72 -0
- package/dist/glob.js.map +1 -1
- package/dist/report.js +1 -0
- package/dist/report.js.map +1 -1
- package/dist/rules.js +13 -4
- package/dist/rules.js.map +1 -1
- package/dist/scope.js +2 -2
- package/dist/scope.js.map +1 -1
- package/dist/trigger.d.ts +2 -1
- package/dist/trigger.js +5 -60
- package/dist/trigger.js.map +1 -1
- package/package.json +1 -1
package/dist/scope.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A brief's scope, and whether two briefs can write the same file.\n *\n * What a brief may write is its `affectedFiles` less its `protectedFiles`:\n * protection wins, so \"all of `src/` but the schema\" is written\n * `affectedFiles: [src/**]` with `protectedFiles: [src/db/schema.ts]`. Two\n * briefs meet when some file lies in both writable scopes - in a pattern of\n * each, and in neither brief's protections - and the witness is such a file\n * the tree holds, or else the shortest such path the search builds, as an\n * example (ADR-0005).\n */\n\nimport type { Brief } from './brief.js';\nimport { type Glob, globCovers, globWitness, type LiteralReading, matchGlob, parseGlob, WITNESS_BUDGET } from './glob.js';\n\n/** How a literal path is read, as {@link readingIn} answers for a tree. */\nexport type Reading = (path: string) => LiteralReading;\n\nexport interface ScopePattern {\n readonly pattern: string;\n readonly glob: Glob;\n}\n\nexport interface Scope {\n readonly brief: Brief;\n /** The `affectedFiles` that parse. One that does not is the `glob` rule's finding, and scopes nothing. */\n readonly affected: readonly ScopePattern[];\n readonly protected: readonly ScopePattern[];\n}\n\n/** Patterns that parse, read with one reading. */\nexport function patternsOf(patterns: readonly string[], reading: Reading): ScopePattern[] {\n return patterns.flatMap((pattern) => {\n const parsed = parseGlob(pattern, { literal: reading });\n return parsed.ok ? [{ pattern, glob: parsed.glob }] : [];\n });\n}\n\nexport function scopeOf(brief: Brief, reading: Reading): Scope {\n return { brief, affected: patternsOf(brief.affectedFiles, reading), protected: patternsOf(brief.protectedFiles, reading) };\n}\n\n/** A pattern from each scope, and a file both may write. */\nexport interface Overlap {\n readonly patterns: readonly [string, string];\n readonly witness: string;\n /**\n * `true` when the witness is a file the tree holds; `false` when the tree\n * holds none both patterns match outside the protections, and the witness\n * is an example the search built; `null` when no tree was given.\n */\n readonly inTree: boolean | null;\n}\n\nexport interface Meeting {\n /** Every pair of patterns that meets outside both protections, the first scope's pattern first. */\n readonly overlaps: readonly Overlap[];\n /** Pairs of patterns the search could not decide within its budget, and no file of the tree decides. */\n readonly undecided: readonly (readonly [string, string])[];\n}\n\nconst matched = new WeakMap<readonly string[], WeakMap<Glob, ReadonlySet<string>>>();\n\n/**\n * The files of a tree a pattern matches, in the tree's order. Found once per\n * pattern and tree, which every pair a brief is in shares; kept or not, the\n * answer is the same, and only the time differs.\n */\nfunction matching(files: readonly string[], glob: Glob): ReadonlySet<string> {\n let byGlob = matched.get(files);\n if (byGlob === undefined) {\n byGlob = new WeakMap();\n matched.set(files, byGlob);\n }\n let found = byGlob.get(glob);\n if (found === undefined) {\n found = new Set(files.filter((file) => matchGlob(glob, file)));\n byGlob.set(glob, found);\n }\n return found;\n}\n\n/**\n * The first file of the tree, in its order, that both patterns match and no\n * protection does. The engine's list is sorted, so the file named is the same\n * on every run.\n */\nfunction heldInBoth(files: readonly string[], x: Glob, y: Glob, avoid: readonly Glob[]): string | undefined {\n const other = matching(files, y);\n for (const file of matching(files, x)) {\n if (other.has(file) && !avoid.some((glob) => matchGlob(glob, file))) return file;\n }\n return undefined;\n}\n\n/**\n * Where two writable scopes meet. Each pair of affected patterns is searched\n * for a file both cover that neither brief protects: a file one brief protects\n * is one that brief will not write, so it is no collision.\n *\n * The search names the shortest such path, which the tree need not hold:\n * `src/Shop/OrderService.cs` where the tree has `src/Shop/Orders/OrderService.cs`.\n * So where the search finds a meeting, or cannot decide one, the tree's files\n * are read, and the first both patterns match outside the protections is the\n * witness (ADR-0005, amended 2026-09-30). A file that exists and both briefs\n * may write settles a pair the search could not: it is a collision, proved by\n * the file, not a guess. Where the tree holds none, the search's path stands,\n * as an example. A pair the search proves apart has no such file, and the tree\n * is not read for it.\n */\nexport function meet(a: Scope, b: Scope, budget: number = WITNESS_BUDGET, files: readonly string[] | null = null): Meeting {\n const avoid = [...a.protected, ...b.protected].map((p) => p.glob);\n const overlaps: Overlap[] = [];\n const undecided: (readonly [string, string])[] = [];\n for (const x of a.affected) {\n for (const y of b.affected) {\n const witness = globWitness([x.glob, y.glob], avoid, budget);\n if (witness.kind === 'none') continue;\n const patterns = [x.pattern, y.pattern] as const;\n const held = files === null ? undefined : heldInBoth(files, x.glob, y.glob, avoid);\n if (held !== undefined) overlaps.push({ patterns, witness: held, inTree: true });\n else if (witness.kind === 'found') overlaps.push({ patterns, witness: witness.path, inTree: files === null ? null : false });\n else undecided.push(patterns);\n }\n }\n return { overlaps, undecided };\n}\n\n/**\n * A witness as a reader is told it: a file of the tree by its path, and a path\n * the search built with a word that it is an example.\n */\nexport function witnessText(overlap: Overlap): string {\n return overlap.inTree === false ? `${overlap.witness}, an example not in the tree` : overlap.witness;\n}\n\nexport interface Contradiction {\n /** Affected patterns every file of which is protected: nothing of them is writable. */\n readonly covered: readonly string[];\n /** Affected patterns for which the search could not decide within its budget. */\n readonly undecided: readonly string[];\n}\n\n/** The affected patterns a brief's own protections cover entirely. */\nexport function contradictions(scope: Scope, budget: number = WITNESS_BUDGET): Contradiction {\n const covered: string[] = [];\n const undecided: string[] = [];\n const outer = scope.protected.map((p) => p.glob);\n for (const { pattern, glob } of scope.affected) {\n const answer = globCovers(outer, glob, budget);\n if (answer === true) covered.push(pattern);\n else if (answer === 'undecided') undecided.push(pattern);\n }\n return { covered, undecided };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A brief's scope, and whether two briefs can write the same file.\n *\n * What a brief may write is its `affectedFiles` less its `protectedFiles`:\n * protection wins, so \"all of `src/` but the schema\" is written\n * `affectedFiles: [src/**]` with `protectedFiles: [src/db/schema.ts]`. Two\n * briefs meet when some file lies in both writable scopes - in a pattern of\n * each, and in neither brief's protections - and the witness is such a file\n * the tree holds, or else the shortest such path the search builds, as an\n * example (ADR-0005).\n */\n\nimport type { Brief } from './brief.js';\nimport { filesMatching, type Glob, globCovers, globWitness, type LiteralReading, matchGlob, parseGlob, WITNESS_BUDGET } from './glob.js';\n\n/** How a literal path is read, as {@link readingIn} answers for a tree. */\nexport type Reading = (path: string) => LiteralReading;\n\nexport interface ScopePattern {\n readonly pattern: string;\n readonly glob: Glob;\n}\n\nexport interface Scope {\n readonly brief: Brief;\n /** The `affectedFiles` that parse. One that does not is the `glob` rule's finding, and scopes nothing. */\n readonly affected: readonly ScopePattern[];\n readonly protected: readonly ScopePattern[];\n}\n\n/** Patterns that parse, read with one reading. */\nexport function patternsOf(patterns: readonly string[], reading: Reading): ScopePattern[] {\n return patterns.flatMap((pattern) => {\n const parsed = parseGlob(pattern, { literal: reading });\n return parsed.ok ? [{ pattern, glob: parsed.glob }] : [];\n });\n}\n\nexport function scopeOf(brief: Brief, reading: Reading): Scope {\n return { brief, affected: patternsOf(brief.affectedFiles, reading), protected: patternsOf(brief.protectedFiles, reading) };\n}\n\n/** A pattern from each scope, and a file both may write. */\nexport interface Overlap {\n readonly patterns: readonly [string, string];\n readonly witness: string;\n /**\n * `true` when the witness is a file the tree holds; `false` when the tree\n * holds none both patterns match outside the protections, and the witness\n * is an example the search built; `null` when no tree was given.\n */\n readonly inTree: boolean | null;\n}\n\nexport interface Meeting {\n /** Every pair of patterns that meets outside both protections, the first scope's pattern first. */\n readonly overlaps: readonly Overlap[];\n /** Pairs of patterns the search could not decide within its budget, and no file of the tree decides. */\n readonly undecided: readonly (readonly [string, string])[];\n}\n\nconst matched = new WeakMap<readonly string[], WeakMap<Glob, ReadonlySet<string>>>();\n\n/**\n * The files of a tree a pattern matches, in the tree's order. Found once per\n * pattern and tree, which every pair a brief is in shares; kept or not, the\n * answer is the same, and only the time differs.\n */\nfunction matching(files: readonly string[], glob: Glob): ReadonlySet<string> {\n let byGlob = matched.get(files);\n if (byGlob === undefined) {\n byGlob = new WeakMap();\n matched.set(files, byGlob);\n }\n let found = byGlob.get(glob);\n if (found === undefined) {\n found = new Set(filesMatching(glob, files));\n byGlob.set(glob, found);\n }\n return found;\n}\n\n/**\n * The first file of the tree, in its order, that both patterns match and no\n * protection does. The engine's list is sorted, so the file named is the same\n * on every run.\n */\nfunction heldInBoth(files: readonly string[], x: Glob, y: Glob, avoid: readonly Glob[]): string | undefined {\n const other = matching(files, y);\n for (const file of matching(files, x)) {\n if (other.has(file) && !avoid.some((glob) => matchGlob(glob, file))) return file;\n }\n return undefined;\n}\n\n/**\n * Where two writable scopes meet. Each pair of affected patterns is searched\n * for a file both cover that neither brief protects: a file one brief protects\n * is one that brief will not write, so it is no collision.\n *\n * The search names the shortest such path, which the tree need not hold:\n * `src/Shop/OrderService.cs` where the tree has `src/Shop/Orders/OrderService.cs`.\n * So where the search finds a meeting, or cannot decide one, the tree's files\n * are read, and the first both patterns match outside the protections is the\n * witness (ADR-0005, amended 2026-09-30). A file that exists and both briefs\n * may write settles a pair the search could not: it is a collision, proved by\n * the file, not a guess. Where the tree holds none, the search's path stands,\n * as an example. A pair the search proves apart has no such file, and the tree\n * is not read for it.\n */\nexport function meet(a: Scope, b: Scope, budget: number = WITNESS_BUDGET, files: readonly string[] | null = null): Meeting {\n const avoid = [...a.protected, ...b.protected].map((p) => p.glob);\n const overlaps: Overlap[] = [];\n const undecided: (readonly [string, string])[] = [];\n for (const x of a.affected) {\n for (const y of b.affected) {\n const witness = globWitness([x.glob, y.glob], avoid, budget);\n if (witness.kind === 'none') continue;\n const patterns = [x.pattern, y.pattern] as const;\n const held = files === null ? undefined : heldInBoth(files, x.glob, y.glob, avoid);\n if (held !== undefined) overlaps.push({ patterns, witness: held, inTree: true });\n else if (witness.kind === 'found') overlaps.push({ patterns, witness: witness.path, inTree: files === null ? null : false });\n else undecided.push(patterns);\n }\n }\n return { overlaps, undecided };\n}\n\n/**\n * A witness as a reader is told it: a file of the tree by its path, and a path\n * the search built with a word that it is an example.\n */\nexport function witnessText(overlap: Overlap): string {\n return overlap.inTree === false ? `${overlap.witness}, an example not in the tree` : overlap.witness;\n}\n\nexport interface Contradiction {\n /** Affected patterns every file of which is protected: nothing of them is writable. */\n readonly covered: readonly string[];\n /** Affected patterns for which the search could not decide within its budget. */\n readonly undecided: readonly string[];\n}\n\n/** The affected patterns a brief's own protections cover entirely. */\nexport function contradictions(scope: Scope, budget: number = WITNESS_BUDGET): Contradiction {\n const covered: string[] = [];\n const undecided: string[] = [];\n const outer = scope.protected.map((p) => p.glob);\n for (const { pattern, glob } of scope.affected) {\n const answer = globCovers(outer, glob, budget);\n if (answer === true) covered.push(pattern);\n else if (answer === 'undecided') undecided.push(pattern);\n }\n return { covered, undecided };\n}\n"]}
|
package/dist/trigger.d.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The lists are deliberately short. A trigger this reads as an event when it
|
|
13
13
|
* is a time is a miss, and a miss costs less here than refusing a real event
|
|
14
|
-
* for looking like a date.
|
|
14
|
+
* for looking like a date. They are English, as the status words are: a time
|
|
15
|
+
* written in another language is a word on no list, so it reads as an event.
|
|
15
16
|
*/
|
|
16
17
|
/** Whether a trigger has a word that says what must happen, rather than only when. */
|
|
17
18
|
export declare function namesAnEvent(trigger: string): boolean;
|
package/dist/trigger.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The lists are deliberately short. A trigger this reads as an event when it
|
|
13
13
|
* is a time is a miss, and a miss costs less here than refusing a real event
|
|
14
|
-
* for looking like a date.
|
|
14
|
+
* for looking like a date. They are English, as the status words are: a time
|
|
15
|
+
* written in another language is a word on no list, so it reads as an event.
|
|
15
16
|
*/
|
|
16
17
|
const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];
|
|
17
18
|
const WEEKDAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
|
@@ -38,68 +39,12 @@ const WHEN = new Set([
|
|
|
38
39
|
* its numbers: `2026-10-01` and `10/01/2026` are read a number at a time.
|
|
39
40
|
*/
|
|
40
41
|
const DATE = /^(?:q[1-4]|\d+(?:st|nd|rd|th)?)$/;
|
|
41
|
-
/**
|
|
42
|
-
* Chinese is written without spaces, so a trigger in it is one run of Han
|
|
43
|
-
* characters that says when or what: `下個月` (next month), `2026年10月`,
|
|
44
|
-
* `月結完成後` (after the month-end close). A run says only when if taking out
|
|
45
|
-
* the words and characters below leaves nothing, Traditional and Simplified
|
|
46
|
-
* alike.
|
|
47
|
-
*
|
|
48
|
-
* The words come out first, since one may hold a character that alone says
|
|
49
|
-
* nothing about time: `稍後` (later) is `稍` and `後`, and `稍` is not on the
|
|
50
|
-
* list. A word whose every character is on the list, such as `之後`, needs no
|
|
51
|
-
* entry of its own.
|
|
52
|
-
*/
|
|
53
|
-
const HAN_WHEN_WORDS = [
|
|
54
|
-
// A weekday, and the stretches: `星期一`, `禮拜三`, a quarter, a year, an hour, a minute.
|
|
55
|
-
...['星期', '禮拜', '礼拜', '季度', '年度', '小時', '小时', '分鐘', '分钟'],
|
|
56
|
-
// Soon, later, one day, in the future, about.
|
|
57
|
-
...['近期', '稍後', '稍后', '以後', '以后', '將來', '将来', '未來', '未来', '改天', '馬上', '马上', '很快', '不久', '左右'],
|
|
58
|
-
];
|
|
59
|
-
/**
|
|
60
|
-
* Characters that say when, and the ones that join them. `時`, `期` and `每`
|
|
61
|
-
* are not among them: `到期時` (when it expires) and `每月結算完成時` are
|
|
62
|
-
* events, and so is a trigger that starts with `當` (when).
|
|
63
|
-
*/
|
|
64
|
-
const HAN_WHEN_CHARACTERS = new Set([
|
|
65
|
-
// Units: year, month, day, week, quarter, a ten-day stretch, the day of the month, noon.
|
|
66
|
-
...['年', '月', '日', '天', '週', '周', '季', '旬', '號', '号', '午'],
|
|
67
|
-
// Where in one: its start, end, middle, half.
|
|
68
|
-
...['初', '底', '末', '中', '半'],
|
|
69
|
-
// Which one: this, next, last, the one before or after, early, late, the nth.
|
|
70
|
-
...['今', '明', '去', '上', '下', '本', '這', '这', '次', '前', '後', '后', '早', '晚', '第', '個', '个'],
|
|
71
|
-
// How many.
|
|
72
|
-
...['〇', '零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '百', '兩', '两', '幾', '几'],
|
|
73
|
-
// The words that join them: at, in, until, of.
|
|
74
|
-
...['在', '於', '于', '到', '的', '之'],
|
|
75
|
-
]);
|
|
76
|
-
/** A run of Han characters, or of other letters and digits. */
|
|
77
|
-
const WORD = /\p{Script=Han}+|(?:(?!\p{Script=Han})[\p{L}\p{N}])+/gu;
|
|
78
|
-
/** A word is all Han or none of it, so one character says which. */
|
|
79
|
-
const HAN = /\p{Script=Han}/u;
|
|
80
|
-
/**
|
|
81
|
-
* The words of a trigger, in lower case: runs of letters and digits, with Han
|
|
82
|
-
* characters in runs of their own, so that `10月` is `10` and `月`. `mid-2027`
|
|
83
|
-
* is two words, and punctuation none. Read in NFKC first, so that full-width
|
|
84
|
-
* `Q3` is `q3` and `2026` is `2026`.
|
|
85
|
-
*/
|
|
42
|
+
/** The runs of letters and digits, in lower case: `mid-2027` is two words, and punctuation none. */
|
|
86
43
|
function words(text) {
|
|
87
|
-
return text.
|
|
88
|
-
}
|
|
89
|
-
/** Whether a run of Han characters says only when. */
|
|
90
|
-
function hanSaysWhen(run) {
|
|
91
|
-
let rest = run;
|
|
92
|
-
for (const word of HAN_WHEN_WORDS)
|
|
93
|
-
rest = rest.replaceAll(word, '');
|
|
94
|
-
return [...rest].every((character) => HAN_WHEN_CHARACTERS.has(character));
|
|
95
|
-
}
|
|
96
|
-
function saysWhen(word) {
|
|
97
|
-
if (HAN.test(word))
|
|
98
|
-
return hanSaysWhen(word);
|
|
99
|
-
return WHEN.has(word) || DATE.test(word);
|
|
44
|
+
return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
100
45
|
}
|
|
101
46
|
/** Whether a trigger has a word that says what must happen, rather than only when. */
|
|
102
47
|
export function namesAnEvent(trigger) {
|
|
103
|
-
return words(trigger).some((word) => !
|
|
48
|
+
return words(trigger).some((word) => !WHEN.has(word) && !DATE.test(word));
|
|
104
49
|
}
|
|
105
50
|
//# sourceMappingURL=trigger.js.map
|
package/dist/trigger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trigger.js","sourceRoot":"","sources":["../src/trigger.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"trigger.js","sourceRoot":"","sources":["../src/trigger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AAC1I,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;AAEhG,4CAA4C;AAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC;IACnB,GAAG,MAAM;IACT,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACnC,MAAM;IACN,GAAG,QAAQ;IACX,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACrC,qBAAqB;IACrB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC;IACzG,OAAO;IACP,UAAU;IACV,MAAM;IACN,OAAO;IACP,oBAAoB;IACpB,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC;IAC5H,4BAA4B;IAC5B,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC;CACrI,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,IAAI,GAAG,kCAAkC,CAAC;AAEhD,oGAAoG;AACpG,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;AAC3D,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * Whether a deferral's trigger names an event or only a time.\n *\n * Deferred work comes back when something happens: \"when the second tenant\n * signs\", \"when p95 exceeds 200 ms\". A date is not that. It passes whether or\n * not the reason for the work has arrived, and a deferral that fires on a date\n * is a reminder to look again, which the deferral itself already is. So a\n * trigger made only of words that say when - a date, a quarter, a month or a\n * weekday, a stretch of time and the small words that join them - is refused,\n * and one word that says what is enough to pass.\n *\n * The lists are deliberately short. A trigger this reads as an event when it\n * is a time is a miss, and a miss costs less here than refusing a real event\n * for looking like a date. They are English, as the status words are: a time\n * written in another language is a word on no list, so it reads as an event.\n */\n\nconst MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];\nconst WEEKDAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];\n\n/** Words that say when and nothing else. */\nconst WHEN = new Set([\n ...MONTHS,\n ...MONTHS.map((m) => m.slice(0, 3)),\n 'sept',\n ...WEEKDAYS,\n ...WEEKDAYS.map((d) => d.slice(0, 3)),\n // A stretch of time.\n ...['day', 'week', 'weekend', 'month', 'quarter', 'year', 'sprint'].flatMap((unit) => [unit, `${unit}s`]),\n 'today',\n 'tomorrow',\n 'soon',\n 'later',\n // How many of them.\n ...['a', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 'few', 'couple'],\n // The words that join them.\n ...['in', 'on', 'by', 'at', 'after', 'before', 'until', 'from', 'of', 'the', 'next', 'this', 'end', 'start', 'early', 'mid', 'late'],\n]);\n\n/**\n * A number or an ordinal - `2026`, `15`, `15th` - or a quarter, `Q3`. A date is\n * its numbers: `2026-10-01` and `10/01/2026` are read a number at a time.\n */\nconst DATE = /^(?:q[1-4]|\\d+(?:st|nd|rd|th)?)$/;\n\n/** The runs of letters and digits, in lower case: `mid-2027` is two words, and punctuation none. */\nfunction words(text: string): readonly string[] {\n return text.toLowerCase().match(/[\\p{L}\\p{N}]+/gu) ?? [];\n}\n\n/** Whether a trigger has a word that says what must happen, rather than only when. */\nexport function namesAnEvent(trigger: string): boolean {\n return words(trigger).some((word) => !WHEN.has(word) && !DATE.test(word));\n}\n"]}
|
package/package.json
CHANGED