@chatpanel/events 0.27.0 → 0.28.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/schedule.js CHANGED
@@ -222,6 +222,11 @@ export const DANGLING_TAILS = Object.freeze(new Set([
222
222
  'and', 'or', 'but', 'so', 'because', 'cause', 'cos', 'since', 'although', 'though', 'while',
223
223
  'whereas', 'unless', 'until', 'if', 'when', 'whenever', 'that', 'which', 'who', 'whom',
224
224
  'whose', 'than', 'as', 'like',
225
+ // 'what' ends a clause only in a handful of set phrases ("guess what") and otherwise means
226
+ // the object of the thought is still coming — "…testing to see what". Live captions cut
227
+ // exactly there and punctuate it, which is how a sentence ABOUT the product became an
228
+ // address to it (see isAddressed in voice-intents.js).
229
+ 'what',
225
230
  // prepositions and particles
226
231
  'to', 'of', 'in', 'on', 'at', 'by', 'for', 'from', 'with', 'without', 'about', 'into',
227
232
  'onto', 'over', 'under', 'between', 'through', 'during', 'against', 'per',
package/voice-intents.js CHANGED
@@ -502,24 +502,66 @@ export function refinementStream({ onChange = null } = {}) {
502
502
  */
503
503
  const NOUN_MARKERS = /^(?:the|a|an|our|your|their|my|this|that|these|those|about|on|in|of|with|via|using|called|named|to)$/i;
504
504
 
505
+ // Adverbs that sit between a subject and its verb — "chat panel ACTUALLY helps us".
506
+ const SUBJECT_ADVERBS = /^(?:actually|really|also|always|never|just|only|still|often|usually|basically|literally|probably|certainly|definitely|now|then|even|apparently|obviously)$/i;
507
+ // Verb forms that make whatever comes before them the SUBJECT of a claim rather than the
508
+ // person being spoken to. A closed list on purpose: the general rule ("any third-person verb")
509
+ // cannot be told from an imperative without a parser, and guessing wrong here costs a command.
510
+ const SUBJECT_VERBS = /^(?:is|isn't|was|wasn't|are|aren't|were|weren't|has|hasn't|have|had|does|doesn't|did|didn't|can|can't|cannot|could|couldn't|will|won't|would|wouldn't|should|shouldn't|shall|may|might|must|seems|helps|lets|gives|allows|works|looks|means|needs|wants|keeps|makes|shows|tells|comes|goes|takes|runs|becomes|provides|supports)$/i;
511
+ // …unless a pronoun follows, which turns the same auxiliary into a question aimed at us:
512
+ // "chatpanel, can YOU set a timer" against "chat panel can help us".
513
+ const QUESTION_PRONOUNS = /^(?:you|we|i|they|it|there|he|she)$/i;
514
+
515
+ const bare = (t) => String(t?.w || '').replace(/[^\p{L}\p{N}']/gu, '');
516
+
517
+ /**
518
+ * Is the wake phrase the SUBJECT of the sentence rather than the person being addressed?
519
+ *
520
+ * "…what chat panel actually helps us to monitor" is a sentence ABOUT the product, and it
521
+ * fired a request. Nothing in the words before the name says so — the giveaway is what comes
522
+ * after it: a vocative is followed by a comma, an imperative or a question word, while a
523
+ * subject is followed by its verb.
524
+ */
525
+ function readsAsSubject(raw, tokens, endIdx) {
526
+ const next = tokens[endIdx + 1];
527
+ if (!next) return false; // nothing after the name at all — not a claim about it
528
+ // Punctuation between the name and what follows is the vocative comma (or a sentence
529
+ // break). Either way the name stands alone, which subjects do not do.
530
+ if (/[.!?,;:–—-]/.test(raw.slice(tokens[endIdx].end, next.start))) return false;
531
+ let j = endIdx + 1;
532
+ if (SUBJECT_ADVERBS.test(bare(tokens[j])) && tokens[j + 1]) j += 1;
533
+ if (!SUBJECT_VERBS.test(bare(tokens[j]))) return false;
534
+ return !QUESTION_PRONOUNS.test(bare(tokens[j + 1]));
535
+ }
536
+
505
537
  function isAddressed(raw, tokens, i, n = 0) {
506
538
  // The fuzzy match is generous enough to SWALLOW a leading article: "a chat panel" squashes
507
539
  // to "achatpanel", one edit from "chatpanel", so the determiner ends up inside the matched
508
540
  // span instead of before it. Check the first matched token too, or "a chat panel would be
509
541
  // useful here" reads as an address purely because the "a" was absorbed.
510
542
  if (n > 0 && NOUN_MARKERS.test(String(tokens[i].w || '').replace(/[^\p{L}\p{N}]/gu, ''))) return false;
543
+ // Whatever came before it, a name followed by its own verb is being TALKED ABOUT.
544
+ if (readsAsSubject(raw, tokens, i + n)) return false;
511
545
  if (i === 0) return true; // nothing before it — it opens the utterance
512
546
  const prev = tokens[i - 1];
513
547
  const word = String(prev.w || '').replace(/[^\p{L}\p{N}]/gu, '');
514
548
  if (NOUN_MARKERS.test(word)) return false; // "the chat panel" — a thing, not a listener
549
+ // "hey chatpanel", "ok chatpanel" — an address word is how people open one.
550
+ if (/^(?:ok|okay|hey|hi|yo|hello|so|um|uh)$/i.test(word)) return true;
515
551
  // Punctuation before it is the vocative comma or a sentence break — "…here. Okay, chat
516
552
  // panel", "so I was thinking. ChatPanel, what did we decide?" — and both mean a fresh
517
553
  // address rather than a continuing noun phrase. Measured from the previous token's START,
518
554
  // because the tokenizer keeps trailing punctuation ON the token ("thinking."), so the gap
519
555
  // between tokens is only the space and the full stop would be missed.
520
- if (/[.!?,;:]["')\]]?\s*$/.test(raw.slice(prev.start, tokens[i].start))) return true;
521
- // "hey chatpanel", "ok chatpanel" — an address word is the other way people open one.
522
- return /^(?:ok|okay|hey|hi|yo|hello|so|um|uh)$/i.test(word);
556
+ const gap = raw.slice(prev.start, tokens[i].start);
557
+ if (!/[.!?,;:]["'’”)\]]*\s*$/.test(gap)) return false;
558
+ // …but ONLY a break the speaker actually made. The transcriber invents full stops, and it
559
+ // invents them mid-clause: "…another round of testing to see what. Chat panel actually
560
+ // helps us to monitor" is one sentence about the product, cut in half by a machine, and the
561
+ // half-stop made the second half read as a fresh address. A break is only a break when the
562
+ // words before it are a finished thought — the same test that decides when a command has
563
+ // stopped growing, for exactly the same reason.
564
+ return !/[.!?…]/.test(gap) || commandLooksFinished(raw.slice(0, prev.end));
523
565
  }
524
566
 
525
567
  /**