slopless 0.2.23 → 0.2.25

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 (32) hide show
  1. package/dist/rules/semantic-thinness/patterns/abstract-metaphor-claim.json +83 -1
  2. package/dist/rules/semantic-thinness/patterns/deictic-summary.json +31 -2
  3. package/dist/rules/semantic-thinness/patterns/empty-quantification.json +33 -0
  4. package/dist/rules/semantic-thinness/patterns/empty-scene-transition.json +2 -1
  5. package/dist/rules/semantic-thinness/private/concrete-guards.js +2 -0
  6. package/dist/rules/semantic-thinness/private/pattern-data-e.js +2 -0
  7. package/dist/rules/semantic-thinness/private/pattern-matcher.js +28 -7
  8. package/dist/rules/syntactic-patterns/closers/affirmation-closers.js +13 -3
  9. package/dist/rules/syntactic-patterns/contrast/contrastive-aphorism.js +2 -0
  10. package/dist/rules/syntactic-patterns/contrast/negation-reframe.js +1 -1
  11. package/dist/rules/syntactic-patterns/contrast/private/action-reframe-vocabulary.js +47 -0
  12. package/dist/rules/syntactic-patterns/contrast/private/audience-replacement.js +267 -0
  13. package/dist/rules/syntactic-patterns/contrast/private/copular-reframe.js +79 -0
  14. package/dist/rules/syntactic-patterns/contrast/private/evidence-limitation-pair.js +307 -0
  15. package/dist/rules/syntactic-patterns/contrast/private/inline-semicolon-reframe.js +50 -0
  16. package/dist/rules/syntactic-patterns/contrast/private/inline-short-negation.js +1 -0
  17. package/dist/rules/syntactic-patterns/contrast/private/negation-reframe-matcher.js +78 -73
  18. package/dist/rules/syntactic-patterns/contrast/private/negation-reframe-parts.js +99 -31
  19. package/dist/rules/syntactic-patterns/contrast/private/negation-vocabulary.js +27 -0
  20. package/dist/rules/syntactic-patterns/contrast/private/negative-consequence.js +301 -0
  21. package/dist/rules/syntactic-patterns/contrast/private/negative-slop-frames.js +155 -39
  22. package/dist/rules/syntactic-patterns/contrast/private/policy-object.js +55 -0
  23. package/dist/rules/syntactic-patterns/contrast/private/reframe-classification.js +214 -0
  24. package/dist/rules/syntactic-patterns/contrast/private/sequence-reframes.js +9 -0
  25. package/dist/rules/syntactic-patterns/contrast/private/temporal-reframe.js +318 -0
  26. package/dist/rules/syntactic-patterns/lead-ins/generic-signposting.js +96 -102
  27. package/dist/rules/syntactic-patterns/lead-ins/private/component-assignment-frame.js +239 -0
  28. package/dist/rules/syntactic-patterns/lead-ins/private/discourse-evaluation.js +150 -4
  29. package/dist/rules/syntactic-patterns/lead-ins/private/evaluative-colon-frame.js +166 -0
  30. package/dist/rules/syntactic-patterns/lead-ins/private/reaction-frame.js +29 -0
  31. package/dist/rules/syntactic-patterns/lead-ins/private/relative-discourse-frame.js +119 -0
  32. package/package.json +1 -1
@@ -1,7 +1,12 @@
1
+ import { defineTextlintRule } from "../../../adapters/textlint/rule.js";
2
+ import { paragraphUnits, sentenceUnits } from "../../../adapters/textlint/units.js";
1
3
  import { hasConcreteImplementationSummary } from "../../../shared/matchers/concrete-evidence.js";
2
4
  import { cleanSentence, containsAny, tokens } from "../../../shared/matchers/prose-patterns.js";
3
- import { oneToOneRule } from "../../private/textlint-rule-builders.js";
4
- import { matchDiscourseEvaluationFrame } from "./private/discourse-evaluation.js";
5
+ import { splitSentences } from "../../../shared/text/sentences.js";
6
+ import { matchComponentAssignmentFrame } from "./private/component-assignment-frame.js";
7
+ import { isAbstractAuditFrame, matchReactionFrame, matchDiscourseEvaluationFrame, matchExpandedDiscourseFrame } from "./private/discourse-evaluation.js";
8
+ import { matchEvaluativeColonFrame } from "./private/evaluative-colon-frame.js";
9
+ import { matchRelativeDiscourseFrame } from "./private/relative-discourse-frame.js";
5
10
  const PREFIXES = ["however, ", "but ", "and ", "so "];
6
11
  // "as such" was removed: it is a normal anaphoric connective ("a registered adviser; as
7
12
  // such, it must...") and was the dominant false positive, not a signposting frame.
@@ -47,7 +52,6 @@ const FRAME_PATTERNS = [
47
52
  "the useful version is",
48
53
  "the point is plain enough"
49
54
  ];
50
- const DETERMINERS = ["a", "an", "the", "this", "that"];
51
55
  const SEQUENCE_PATTERNS = [
52
56
  "a simple sequence works well",
53
57
  "a simple pattern works well",
@@ -88,72 +92,22 @@ const WHAT_FRAME_TAIL_STARTERS = [
88
92
  "true",
89
93
  "usually"
90
94
  ];
91
- const FRAME_ADJECTIVES = [
92
- "basic",
93
- "best",
94
- "better",
95
- "biggest",
96
- "bigger",
97
- "central",
98
- "clearest",
99
- "core",
100
- "easiest",
101
- "final",
102
- "first",
103
- "hardest",
104
- "honest",
105
- "important",
106
- "main",
107
- "only",
108
- "obvious",
109
- "practical",
110
- "real",
111
- "simple",
112
- "useful"
113
- ];
114
- const FRAME_NOUNS = [
115
- "answer",
116
- "approach",
117
- "challenge",
118
- "choice",
119
- "conclusion",
120
- "fact",
121
- "fix",
122
- "focus",
123
- "frame",
124
- "idea",
125
- "lesson",
126
- "move",
127
- "path",
128
- "point",
129
- "principle",
130
- "priority",
131
- "problem",
132
- "question",
133
- "result",
134
- "rule",
135
- "shift",
136
- "signal",
137
- "strategy",
138
- "test",
139
- "thing",
140
- "tradeoff",
141
- "truth",
142
- "version",
143
- "way",
144
- "win"
95
+ const WHAT_MATTERS_CLAUSE_STARTERS = [
96
+ "how",
97
+ "if",
98
+ "that",
99
+ "what",
100
+ "when",
101
+ "where",
102
+ "whether",
103
+ "which",
104
+ "who",
105
+ "whose",
106
+ "why"
145
107
  ];
146
- const ABSTRACT_FRAME_VERBS = ["is", "are", "was"];
147
108
  const POINT_NOUNS = ["goal", "job", "key", "point", "takeaway", "trick"];
148
109
  function matchModifiedAbstractFrame(words) {
149
- const [first, second, third, fourth, fifth] = words;
150
- if (first === "the" &&
151
- second === "result" &&
152
- third === "worth" &&
153
- fourth === "caring" &&
154
- fifth === "about") {
155
- return "the-result-worth-caring-about";
156
- }
110
+ const [first, second, third, fourth] = words;
157
111
  if (first !== "the") {
158
112
  return undefined;
159
113
  }
@@ -167,20 +121,6 @@ function matchModifiedAbstractFrame(words) {
167
121
  ]);
168
122
  return matches.get(key);
169
123
  }
170
- function matchEvaluativeFrame(words) {
171
- const [first, second, third, fourth] = words;
172
- if (first !== undefined &&
173
- second !== undefined &&
174
- third !== undefined &&
175
- fourth !== undefined &&
176
- DETERMINERS.includes(first) &&
177
- FRAME_ADJECTIVES.includes(second) &&
178
- FRAME_NOUNS.includes(third) &&
179
- ABSTRACT_FRAME_VERBS.includes(fourth)) {
180
- return `the-${second}-${third}-${fourth}`;
181
- }
182
- return undefined;
183
- }
184
124
  function matchPointIsToFrame(words) {
185
125
  const [first, second, third, fourth, fifth] = words;
186
126
  if (first === "the" &&
@@ -201,7 +141,8 @@ function matchWhatFrame(words) {
201
141
  second === "changes") &&
202
142
  third === "is" &&
203
143
  fourth !== undefined &&
204
- WHAT_FRAME_TAIL_STARTERS.includes(fourth)) {
144
+ (WHAT_FRAME_TAIL_STARTERS.includes(fourth) ||
145
+ (second === "matters" && !WHAT_MATTERS_CLAUSE_STARTERS.includes(fourth)))) {
205
146
  return `what-${second}-is`;
206
147
  }
207
148
  if (first === "what" && second === "matters" && third === "most") {
@@ -217,11 +158,13 @@ function matchWhatFrame(words) {
217
158
  }
218
159
  function matchAbstractFrame(text) {
219
160
  const words = tokens(text);
220
- return (matchModifiedAbstractFrame(words) ??
221
- matchEvaluativeFrame(words) ??
161
+ return (matchRelativeDiscourseFrame(text, words) ??
162
+ matchExpandedDiscourseFrame(words) ??
163
+ matchModifiedAbstractFrame(words) ??
222
164
  matchDiscourseEvaluationFrame(words) ??
223
165
  matchPointIsToFrame(words) ??
224
- matchWhatFrame(words));
166
+ matchWhatFrame(words) ??
167
+ matchReactionFrame(words));
225
168
  }
226
169
  function matchFormulaicContentSetup(text) {
227
170
  if (text.startsWith("each ") &&
@@ -266,11 +209,20 @@ function matchGeneratedFormula(text) {
266
209
  }
267
210
  function matchSignposting(sentence) {
268
211
  const stripped = cleanSentence(sentence, PREFIXES);
269
- const concreteImplementation = hasConcreteImplementationSummary(stripped);
270
212
  const abstract = matchAbstractFrame(stripped);
213
+ const strippedWords = tokens(stripped);
214
+ const auditStart = isAbstractAuditFrame(strippedWords)
215
+ ? stripped.indexOf("audit")
216
+ : -1;
217
+ const concreteImplementation = hasConcreteImplementationSummary(abstract !== undefined && auditStart >= 0
218
+ ? stripped.slice(0, auditStart) + stripped.slice(auditStart + 5)
219
+ : stripped);
271
220
  const formulaicSetup = matchFormulaicContentSetup(stripped);
272
221
  const generatedFormula = matchGeneratedFormula(stripped);
273
- if (abstract !== undefined && !concreteImplementation) {
222
+ if (abstract !== undefined &&
223
+ (abstract === "what-matters-is" ||
224
+ abstract.startsWith("relative-") ||
225
+ !concreteImplementation)) {
274
226
  return { kind: "abstract-evaluation-frame", signal: abstract };
275
227
  }
276
228
  if (generatedFormula !== undefined && !concreteImplementation) {
@@ -279,6 +231,9 @@ function matchSignposting(sentence) {
279
231
  if (formulaicSetup !== undefined) {
280
232
  return { kind: "formulaic-content-setup", signal: formulaicSetup };
281
233
  }
234
+ if (stripped.startsWith("the fun part is:") && !concreteImplementation) {
235
+ return { kind: "frame-signpost", signal: "the fun part is:" };
236
+ }
282
237
  const checks = [
283
238
  ["transition", TRANSITION_PATTERNS],
284
239
  ["consultation-signpost", CONSULTATION_PATTERNS],
@@ -302,24 +257,63 @@ function matchSignposting(sentence) {
302
257
  }
303
258
  return undefined;
304
259
  }
305
- const rule = oneToOneRule({
306
- detect: (unit) => {
307
- const matched = matchSignposting(unit.text);
308
- if (matched === undefined) {
309
- return [];
310
- }
311
- return [
312
- {
313
- evidence: matched.signal,
314
- label: matched.kind,
315
- range: { start: 0, end: unit.text.length }
316
- }
317
- ];
260
+ const rule = defineTextlintRule({
261
+ detector: {
262
+ detect: ({ units }) => {
263
+ const detections = units.flatMap((unit) => {
264
+ if (unit.kind === "paragraph") {
265
+ const sentences = splitSentences(unit.text);
266
+ const pairDetections = [];
267
+ for (let index = 0; index < sentences.length - 1; index += 1) {
268
+ const current = sentences[index];
269
+ const next = sentences[index + 1];
270
+ if (current === undefined || next === undefined) {
271
+ continue;
272
+ }
273
+ const signal = matchComponentAssignmentFrame(current.text, next.text);
274
+ if (signal !== undefined) {
275
+ pairDetections.push({
276
+ evidence: signal,
277
+ label: "component-assignment-frame",
278
+ range: { start: current.start, end: next.end },
279
+ ruleId: "syntactic-patterns:generic-signposting",
280
+ unitId: unit.id
281
+ });
282
+ }
283
+ }
284
+ return pairDetections;
285
+ }
286
+ const componentSignal = matchComponentAssignmentFrame(unit.text);
287
+ const colonSignal = matchEvaluativeColonFrame(unit.text);
288
+ const matched = componentSignal === undefined && colonSignal === undefined
289
+ ? matchSignposting(unit.text)
290
+ : undefined;
291
+ const signal = componentSignal ?? colonSignal ?? matched?.signal;
292
+ if (signal === undefined) {
293
+ return [];
294
+ }
295
+ return [
296
+ {
297
+ evidence: signal,
298
+ label: componentSignal !== undefined
299
+ ? "component-assignment-frame"
300
+ : colonSignal !== undefined
301
+ ? "evaluative-colon-frame"
302
+ : (matched?.kind ?? "generic-signposting"),
303
+ range: { start: 0, end: unit.text.length },
304
+ ruleId: "syntactic-patterns:generic-signposting",
305
+ unitId: unit.id
306
+ }
307
+ ];
308
+ });
309
+ return detections;
310
+ },
311
+ family: "syntactic-patterns",
312
+ id: "syntactic-patterns:generic-signposting"
318
313
  },
319
- family: "syntactic-patterns",
320
314
  formatMessage: (report) => `Generic signposting found: ${report.evidence}. Replace the frame with the concrete claim.`,
321
- ruleId: "syntactic-patterns:generic-signposting",
322
- unitKind: "sentence"
315
+ reportPolicy: { kind: "one-to-one" },
316
+ units: (document) => [...sentenceUnits(document), ...paragraphUnits(document)]
323
317
  });
324
318
  export default rule;
325
319
  //# sourceMappingURL=generic-signposting.js.map
@@ -0,0 +1,239 @@
1
+ import { tokens } from "../../../../shared/matchers/prose-patterns.js";
2
+ import { normalizeForMatch } from "../../../../shared/text/normalize.js";
3
+ const COPULAS = new Set(["are", "is", "was", "were"]);
4
+ const DETERMINERS = new Set(["a", "an", "the"]);
5
+ const ACTOR_ROLES = new Set([
6
+ "agent",
7
+ "analyst",
8
+ "author",
9
+ "creator",
10
+ "editor",
11
+ "model",
12
+ "producer",
13
+ "researcher",
14
+ "writer"
15
+ ]);
16
+ const WRITING_ROLES = new Set([
17
+ "author",
18
+ "creator",
19
+ "editor",
20
+ "producer",
21
+ "writer"
22
+ ]);
23
+ const COMPONENT_HEADS = new Set([
24
+ "basis",
25
+ "context",
26
+ "goal",
27
+ "input",
28
+ "material",
29
+ "output",
30
+ "source"
31
+ ]);
32
+ const WRITING_MATERIALS = new Set([
33
+ "article",
34
+ "brief",
35
+ "content",
36
+ "copy",
37
+ "draft",
38
+ "evidence",
39
+ "findings",
40
+ "interviews",
41
+ "notes",
42
+ "recommendation",
43
+ "report",
44
+ "research",
45
+ "transcript"
46
+ ]);
47
+ const EXPLANATION_WORDS = new Set([
48
+ "although",
49
+ "because",
50
+ "if",
51
+ "since",
52
+ "so",
53
+ "therefore",
54
+ "unless",
55
+ "when",
56
+ "where",
57
+ "which",
58
+ "while",
59
+ "who"
60
+ ]);
61
+ const DETAIL_PREPOSITIONS = new Set([
62
+ "after",
63
+ "at",
64
+ "before",
65
+ "by",
66
+ "from",
67
+ "in",
68
+ "into",
69
+ "of",
70
+ "on",
71
+ "through",
72
+ "to",
73
+ "under",
74
+ "with"
75
+ ]);
76
+ const TECHNICAL_LABELS = new Set([
77
+ "array",
78
+ "bytes",
79
+ "classifier",
80
+ "csv",
81
+ "event",
82
+ "file",
83
+ "image",
84
+ "interface",
85
+ "json",
86
+ "object",
87
+ "request",
88
+ "response",
89
+ "schema",
90
+ "service",
91
+ "string",
92
+ "tensor",
93
+ "vector"
94
+ ]);
95
+ function withoutDeterminer(words) {
96
+ return DETERMINERS.has(words[0] ?? "") ? words.slice(1) : words;
97
+ }
98
+ function trimTerminalPunctuation(text) {
99
+ let end = text.length;
100
+ while (end > 0 && [".", "!", "?"].includes(text[end - 1] ?? "")) {
101
+ end -= 1;
102
+ }
103
+ return text.slice(0, end);
104
+ }
105
+ function hasTechnicalSyntax(text) {
106
+ const blockedCharacters = new Set([
107
+ "`",
108
+ "(",
109
+ ")",
110
+ "[",
111
+ "]",
112
+ "{",
113
+ "}",
114
+ "=",
115
+ "<",
116
+ ">",
117
+ "/",
118
+ "\\"
119
+ ]);
120
+ for (const character of text) {
121
+ if (blockedCharacters.has(character) ||
122
+ (character >= "0" && character <= "9")) {
123
+ return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ function parseNormalizedAssignment(normalized) {
129
+ if (normalized.length === 0 || hasTechnicalSyntax(normalized)) {
130
+ return undefined;
131
+ }
132
+ const words = tokens(normalized);
133
+ const copulaIndex = words.findIndex((word) => COPULAS.has(word));
134
+ if (copulaIndex < 1 ||
135
+ copulaIndex > 3 ||
136
+ words.length - copulaIndex - 1 < 1 ||
137
+ words.length - copulaIndex - 1 > 4) {
138
+ return undefined;
139
+ }
140
+ const subject = withoutDeterminer(words.slice(0, copulaIndex));
141
+ const complement = withoutDeterminer(words.slice(copulaIndex + 1));
142
+ const blocked = [...subject, ...complement].some((word) => EXPLANATION_WORDS.has(word) || DETAIL_PREPOSITIONS.has(word));
143
+ return subject.length === 0 || complement.length === 0 || blocked
144
+ ? undefined
145
+ : { complement, subject };
146
+ }
147
+ function assignmentClauseCandidates(clause) {
148
+ const normalized = trimTerminalPunctuation(normalizeForMatch(clause));
149
+ const candidates = [normalized];
150
+ const delimiterIndexes = [
151
+ normalized.indexOf(","),
152
+ normalized.indexOf(";"),
153
+ normalized.indexOf(" - "),
154
+ normalized.indexOf(" \u2013 "),
155
+ normalized.indexOf(" \u2014 ")
156
+ ].filter((index) => index > 0);
157
+ const firstDelimiter = Math.min(...delimiterIndexes);
158
+ if (Number.isFinite(firstDelimiter)) {
159
+ candidates.push(normalized.slice(0, firstDelimiter).trim());
160
+ }
161
+ return candidates;
162
+ }
163
+ function parseAssignment(clause) {
164
+ for (const candidate of assignmentClauseCandidates(clause)) {
165
+ const assignment = parseNormalizedAssignment(candidate);
166
+ if (assignment !== undefined) {
167
+ return assignment;
168
+ }
169
+ }
170
+ return undefined;
171
+ }
172
+ function isActorAssignment(assignment) {
173
+ const subjectHead = assignment.subject.at(-1) ?? "";
174
+ const complementHead = assignment.complement.at(-1) ?? "";
175
+ return ACTOR_ROLES.has(subjectHead) && ACTOR_ROLES.has(complementHead);
176
+ }
177
+ function isComponentAssignment(assignment) {
178
+ const subjectHead = assignment.subject.at(-1) ?? "";
179
+ return COMPONENT_HEADS.has(subjectHead);
180
+ }
181
+ function hasWritingContext(actor, component) {
182
+ const actorWords = [...actor.subject, ...actor.complement];
183
+ const componentHead = component.complement.at(-1) ?? "";
184
+ return (actorWords.some((word) => WRITING_ROLES.has(word)) ||
185
+ WRITING_MATERIALS.has(componentHead));
186
+ }
187
+ function isTechnicalInterface(actor, component) {
188
+ return [...actor.complement, ...component.complement].some((word) => TECHNICAL_LABELS.has(word));
189
+ }
190
+ function splitClauses(text) {
191
+ const normalized = normalizeForMatch(text);
192
+ const separators = [];
193
+ for (let index = 0; index < normalized.length; index += 1) {
194
+ const character = normalized[index] ?? "";
195
+ const isPunctuationSeparator = character === "," || character === ";";
196
+ const isSpacedDash = ["-", "\u2013", "\u2014"].includes(character) &&
197
+ (normalized[index - 1] ?? "").trim() === "" &&
198
+ (normalized[index + 1] ?? "").trim() === "";
199
+ if (isPunctuationSeparator || isSpacedDash) {
200
+ separators.push(index);
201
+ }
202
+ }
203
+ if (separators.length !== 1) {
204
+ return undefined;
205
+ }
206
+ const separator = separators[0] ?? -1;
207
+ const first = normalized.slice(0, separator).trim();
208
+ const second = normalized.slice(separator + 1).trim();
209
+ return first.length > 0 && second.length > 0 ? [first, second] : undefined;
210
+ }
211
+ export function matchComponentAssignmentFrame(first, second) {
212
+ const pair = second === undefined ? splitClauses(first) : [first, second];
213
+ if (pair === undefined) {
214
+ return undefined;
215
+ }
216
+ const firstAssignment = parseAssignment(pair[0]);
217
+ const secondAssignment = parseAssignment(pair[1]);
218
+ if (firstAssignment === undefined || secondAssignment === undefined) {
219
+ return undefined;
220
+ }
221
+ const actor = isActorAssignment(firstAssignment)
222
+ ? firstAssignment
223
+ : isActorAssignment(secondAssignment)
224
+ ? secondAssignment
225
+ : undefined;
226
+ const component = isComponentAssignment(firstAssignment)
227
+ ? firstAssignment
228
+ : isComponentAssignment(secondAssignment)
229
+ ? secondAssignment
230
+ : undefined;
231
+ return actor !== undefined &&
232
+ component !== undefined &&
233
+ actor !== component &&
234
+ hasWritingContext(actor, component) &&
235
+ !isTechnicalInterface(actor, component)
236
+ ? "component-assignment-frame"
237
+ : undefined;
238
+ }
239
+ //# sourceMappingURL=component-assignment-frame.js.map
@@ -1,4 +1,75 @@
1
+ export { matchReactionFrame } from "./reaction-frame.js";
1
2
  const ABSTRACT_FRAME_VERBS = ["is", "are", "was", "were"];
3
+ const DISCOURSE_WORK_TAILS = [
4
+ ["doing", "real", "work"],
5
+ ["load", "bearing"]
6
+ ];
7
+ const DETERMINERS = new Set(["a", "an", "the", "this", "that"]);
8
+ const FRAME_ADJECTIVES = new Set([
9
+ "basic",
10
+ "best",
11
+ "better",
12
+ "biggest",
13
+ "bigger",
14
+ "central",
15
+ "clearest",
16
+ "core",
17
+ "easiest",
18
+ "final",
19
+ "first",
20
+ "hardest",
21
+ "honest",
22
+ "important",
23
+ "main",
24
+ "only",
25
+ "obvious",
26
+ "practical",
27
+ "real",
28
+ "simple",
29
+ "useful"
30
+ ]);
31
+ const FRAME_NOUNS = new Set([
32
+ "answer",
33
+ "approach",
34
+ "audit",
35
+ "challenge",
36
+ "choice",
37
+ "conclusion",
38
+ "diagnosis",
39
+ "fact",
40
+ "fix",
41
+ "focus",
42
+ "frame",
43
+ "idea",
44
+ "lesson",
45
+ "move",
46
+ "path",
47
+ "point",
48
+ "principle",
49
+ "priority",
50
+ "problem",
51
+ "question",
52
+ "result",
53
+ "rule",
54
+ "shift",
55
+ "signal",
56
+ "strategy",
57
+ "test",
58
+ "thing",
59
+ "tradeoff",
60
+ "truth",
61
+ "version",
62
+ "way",
63
+ "win"
64
+ ]);
65
+ const VAGUE_FRAME_VERBS = new Set(["begins", "happens", "lives", "starts"]);
66
+ const VAGUE_FRAME_LOCATIONS = new Set([
67
+ "downstream",
68
+ "earlier",
69
+ "here",
70
+ "there",
71
+ "upstream"
72
+ ]);
2
73
  const EVALUATION_TAILS = new Set([
3
74
  "boring",
4
75
  "clear",
@@ -72,15 +143,90 @@ function isDiscourseEvaluationSubject(first, subject) {
72
143
  }
73
144
  return subject.some((word) => DISCOURSE_SUBJECT_HEADS.has(word));
74
145
  }
75
- export function matchDiscourseEvaluationFrame(words) {
76
- const [first] = words;
77
- if (words.length > 8 ||
78
- !["the", "this", "that", "it"].includes(first ?? "")) {
146
+ function matchDiscourseWorkClaim(words, verbIndex, subject) {
147
+ if (verbIndex <= 0 ||
148
+ words[verbIndex] !== "is" ||
149
+ !DISCOURSE_SUBJECT_HEADS.has(subject.at(-1) ?? "")) {
150
+ return undefined;
151
+ }
152
+ const tail = DISCOURSE_WORK_TAILS.find((candidate) => candidate.every((word, index) => words[verbIndex + index + 1] === word));
153
+ return tail === undefined ? undefined : `is-${tail.join("-")}`;
154
+ }
155
+ function frameNounIndex(words) {
156
+ if (FRAME_NOUNS.has(words[1] ?? "")) {
157
+ return 1;
158
+ }
159
+ return FRAME_ADJECTIVES.has(words[1] ?? "") && FRAME_NOUNS.has(words[2] ?? "")
160
+ ? 2
161
+ : -1;
162
+ }
163
+ function matchWorthAttentionFrame(words) {
164
+ if (words[0] !== "the") {
79
165
  return undefined;
80
166
  }
167
+ const nounIndex = frameNounIndex(words);
168
+ if (nounIndex < 0 || words[nounIndex + 1] !== "worth") {
169
+ return undefined;
170
+ }
171
+ const tail = words.slice(nounIndex + 2);
172
+ const matches = tail[0] === "noticing" ||
173
+ tail[0] === "watching" ||
174
+ tail[0] === "tracking" ||
175
+ (tail[0] === "caring" && tail[1] === "about") ||
176
+ (tail[0] === "paying" && tail[1] === "attention" && tail[2] === "to");
177
+ return matches
178
+ ? `the-${words.slice(1, nounIndex + 1).join("-")}-worth-attention`
179
+ : undefined;
180
+ }
181
+ function matchVagueFrameLocation(words) {
182
+ const [first, adjective, noun, verb, location] = words;
183
+ return words.length === 5 &&
184
+ first === "the" &&
185
+ FRAME_ADJECTIVES.has(adjective ?? "") &&
186
+ FRAME_NOUNS.has(noun ?? "") &&
187
+ VAGUE_FRAME_VERBS.has(verb ?? "") &&
188
+ VAGUE_FRAME_LOCATIONS.has(location ?? "")
189
+ ? `the-${adjective ?? "useful"}-${noun ?? "frame"}-${verb ?? "starts"}-${location ?? "here"}`
190
+ : undefined;
191
+ }
192
+ function matchEvaluativeFrame(words) {
193
+ const [first, adjective, noun, verb] = words;
194
+ return first !== undefined &&
195
+ adjective !== undefined &&
196
+ noun !== undefined &&
197
+ verb !== undefined &&
198
+ DETERMINERS.has(first) &&
199
+ FRAME_ADJECTIVES.has(adjective) &&
200
+ FRAME_NOUNS.has(noun) &&
201
+ ["is", "are", "was"].includes(verb)
202
+ ? `the-${adjective}-${noun}-${verb}`
203
+ : undefined;
204
+ }
205
+ export function isAbstractAuditFrame(words) {
206
+ return words[0] === "the" && frameNounIndex(words) > 0
207
+ ? words[frameNounIndex(words)] === "audit"
208
+ : false;
209
+ }
210
+ export function matchExpandedDiscourseFrame(words) {
211
+ return (matchWorthAttentionFrame(words) ??
212
+ matchVagueFrameLocation(words) ??
213
+ matchEvaluativeFrame(words));
214
+ }
215
+ export function matchDiscourseEvaluationFrame(words) {
216
+ const [first] = words;
81
217
  const verbIndex = words.findIndex((word) => ABSTRACT_FRAME_VERBS.includes(word));
82
218
  const tail = words.at(-1);
83
219
  const subject = verbIndex > 0 ? words.slice(0, verbIndex) : [];
220
+ const workClaim = matchDiscourseWorkClaim(words, verbIndex, subject);
221
+ if (workClaim !== undefined) {
222
+ return workClaim;
223
+ }
224
+ if (words.length > 8) {
225
+ return undefined;
226
+ }
227
+ if (!["the", "this", "that", "it"].includes(first ?? "")) {
228
+ return undefined;
229
+ }
84
230
  return verbIndex > 0 &&
85
231
  tail !== undefined &&
86
232
  EVALUATION_TAILS.has(tail) &&