@nightkatana/kronosys-app 1.0.0-beta.20 → 1.0.0-beta.21

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/README.md CHANGED
@@ -99,7 +99,7 @@ Sinon : `NODE_EXTRA_CA_CERTS` pointant vers le PEM racine de l’organisation, o
99
99
 
100
100
  ## Publication npm (`@nightkatana/kronosys-app`)
101
101
 
102
- La version est définie dans `package.json` (référence actuelle : **`1.0.0-beta.20`**). Avant la première publication : créer le scope **@nightkatana** sur [npmjs.com](https://www.npmjs.com/) si besoin, puis `npm login`. Depuis ce dossier :
102
+ La version est définie dans `package.json` (référence actuelle : **`1.0.0-beta.21`**). Avant la première publication : créer le scope **@nightkatana** sur [npmjs.com](https://www.npmjs.com/) si besoin, puis `npm login`. Depuis ce dossier :
103
103
 
104
104
  ```bash
105
105
  npm publish --access public
@@ -5,6 +5,18 @@ export type UserChangelogEntry = {
5
5
  };
6
6
 
7
7
  export const USER_CHANGELOG_ENTRIES: UserChangelogEntry[] = [
8
+ {
9
+ "version": "1.0.0-beta.21",
10
+ "items": [
11
+ "**Parsing** : `findProjectTokens` (`lib/taskParsing.ts`) reconnaît désormais **`!projet`** et **`!projet#sous-étiquette`** lorsque le **`!`** suit **directement** un caractère autorisé (sans espace avant le sigil), sur le même principe que les **`#étiquettes`** « collées » au mot précédent. Les jetons **`@`** restent limités à une **frontière début de chaîne ou espace** afin d’éviter les faux positifs (ex. adresses courriel du type `contact@domaine`).",
12
+ "**Effet** : la modification d’un libellé du type `Courrier!perso#dentiste` met correctement à jour **`project`**, **`personalProject`** et les **étiquettes** persistées (y compris via **`updateTask`**), ce qui aligne le **reporting** (étiquettes, ventilation par projet selon le scope) sur l’intention utilisateur.",
13
+ "**Guide utilisateur** : `lib/userGuideCopy.ts` et `docs/USER_GUIDE.md` (FR/EN) — précision sur l’espace optionnel avant **`!`** ; en-tête et exemples de livraison pointent **`1.0.0-beta.21`**.",
14
+ "**Implémentation** : `docs/IMPLEMENTATION_DETAILS.md` (règle de frontière `!` / `@`) ; `lib/implementationNotes.ts` (version affichée et détail parsing scoped `!`).",
15
+ "**Dépôt** : `README.md` (référence de version npm).",
16
+ "**Tests** : `lib/taskParsing.test.ts` — cas `Courrier!perso#dentiste` ; attentes **`personalProject`** pour `parseQuickAddTagOrProjectLine` / `buildStartTaskFromInput` lorsqu’un projet est présent.",
17
+ "Bump applicatif : **`1.0.0-beta.21`** (`package.json`, `package-lock.json`) ; **CHANGELOG** usager régénéré (`npm run changelog:build`)."
18
+ ]
19
+ },
8
20
  {
9
21
  "version": "1.0.0-beta.20",
10
22
  "items": [
@@ -55,7 +55,7 @@ const frBundle: ImplementationNotesBundle = {
55
55
  "Liste exhaustive des intentions utilisateur·rice·s : chaque ligne indique si la capacité est livrée (✓) ou absente / hors périmètre (✗). Les cases à droite servent à cocher une ligne « revue » sur cet appareil — le détail technique long reste dans le dépôt.",
56
56
  statusKeyLine:
57
57
  "✓ vert = implémenté dans le produit · ✗ rouge = non livré ou volontairement exclu.",
58
- lastUpdated: "Dernière mise à jour : 2026-05-12 (v1.0.0-beta.20)",
58
+ lastUpdated: "Dernière mise à jour : 2026-05-12 (v1.0.0-beta.21)",
59
59
  repositoryDocLabel: "Document dépôt : docs/IMPLEMENTATION_DETAILS.md",
60
60
  storyGroupsHeading: "Inventaire des user stories",
61
61
  implementationColumnLabel: "Implémentation",
@@ -92,7 +92,7 @@ const frBundle: ImplementationNotesBundle = {
92
92
  },
93
93
  {
94
94
  implemented: true,
95
- text: "Pour une livraison donnée (p. ex. 1.0.0-beta.20), valider le comportement en croisant la section correspondante du **CHANGELOG**, les récits marqués livrés sur cette page et `docs/IMPLEMENTATION_DETAILS.md`, puis cocher au besoin **intégration** / **E2E**.",
95
+ text: "Pour une livraison donnée (p. ex. 1.0.0-beta.21), valider le comportement en croisant la section correspondante du **CHANGELOG**, les récits marqués livrés sur cette page et `docs/IMPLEMENTATION_DETAILS.md`, puis cocher au besoin **intégration** / **E2E**.",
96
96
  },
97
97
  {
98
98
  implemented: true,
@@ -340,7 +340,7 @@ const frBundle: ImplementationNotesBundle = {
340
340
  e2eChecked: true,
341
341
  text: "Créer une tâche avec titre et utiliser # pour étiquettes, @ pour projets productifs et ! pour projets personnels, avec aide à la saisie.",
342
342
  detail:
343
- "Le champ « sur quoi travaillez-vous » accepte dans le libellé des jetons #étiquette, @projet (travail) et !projet (personnel) ; ils sont interprétés au lancement (`startTask`), alimentent les listes d’étiquettes, `knownProjects` / `knownPersonalProjects`, et s’affichent sur la carte tâche. Les suggestions (modèles, historique) complètent la saisie.",
343
+ "Le champ « sur quoi travaillez-vous » accepte dans le libellé des jetons #étiquette, @projet (travail) et !projet (personnel) ; ils sont interprétés au lancement (`startTask`), alimentent les listes d’étiquettes, `knownProjects` / `knownPersonalProjects`, et s’affichent sur la carte tâche. Depuis **1.0.0-beta.21**, le **`!`** peut être **collé** au mot précédent (sans espace), comme certaines formes de **`#`** — ex. `Courrier!perso#dentiste` ; le **`@`** reste en général après **espace** ou en **début** de segment pour limiter les faux positifs sur les courriels. Les suggestions (modèles, historique) complètent la saisie.",
344
344
  example:
345
345
  "Saisir « Correctifs auth #bugfix @mobile » ou « Repos #repos !perso », lancer le suivi : pastille projet ciel ou rose selon le jeton ; les rubriques connues se mettent à jour pour les prochains choix.",
346
346
  },
@@ -360,7 +360,7 @@ const frBundle: ImplementationNotesBundle = {
360
360
  e2eChecked: false,
361
361
  text: "Utiliser des étiquettes sous portée projet personnel au format `!projet#sous-étiquette` (équivalent de `@projet#code` pour le travail).",
362
362
  detail:
363
- "`TagPills`, `formatTagDisplayForTask` et le parsing scoped dans `lib/taskParsing.ts` propagent le contexte `personalProject` pour l’affichage.",
363
+ "`TagPills`, `formatTagDisplayForTask` et le parsing scoped dans `lib/taskParsing.ts` propagent le contexte `personalProject` pour l’affichage. Le jeton **`!`** de portée peut être **collé** au mot précédent (**1.0.0-beta.21**, `findProjectTokens`).",
364
364
  example:
365
365
  "« Suivi !dentiste#urgent » : l’étiquette scoped s’affiche avec le préfixe `!`.",
366
366
  },
@@ -622,7 +622,7 @@ const enBundle: ImplementationNotesBundle = {
622
622
  "An exhaustive list of user intentions: each row shows whether the capability is shipped (✓) or missing / out of scope (✗). Checkboxes on the right mark a row as reviewed on this device — long technical detail stays in the repo doc.",
623
623
  statusKeyLine:
624
624
  "Green ✓ = implemented in the product · Red ✗ = not shipped or intentionally excluded.",
625
- lastUpdated: "Last updated: 2026-05-12 (v1.0.0-beta.20)",
625
+ lastUpdated: "Last updated: 2026-05-12 (v1.0.0-beta.21)",
626
626
  repositoryDocLabel: "Repository document: docs/IMPLEMENTATION_DETAILS.md",
627
627
  storyGroupsHeading: "User story inventory",
628
628
  implementationColumnLabel: "Implementation",
@@ -659,7 +659,7 @@ const enBundle: ImplementationNotesBundle = {
659
659
  },
660
660
  {
661
661
  implemented: true,
662
- text: "For a given release (e.g. 1.0.0-beta.20), cross-check the matching **CHANGELOG** section, the shipped stories on this page and `docs/IMPLEMENTATION_DETAILS.md`, then tick **Integration** / **E2E** as appropriate.",
662
+ text: "For a given release (e.g. 1.0.0-beta.21), cross-check the matching **CHANGELOG** section, the shipped stories on this page and `docs/IMPLEMENTATION_DETAILS.md`, then tick **Integration** / **E2E** as appropriate.",
663
663
  },
664
664
  {
665
665
  implemented: true,
@@ -907,7 +907,7 @@ const enBundle: ImplementationNotesBundle = {
907
907
  e2eChecked: true,
908
908
  text: "Create a task with a title and use # for tags, @ for work projects, and ! for personal projects with typing assistance.",
909
909
  detail:
910
- "The “what are you working on” field accepts #tag, @project (work), and !project (personal) tokens in the title; they are parsed on start (`startTask`), feed known tag lists plus `knownProjects` / `knownPersonalProjects`, and render on the task card. Templates and suggestions complement typing.",
910
+ "The “what are you working on” field accepts #tag, @project (work), and !project (personal) tokens in the title; they are parsed on start (`startTask`), feed known tag lists plus `knownProjects` / `knownPersonalProjects`, and render on the task card. Since **1.0.0-beta.21**, **`!`** may be **glued** to the previous word (no space), like some **`#`** forms — e.g. `Errands!me#dentist`; **`@`** still expects **leading whitespace** or **string start** so mid-token emails are not parsed as projects. Templates and suggestions complement typing.",
911
911
  example:
912
912
  "Enter “Auth fixes #bugfix @mobile” or “Rest #rest !me”, start tracking: the project chip is sky or rose depending on the token; known lists update for the next picks.",
913
913
  },
@@ -927,7 +927,7 @@ const enBundle: ImplementationNotesBundle = {
927
927
  e2eChecked: false,
928
928
  text: "Use per-personal-project scoped tags in the `!project#subtag` form (same idea as `@project#code` for work).",
929
929
  detail:
930
- "`TagPills`, `formatTagDisplayForTask`, and scoped parsing in `lib/taskParsing.ts` carry `personalProject` for display.",
930
+ "`TagPills`, `formatTagDisplayForTask`, and scoped parsing in `lib/taskParsing.ts` carry `personalProject` for display. The scoped **`!`** token may be **glued** to the previous word (**1.0.0-beta.21**, `findProjectTokens`).",
931
931
  example:
932
932
  "“Follow-up !dentist#urgent”: scoped tag renders with the `!` prefix.",
933
933
  },
@@ -22,31 +22,59 @@ type ProjectTokenMatch = {
22
22
 
23
23
  /**
24
24
  * Repère les jetons `@projet`, `!projet`, `@p#t` et `!p#t` (ordre d’apparition dans la chaîne).
25
+ *
26
+ * - `@` : uniquement après début de chaîne ou un espace (évite `courriel@domaine`).
27
+ * - `!` : idem **ou** collé au caractère précédent (comme les `#tags`), ex. `Courrier!perso#dentiste`.
25
28
  */
26
29
  export function findProjectTokens(raw: string): ProjectTokenMatch[] {
27
30
  const norm = raw.replace(/\s+/g, " ").trim();
28
- const re = /(?:^|\s)([@!])([^\s@!#]+)(?:#([^\s#]+))?/g;
29
- const out: ProjectTokenMatch[] = [];
30
- let m: RegExpExecArray | null;
31
- while ((m = re.exec(norm)) !== null) {
32
- const sigil = m[1] as "@" | "!";
33
- const projRaw = (m[2] || "").trim();
34
- const locRaw = m[3];
35
- const full = m[0];
36
- const start = m.index;
37
- const end = start + full.length;
38
- if (!projRaw) {
39
- continue;
31
+ /** Jetons `@` / `!` précédés d’une frontière « début ou espace ». */
32
+ const reBoundary = /(?:^|\s)([@!])([^\s@!#]+)(?:#([^\s#]+))?/g;
33
+ /** `!` seul : autorisé après un caractère non blanc, non `!`, non `#` (aligné sur les `#tags` collés). */
34
+ const reBangGlued = /(?<=[^\s!#])(!)([^\s@!#]+)(?:#([^\s#]+))?/g;
35
+
36
+ const pushMatch = (
37
+ out: ProjectTokenMatch[],
38
+ sigil: "@" | "!",
39
+ projRaw: string,
40
+ locRaw: string | undefined,
41
+ start: number,
42
+ fullLen: number,
43
+ ) => {
44
+ const end = start + fullLen;
45
+ const proj = projRaw.trim();
46
+ if (!proj) {
47
+ return;
40
48
  }
41
49
  if (locRaw !== undefined && locRaw.length > 0) {
42
50
  const loc = locRaw.trim();
43
51
  if (!loc || loc.includes("#")) {
44
- continue;
52
+ return;
45
53
  }
46
- out.push({ start, end, plain: false, sigil, projRaw, loc });
47
- } else if (!projRaw.includes("#")) {
48
- out.push({ start, end, plain: true, sigil, projRaw });
54
+ out.push({ start, end, plain: false, sigil, projRaw: proj, loc });
55
+ } else if (!proj.includes("#")) {
56
+ out.push({ start, end, plain: true, sigil, projRaw: proj });
57
+ }
58
+ };
59
+
60
+ const buf: ProjectTokenMatch[] = [];
61
+ let m: RegExpExecArray | null;
62
+ while ((m = reBoundary.exec(norm)) !== null) {
63
+ pushMatch(buf, m[1] as "@" | "!", m[2], m[3], m.index, m[0].length);
64
+ }
65
+ while ((m = reBangGlued.exec(norm)) !== null) {
66
+ pushMatch(buf, "!", m[2], m[3], m.index, m[0].length);
67
+ }
68
+
69
+ buf.sort((a, b) => a.start - b.start || b.end - a.end);
70
+ const out: ProjectTokenMatch[] = [];
71
+ let lastEnd = -1;
72
+ for (const t of buf) {
73
+ if (t.start < lastEnd) {
74
+ continue;
49
75
  }
76
+ out.push(t);
77
+ lastEnd = t.end;
50
78
  }
51
79
  return out;
52
80
  }
@@ -130,9 +130,9 @@ function frBundle(): UserGuideBundle {
130
130
  id: "ug-sessions-tasks",
131
131
  title: "Sessions, tâches, étiquettes et projets",
132
132
  searchIndex:
133
- "session live archiver terminer tâche minuteur pause sous-tâche historique liste résumé planifiée pastille badge conflit navigation projet personnel !perso",
133
+ "session live archiver terminer tâche minuteur pause sous-tâche historique liste résumé planifiée pastille badge conflit navigation projet personnel !perso jeton collé courriel",
134
134
  paragraphs: [
135
- "La **session** est le **sac** où vient s’additionner le travail sur une période ; la **tâche** est le fil conducteur du moment. **#mot** = thème ; **@nom** = projet **productif** ; **!nom** = projet **personnel** (suivi hors travail facturable, pastille souvent **rose**) — c’est de la **classification**, pas un examen d’orthographe.",
135
+ "La **session** est le **sac** où vient s’additionner le travail sur une période ; la **tâche** est le fil conducteur du moment. **#mot** = thème ; **@nom** = projet **productif** ; **!nom** = projet **personnel** (suivi hors travail facturable, pastille souvent **rose**) — c’est de la **classification**, pas un examen d’orthographe. Le **`!`** (y compris **`!projet#sous-étiquette`**) peut être **collé** au mot précédent **sans espace** — ex. `Courrier!perso#dentiste` ; le **`@`** reste en principe **après un espace** ou en **début** de segment pour limiter les faux positifs sur les courriels.",
136
136
  ],
137
137
  optionsTitle: "Nouvelle session — le cadre, au choix",
138
138
  options: [
@@ -366,9 +366,9 @@ function enBundle(): UserGuideBundle {
366
366
  id: "ug-sessions-tasks",
367
367
  title: "Sessions, tasks, tags, and projects",
368
368
  searchIndex:
369
- "session archive end task timer pause subtask history list summary planned badge chip conflict navigation personal project",
369
+ "session archive end task timer pause subtask history list summary planned badge chip conflict navigation personal project glued bang email",
370
370
  paragraphs: [
371
- "A **session** is the **bag** work piles into; a **task** is the **thread** of a moment. **#** tags themes; **@** marks a **work** project; **!** marks a **personal** project (often a **rose** chip) — naming helpers, not a grammar test.",
371
+ "A **session** is the **bag** work piles into; a **task** is the **thread** of a moment. **#** tags themes; **@** marks a **work** project; **!** marks a **personal** project (often a **rose** chip) — naming helpers, not a grammar test. **`!`** (including **`!project#subtag`**) may be **glued** to the previous word with **no space** — e.g. `Errands!personal#dentist`; **`@`** still expects **whitespace before** or **start of field** so emails like `user@host` are not parsed as projects.",
372
372
  ],
373
373
  optionsTitle: "New session — the frame, your choice",
374
374
  options: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightkatana/kronosys-app",
3
- "version": "1.0.0-beta.20",
3
+ "version": "1.0.0-beta.21",
4
4
  "description": "Kronosys — application Next.js (UI + API + SQLite).",
5
5
  "license": "MIT",
6
6
  "author": "nightkatana",