symbiot 0.41.6 → 0.41.7

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 (2) hide show
  1. package/core.mjs +38 -1
  2. package/package.json +1 -1
package/core.mjs CHANGED
@@ -41,12 +41,49 @@ function saveTasks(t) { try { mkdirSync(CONFIG_DIR, { recursive: true }); writeF
41
41
  // with a clause more on the end. An extension only counts when the shorter one
42
42
  // is a whole sentence (8+ words), so "Fix the bug" never swallows "Fix the bug
43
43
  // in the login form".
44
+ // Or nearly the same (nearTask): an agent that suggests a task again rewords
45
+ // it, and TASKS.md listed both.
44
46
  const taskWords = (s) => String(s || "").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
45
47
  function sameTask(a, b) {
46
48
  const x = taskWords(a), y = taskWords(b); if (!x || !y) return false;
47
49
  if (x === y) return true;
48
50
  const [s, l] = x.length < y.length ? [x, y] : [y, x];
49
- return s.split(" ").length >= 8 && l.startsWith(s + " ");
51
+ return (s.split(" ").length >= 8 && l.startsWith(s + " ")) || nearTask(a, b);
52
+ }
53
+ // Nearly the same task, two ways:
54
+ // - nearly all the same words in the same order, 12+ words each ("…nothing
55
+ // recorded since 2026-08-03" and "…since 2026-08-04")
56
+ // - the same first sentence of 5+ words, with only what follows it different:
57
+ // "Set `WA_WABA_ID` in `.env`. It's needed to list templates" / "…to create Flows"
58
+ // Never when each names something the other doesn't: "Watch Gmail…" and
59
+ // "Watch WhatsApp…" are two tasks, however alike the rest. A name is a word
60
+ // with a capital that doesn't just start a sentence (Gmail, WhatsApp, WA).
61
+ const NEAR = 0.85, NEAR_WORDS = 12, LEAD_WORDS = 5;
62
+ function wordsOf(s) {
63
+ s = String(s || "");
64
+ return [...s.matchAll(/[\p{L}\p{N}]+/gu)].map((m) => {
65
+ const w = m[0], before = s.slice(0, m.index);
66
+ const starts = !before.trim() || /[.!?]["'`)\]]*\s*$/.test(before);
67
+ return { w: w.toLowerCase(), name: w.length > 1 && /\p{Lu}/u.test(w) && (!starts || /\p{Lu}/u.test(w.slice(1))) };
68
+ });
69
+ }
70
+ // The first sentence's words, when more follows it ("" when it's all one sentence).
71
+ function leadOf(s) { const m = String(s || "").match(/^(.*?[.!?])["'`)\]]*\s+\S/s); return m ? taskWords(m[1]) : ""; }
72
+ function nearTask(a, b) {
73
+ const x = wordsOf(a), y = wordsOf(b), m = x.length, n = y.length; if (!m || !n) return false;
74
+ const lead = leadOf(a), sameLead = !!lead && lead === leadOf(b) && lead.split(" ").length >= LEAD_WORDS;
75
+ if (!sameLead && (Math.min(m, n) < NEAR_WORDS || Math.min(m, n) / Math.max(m, n) < NEAR)) return false;
76
+ // the longest run of words both have in order, and the words left over on each side
77
+ const L = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1));
78
+ for (let i = m - 1; i >= 0; i--) for (let j = n - 1; j >= 0; j--) L[i][j] = x[i].w === y[j].w ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
79
+ if (!sameLead && (2 * L[0][0]) / (m + n) < NEAR) return false;
80
+ let i = 0, j = 0, nx = false, ny = false;
81
+ while (i < m && j < n) {
82
+ if (x[i].w === y[j].w) { i++; j++; } else if (L[i + 1][j] >= L[i][j + 1]) { if (x[i++].name) nx = true; } else if (y[j++].name) ny = true;
83
+ }
84
+ for (; i < m; i++) if (x[i].name) nx = true;
85
+ for (; j < n; j++) if (y[j].name) ny = true;
86
+ return !(nx && ny);
50
87
  }
51
88
  // One of each: near-duplicates collapse into the wording that says the most.
52
89
  function uniqueTasks(texts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.41.6",
3
+ "version": "0.41.7",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {