@piecemaker-legal/piecemaker 2.0.11 → 2.0.12

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 (120) hide show
  1. package/dist/assets/{FindReplaceDialog-KU5TEIBJ-BucOHLdm.js → FindReplaceDialog-KU5TEIBJ-BBxf1he7.js} +1 -1
  2. package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-D5UqbelV.js → FootnotePropertiesDialog-W3YHM4BV-BYqp6U4x.js} +1 -1
  3. package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-8yEhLigd.js → HyperlinkDialog-ZYH7ZHRF-BVC9zOMs.js} +4 -4
  4. package/dist/assets/{ImagePositionDialog-HEWYR6Q3-wGIBYxCm.js → ImagePositionDialog-HEWYR6Q3-Xn2mC64y.js} +1 -1
  5. package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D86PckZ2.js → ImagePropertiesDialog-RAB47C6M-CFcdqEif.js} +1 -1
  6. package/dist/assets/{PageSetupDialog-AIGIXO3E-DLETP-7v.js → PageSetupDialog-AIGIXO3E-DECaeRS9.js} +1 -1
  7. package/dist/assets/{SplitCellDialog-IFBKYCD6-CwdQBXeb.js → SplitCellDialog-IFBKYCD6-ByYa04H_.js} +1 -1
  8. package/dist/assets/{TablePropertiesDialog-TMLKGOOW-j2nA7KJs.js → TablePropertiesDialog-TMLKGOOW-BuyqZB5A.js} +1 -1
  9. package/dist/assets/{WatermarkDialog-KRL7WMIS-XFJ9gTkC.js → WatermarkDialog-KRL7WMIS-ByZ1Acr2.js} +1 -1
  10. package/dist/assets/{abnfDiagram-VCTEODGH-DiCrND_Y.js → abnfDiagram-VCTEODGH-BtttzdZA.js} +1 -1
  11. package/dist/assets/{arc-DOjgbMYz.js → arc-DN4m1tLs.js} +1 -1
  12. package/dist/assets/{architectureDiagram-5GKGNRK7-brzIFVhl.js → architectureDiagram-5GKGNRK7-Bt-zFumG.js} +1 -1
  13. package/dist/assets/{blockDiagram-I7D4REHJ-C8JhTjQQ.js → blockDiagram-I7D4REHJ-D14V8v4j.js} +1 -1
  14. package/dist/assets/{c4Diagram-7LVT6UL2-jfYxccyk.js → c4Diagram-7LVT6UL2-BynDo1-W.js} +1 -1
  15. package/dist/assets/channel-C4gJXzYa.js +1 -0
  16. package/dist/assets/{chunk-2Q5K7J3B-CrlEShp9.js → chunk-2Q5K7J3B-B8gKhSj9.js} +1 -1
  17. package/dist/assets/{chunk-5VM5RSS4-DzPlUHGq.js → chunk-5VM5RSS4-GvOpglRR.js} +1 -1
  18. package/dist/assets/{chunk-F27PBJKO-Cp9cpeWU.js → chunk-F27PBJKO-CAgZy0qc.js} +1 -1
  19. package/dist/assets/{chunk-IMKFNOWR-BifUldEN.js → chunk-IMKFNOWR-gf8Ux8Zd.js} +1 -1
  20. package/dist/assets/{chunk-JWPE2WC7-ggPEIkWu.js → chunk-JWPE2WC7-D4zFFRMp.js} +1 -1
  21. package/dist/assets/{chunk-POPQ4Y6H-3h_nayRX.js → chunk-POPQ4Y6H-Ct8X_LaT.js} +1 -1
  22. package/dist/assets/{chunk-SVP7TREG-CH29aG6J.js → chunk-SVP7TREG-g5iH1bs5.js} +1 -1
  23. package/dist/assets/{chunk-TICWLB2K-PluJUM5k.js → chunk-TICWLB2K-89-VFKTn.js} +1 -1
  24. package/dist/assets/{chunk-XXDRQBXY-DXU39Vwg.js → chunk-XXDRQBXY-CaQbdObb.js} +1 -1
  25. package/dist/assets/classDiagram-ZZMXUADV-Cm08FeYk.js +1 -0
  26. package/dist/assets/classDiagram-v2-VYDZK3BY-Cm08FeYk.js +1 -0
  27. package/dist/assets/{cose-bilkent-JH36ORCC-Dz6roYrN.js → cose-bilkent-JH36ORCC-BiYOGJAU.js} +1 -1
  28. package/dist/assets/{cynefin-OW5HDTMX-BLzrpSit.js → cynefin-OW5HDTMX-F6mJ-4tj.js} +1 -1
  29. package/dist/assets/{cynefinDiagram-5FMLGOSQ-kfxnERDZ.js → cynefinDiagram-5FMLGOSQ-q-Rggn6r.js} +1 -1
  30. package/dist/assets/{dagre-GXQ25YYZ-DP57bp-N.js → dagre-GXQ25YYZ-COqImSUH.js} +1 -1
  31. package/dist/assets/{diagram-S7CK7UJ4-DAlsrQVW.js → diagram-S7CK7UJ4-BAg1dyEc.js} +1 -1
  32. package/dist/assets/{diagram-UQ7AKVKN-By5yrMer.js → diagram-UQ7AKVKN-DFJS8qCV.js} +1 -1
  33. package/dist/assets/{diagram-VSXAHHWV-Dx89kryN.js → diagram-VSXAHHWV-BfldL35r.js} +1 -1
  34. package/dist/assets/{diagram-VX7I27RA-B9fARitZ.js → diagram-VX7I27RA-kLjovpYe.js} +1 -1
  35. package/dist/assets/{diagram-Z3DM3KII-DC15CEH1.js → diagram-Z3DM3KII-Dhwvi3Iu.js} +1 -1
  36. package/dist/assets/{ebnfDiagram-PWID7BFC-BKQzghGF.js → ebnfDiagram-PWID7BFC-Bp4YE1i7.js} +1 -1
  37. package/dist/assets/{erDiagram-RLTQ6QDP-CPY6RM7G.js → erDiagram-RLTQ6QDP-BKDdaSWz.js} +1 -1
  38. package/dist/assets/{flowDiagram-HODETNUW-4dL4GJcf.js → flowDiagram-HODETNUW-zHProVxL.js} +1 -1
  39. package/dist/assets/{ganttDiagram-EL5Y4UJY-D4-cVcfb.js → ganttDiagram-EL5Y4UJY-B-B-BI6j.js} +1 -1
  40. package/dist/assets/{gitGraphDiagram-WWUBYQGX-BMeVIEiX.js → gitGraphDiagram-WWUBYQGX-BTHMg5CH.js} +1 -1
  41. package/dist/assets/index-C5slQNvE.css +1 -0
  42. package/dist/assets/{index-DNtFDOni.js → index-CJP4dZ-c.js} +146 -146
  43. package/dist/assets/{infoDiagram-27XIBGKW-D5qMSUEU.js → infoDiagram-27XIBGKW-mabp4H9I.js} +1 -1
  44. package/dist/assets/{ishikawaDiagram-5VMMS53U-BDnyKRHm.js → ishikawaDiagram-5VMMS53U-CLNhcCrM.js} +1 -1
  45. package/dist/assets/{journeyDiagram-3NMN7TZE-dGz5ZGzq.js → journeyDiagram-3NMN7TZE-CGEi2RB6.js} +1 -1
  46. package/dist/assets/{kanban-definition-UXKFOSKX-D7UAz7o_.js → kanban-definition-UXKFOSKX-B2fLX1fK.js} +1 -1
  47. package/dist/assets/{layout-DKrmEGrz.js → layout-CloKWsFq.js} +1 -1
  48. package/dist/assets/{linear-CrTdMY-p.js → linear-BFyydI0c.js} +1 -1
  49. package/dist/assets/{mermaid.core-CXQOT5G6.js → mermaid.core-Dxl3LVAh.js} +6 -6
  50. package/dist/assets/{mindmap-definition-YA3MSWOX-VgEiYfay.js → mindmap-definition-YA3MSWOX-FT4pm55s.js} +1 -1
  51. package/dist/assets/{pegDiagram-XKGWAZYB-9IqT8gGx.js → pegDiagram-XKGWAZYB-okyIfJWM.js} +1 -1
  52. package/dist/assets/{pieDiagram-E7YTZNPT-D98pRE-t.js → pieDiagram-E7YTZNPT-NF61CQz8.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-AXDQQJYC-b9PwkYzL.js → quadrantDiagram-AXDQQJYC-uVfvZzzt.js} +1 -1
  54. package/dist/assets/{railroadDiagram-O6MQD6OU-Dz87hsPE.js → railroadDiagram-O6MQD6OU-Dp8IpeoH.js} +1 -1
  55. package/dist/assets/{requirementDiagram-BXWQKSXE-CkcV73dK.js → requirementDiagram-BXWQKSXE-D8VV0oQe.js} +1 -1
  56. package/dist/assets/{sankeyDiagram-P5KCCOFB-UBN31Yrs.js → sankeyDiagram-P5KCCOFB-D-xhxjob.js} +1 -1
  57. package/dist/assets/{sequenceDiagram-WJ2MYXX4-CpQxd5Za.js → sequenceDiagram-WJ2MYXX4-BAQBK95h.js} +1 -1
  58. package/dist/assets/{sizeCapture-INFHLROL-mCsFmyzw.js → sizeCapture-INFHLROL-C6rk9SVx.js} +1 -1
  59. package/dist/assets/{stateDiagram-D77RDMKH-B0MTBN-3.js → stateDiagram-D77RDMKH-DshqaEOj.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-MP3YSRHH-DzBPN39W.js → stateDiagram-v2-MP3YSRHH-BohZIrk5.js} +1 -1
  61. package/dist/assets/{swimlanes-42K2YHIH-Dzydjm0e.js → swimlanes-42K2YHIH-DgcPDy4l.js} +1 -1
  62. package/dist/assets/swimlanesDiagram-VR7AAH4N-CuM3Su0K.js +8 -0
  63. package/dist/assets/{timeline-definition-24CTP7MA-Dz0toOLu.js → timeline-definition-24CTP7MA-OgpIe-gQ.js} +1 -1
  64. package/dist/assets/{vennDiagram-4TSXK5OY-DOWp7dkf.js → vennDiagram-4TSXK5OY-BZzGd6jt.js} +1 -1
  65. package/dist/assets/{wardleyDiagram-VM6X3IG4-CUxsztUB.js → wardleyDiagram-VM6X3IG4-1tg-H7l_.js} +1 -1
  66. package/dist/assets/{xychartDiagram-S5SC5T6Z-DS96vNED.js → xychartDiagram-S5SC5T6Z-ByTsX8g0.js} +1 -1
  67. package/dist/index.html +2 -2
  68. package/dist/sw.js +1 -1
  69. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +61 -1
  70. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -1
  71. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +0 -48
  72. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -1
  73. package/dist-server/server/piecemaker/knowledge/pipeline.js +25 -29
  74. package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -1
  75. package/dist-server/server/piecemaker/knowledge/service.js +1 -16
  76. package/dist-server/server/piecemaker/knowledge/service.js.map +1 -1
  77. package/dist-server/server/piecemaker/project-registry.js +5 -5
  78. package/dist-server/server/piecemaker/project-registry.js.map +1 -1
  79. package/package.json +1 -1
  80. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +1 -1
  81. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +2 -1
  82. package/server/piecemaker/anonymizer/rewrite.cjs +2 -2
  83. package/server/piecemaker/anonymizer/service.cjs +3 -2
  84. package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +3 -31
  85. package/server/piecemaker/index.ts +1 -1
  86. package/server/piecemaker/knowledge/pipeline.ts +24 -31
  87. package/server/piecemaker/knowledge/service.ts +1 -14
  88. package/server/piecemaker/project-registry.ts +6 -6
  89. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +9 -162
  90. package/server/piecemaker/vendor/installer/lib/commandes.mjs +1 -2
  91. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +62 -3
  92. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +23 -69
  93. package/server/piecemaker/vendor/installer/steps/12-mcp-piecemaker.mjs +3 -3
  94. package/server/piecemaker/vendor/mcp/README.md +1 -1
  95. package/server/piecemaker/vendor/mcp/piecemaker/server.mjs +12 -75
  96. package/server/piecemaker/vendor/piecemaker-plugin/hooks/hooks.json +0 -40
  97. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +2 -2
  98. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +26 -7
  99. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/commits.cjs +0 -28
  100. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/hook-io.mjs +0 -13
  101. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/institutional-terms.cjs +3 -3
  102. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +10 -143
  103. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +1 -1
  104. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/session-timing.cjs +0 -65
  105. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +11 -2
  106. package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +9 -5
  107. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +2 -150
  108. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +3 -0
  109. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +23 -372
  110. package/server/piecemaker/vendor/websocket-server/renamed-originals.cjs +136 -0
  111. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +115 -96
  112. package/dist/assets/channel-Dajzu2W1.js +0 -1
  113. package/dist/assets/classDiagram-ZZMXUADV-dDEP_giv.js +0 -1
  114. package/dist/assets/classDiagram-v2-VYDZK3BY-dDEP_giv.js +0 -1
  115. package/dist/assets/index-Dc9wNuS4.css +0 -1
  116. package/dist/assets/swimlanesDiagram-VR7AAH4N-BXQpbUMD.js +0 -8
  117. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +0 -194
  118. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +0 -71
  119. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit-worker.mjs +0 -260
  120. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit.mjs +0 -83
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Serveur MCP « piecemaker » — outils de conversion et de chronologie exposés
3
+ * Serveur MCP « piecemaker » — outil de conversion exposé
4
4
  * à Claude Code (et à tout client MCP) sans passer
5
5
  * par du texte injecté dans un CLAUDE.md.
6
6
  *
@@ -10,10 +10,10 @@
10
10
  * d'arguments, même localisation de dossier, une seule implémentation
11
11
  * (voir `installer/bin/piecemaker.mjs`).
12
12
  *
13
- * Les commandes lancées ici (`conversion --json`, `chronology --json`, `chronology --action write/edit
14
- * --json`) court-circuitent toutes le bandeau, la vérification de mise à jour
15
- * et le menu interactif (`installer/bin/piecemaker.mjs:1006-1009`) : aucun
16
- * service PieceMaker n'est démarré, arrêté ni redémarré par ce serveur.
13
+ * La commande lancée ici (`conversion --json`) court-circuite le bandeau, la
14
+ * vérification de mise à jour et le menu interactif. Elle confie le travail au
15
+ * serveur applicatif déjà lancé (application de bureau ou `piecemaker`) et ne
16
+ * démarre un serveur que si aucun ne répond.
17
17
  */
18
18
 
19
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -95,28 +95,6 @@ export function conversionArgs({ dossier, pieces, force }) {
95
95
  return args;
96
96
  }
97
97
 
98
- export function chronologyArgs({ dossier }) {
99
- return ['chronology', '--json', '--case', dossier];
100
- }
101
-
102
- export function chronologyCorrectionArgs(action, { dossier, path: piecePath, nature, dateIso, localisation, fields, entityAdditions, entityExclusions, reason }) {
103
- const correction = {};
104
- if (nature !== undefined) correction.nature = nature;
105
- if (dateIso !== undefined) correction.dateIso = dateIso;
106
- if (localisation !== undefined) correction.localisation = localisation;
107
- if (fields !== undefined) correction.fields = fields;
108
- if (reason !== undefined) correction.reason = reason;
109
- if (entityAdditions !== undefined || entityExclusions !== undefined) {
110
- correction.entityDecisions = {
111
- additions: entityAdditions || [],
112
- exclusions: entityExclusions || [],
113
- };
114
- }
115
- const args = ['chronology', '--action', action, '--path', piecePath, '--correction-json', JSON.stringify(correction), '--json'];
116
- if (dossier) args.push('--case', dossier);
117
- return args;
118
- }
119
-
120
98
  /** Dossier ciblé par un appel d'outil : celui demandé, sinon la session en cours. */
121
99
  export function resolveDossier(dossier) {
122
100
  return dossier && String(dossier).trim() ? dossier : process.cwd();
@@ -126,21 +104,6 @@ const DOSSIER_SCHEMA = z.string()
126
104
  .optional()
127
105
  .describe('Chemin absolu du dossier juridique ciblé. Par défaut, le répertoire de la session Claude Code en cours.');
128
106
 
129
- const CHRONOLOGY_CORRECTION_SCHEMA = {
130
- dossier: DOSSIER_SCHEMA,
131
- path: z.string().min(1).describe('Chemin relatif de la pièce, tel qu\'indexé par la chronologie.'),
132
- nature: z.string().optional().describe('Nature de l\'acte (ex. « Assignation », « Conclusions »).'),
133
- dateIso: z.string().optional().describe('Date de la pièce au format ISO 8601 (AAAA-MM-JJ).'),
134
- localisation: z.string().optional().describe('Juridiction ou localisation associée à la pièce.'),
135
- fields: z.array(z.object({ label: z.string(), value: z.string() })).optional()
136
- .describe('Champs personnalisés additionnels de la pièce.'),
137
- entityAdditions: z.array(z.string()).optional()
138
- .describe('Codes d\'entités (personnes/sociétés pseudonymisées) à rattacher manuellement à la pièce.'),
139
- entityExclusions: z.array(z.string()).optional()
140
- .describe('Codes d\'entités détectés automatiquement à exclure manuellement de la pièce.'),
141
- reason: z.string().optional().describe('Motif de la correction, consigné dans l\'historique du dossier.'),
142
- };
143
-
144
107
  /**
145
108
  * Construit le serveur MCP et y enregistre les outils. Séparé de
146
109
  * `main()` pour rester testable sans jamais brancher de transport stdio.
@@ -160,45 +123,19 @@ export function createServer({ execFn } = {}) {
160
123
  .describe('Noms ou chemins relatifs des pièces à convertir. Par défaut, toutes les pièces pas encore prêtes.'),
161
124
  force: z.boolean().optional().describe('Reconvertit et rescanne même les pièces déjà prêtes.'),
162
125
  },
126
+ annotations: {
127
+ title: 'Conversion et pseudonymisation des pièces',
128
+ readOnlyHint: false,
129
+ destructiveHint: false,
130
+ idempotentHint: true,
131
+ openWorldHint: false,
132
+ },
163
133
  }, async ({ dossier, pieces, force }) => {
164
134
  const resolved = resolveDossier(dossier);
165
135
  const result = await run(conversionArgs({ dossier: resolved, pieces, force }), resolved);
166
136
  return toToolResult(result);
167
137
  });
168
138
 
169
- server.registerTool('chronologie', {
170
- description: 'Affiche la chronologie pseudonymisée du dossier juridique (dates, pièces, événements), en JSON.',
171
- inputSchema: {
172
- dossier: DOSSIER_SCHEMA,
173
- },
174
- }, async ({ dossier }) => {
175
- const resolved = resolveDossier(dossier);
176
- const result = await run(chronologyArgs({ dossier: resolved }), resolved);
177
- return toToolResult(result);
178
- });
179
-
180
- server.registerTool('write_chronology', {
181
- description: 'Crée une correction manuelle de chronologie pour une pièce (date, nature d\'acte, '
182
- + 'localisation, personnes citées, champs custom). Échoue si une correction existe déjà pour cette '
183
- + 'pièce — utiliser edit_chronology pour la modifier.',
184
- inputSchema: CHRONOLOGY_CORRECTION_SCHEMA,
185
- }, async ({ dossier, ...params }) => {
186
- const resolved = resolveDossier(dossier);
187
- const result = await run(chronologyCorrectionArgs('write', { dossier: resolved, ...params }), resolved);
188
- return toToolResult(result);
189
- });
190
-
191
- server.registerTool('edit_chronology', {
192
- description: 'Modifie la correction manuelle de chronologie déjà enregistrée pour une pièce (date, '
193
- + 'nature d\'acte, localisation, personnes citées, champs custom). Échoue si aucune correction '
194
- + 'n\'existe pour cette pièce — utiliser write_chronology pour en créer une.',
195
- inputSchema: CHRONOLOGY_CORRECTION_SCHEMA,
196
- }, async ({ dossier, ...params }) => {
197
- const resolved = resolveDossier(dossier);
198
- const result = await run(chronologyCorrectionArgs('edit', { dossier: resolved, ...params }), resolved);
199
- return toToolResult(result);
200
- });
201
-
202
139
  return server;
203
140
  }
204
141
 
@@ -21,15 +21,6 @@
21
21
  }
22
22
  ]
23
23
  },
24
- {
25
- "matcher": "Write|Edit",
26
- "hooks": [
27
- {
28
- "type": "command",
29
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/commit-track.mjs\""
30
- }
31
- ]
32
- },
33
24
  {
34
25
  "matcher": "Write",
35
26
  "hooks": [
@@ -48,37 +39,6 @@
48
39
  }
49
40
  ]
50
41
  }
51
- ],
52
- "Stop": [
53
- {
54
- "matcher": "*",
55
- "hooks": [
56
- {
57
- "type": "command",
58
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/billing-track.mjs\""
59
- }
60
- ]
61
- },
62
- {
63
- "matcher": "*",
64
- "hooks": [
65
- {
66
- "type": "command",
67
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-commit.mjs\""
68
- }
69
- ]
70
- }
71
- ],
72
- "TaskCompleted": [
73
- {
74
- "matcher": "*",
75
- "hooks": [
76
- {
77
- "type": "command",
78
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/billing-track.mjs\""
79
- }
80
- ]
81
- }
82
42
  ]
83
43
  }
84
44
  }
@@ -72,8 +72,8 @@ async function main() {
72
72
  const pdfPath = path.join(rechercheDir, `${slug}.pdf`);
73
73
 
74
74
  // Le modèle n'a vu que des codes ; l'historique du cabinet doit rester
75
- // lisible → on ré-identifie le Markdown final avant écriture disque (même
76
- // logique que commit-track sur les libellés). Idempotent.
75
+ // lisible → on ré-identifie le Markdown final avant écriture disque.
76
+ // Idempotent.
77
77
  let markdown = renderMarkdown(data, { caseName: located.caseName });
78
78
  const legalCase = resolveProjectCaseMapping(located.caseRoot);
79
79
  if (legalCase?.reverse_mapping) markdown = revertMapping(markdown, legalCase.reverse_mapping);
@@ -4,6 +4,7 @@ const os = require('node:os');
4
4
  const path = require('node:path');
5
5
 
6
6
  const PROJECTS_FILE = 'projects.json';
7
+ const ANONYMIZED_PROJECTS_FILE = 'anonymized-projects.json';
7
8
 
8
9
  function piecemakerHome() {
9
10
  return process.env.PIECEMAKER_HOME || path.join(os.homedir(), '.piecemaker');
@@ -13,6 +14,10 @@ function projectsFile() {
13
14
  return path.join(piecemakerHome(), PROJECTS_FILE);
14
15
  }
15
16
 
17
+ function anonymizedProjectsFile() {
18
+ return path.join(piecemakerHome(), ANONYMIZED_PROJECTS_FILE);
19
+ }
20
+
16
21
  function realDirectory(value) {
17
22
  const requested = String(value || '').trim();
18
23
  if (!requested || !path.isAbsolute(requested)) return null;
@@ -41,24 +46,31 @@ function isEligibleProjectFolder(folder) {
41
46
  return !excludedRoots().some((root) => isInsideOrEqualPath(root, folder));
42
47
  }
43
48
 
44
- function readProjectSources() {
49
+ function readSources(file) {
45
50
  try {
46
- const parsed = JSON.parse(fs.readFileSync(projectsFile(), 'utf8'));
51
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
47
52
  return parsed?.sources && typeof parsed.sources === 'object' && !Array.isArray(parsed.sources) ? parsed.sources : {};
48
53
  } catch {
49
54
  return {};
50
55
  }
51
56
  }
52
57
 
58
+ function publishedFolders(file) {
59
+ const folders = Object.values(readSources(file)).flatMap((list) => (Array.isArray(list) ? list : []));
60
+ return [...new Set(folders.map(realDirectory).filter(Boolean))].filter(isEligibleProjectFolder);
61
+ }
62
+
53
63
  /** Absolute, existing and de-duplicated project folders, across every publishing server. */
54
64
  function registeredProjectFolders() {
55
- const folders = Object.values(readProjectSources()).flatMap((list) => (Array.isArray(list) ? list : []));
56
- return [...new Set(folders.map(realDirectory).filter(Boolean))].filter(isEligibleProjectFolder);
65
+ return publishedFolders(projectsFile());
66
+ }
67
+
68
+ function anonymizedProjectFolders() {
69
+ return publishedFolders(anonymizedProjectsFile());
57
70
  }
58
71
 
59
- function publishProjectSource(sourceId, folders) {
60
- const file = projectsFile();
61
- const sources = readProjectSources();
72
+ function writeSource(file, sourceId, folders) {
73
+ const sources = readSources(file);
62
74
  if (folders === null) delete sources[sourceId];
63
75
  else sources[sourceId] = [...new Set(folders.map((folder) => path.resolve(String(folder))))].sort();
64
76
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -72,6 +84,11 @@ function publishProjectSource(sourceId, folders) {
72
84
  }
73
85
  }
74
86
 
87
+ function publishProjectSource(sourceId, folders, anonymizedFolders = []) {
88
+ writeSource(projectsFile(), sourceId, folders);
89
+ writeSource(anonymizedProjectsFile(), sourceId, folders === null ? null : anonymizedFolders);
90
+ }
91
+
75
92
  function resolveTarget(target) {
76
93
  try {
77
94
  return fs.realpathSync(path.resolve(String(target)));
@@ -101,6 +118,7 @@ function locateProjectCase(target) {
101
118
  for (const caseRoot of roots) {
102
119
  if (!isInsideOrEqualPath(caseRoot, absolute)) continue;
103
120
  return {
121
+ anonymized: anonymizedProjectFolders().includes(caseRoot),
104
122
  casesRoot: path.dirname(caseRoot),
105
123
  caseName: path.basename(caseRoot),
106
124
  caseRoot,
@@ -113,6 +131,7 @@ function locateProjectCase(target) {
113
131
  }
114
132
 
115
133
  module.exports = {
134
+ anonymizedProjectFolders,
116
135
  locateProjectCase,
117
136
  publishProjectSource,
118
137
  registeredProjectFolders,
@@ -564,33 +564,6 @@ function resolveCase(casesRoot, caseName) {
564
564
  return { casesRoot: root, name, root: caseRoot };
565
565
  }
566
566
 
567
- function locateCaseFile(casesRoot, filePath) {
568
- const root = resolveCasesRoot(casesRoot);
569
- const requested = path.resolve(String(filePath || ''));
570
- let absolute = requested;
571
- try {
572
- absolute = fs.realpathSync(requested);
573
- } catch {
574
- try {
575
- absolute = path.join(fs.realpathSync(path.dirname(requested)), path.basename(requested));
576
- } catch {}
577
- }
578
- if (absolute === root || !absolute.startsWith(`${root}${path.sep}`)) return null;
579
- const relativeToRoot = path.relative(root, absolute);
580
- const [caseName, ...parts] = relativeToRoot.split(path.sep);
581
- if (!caseName || parts.length === 0) return null;
582
- const legalCase = resolveCase(root, caseName);
583
- if (isProtectedFile(absolute, legalCase.root)) {
584
- return { ...legalCase, absolute, protected: true, relative: parts.join('/') };
585
- }
586
- if (path.extname(absolute).toLowerCase() && !SAFE_EXTENSIONS.has(path.extname(absolute).toLowerCase())) {
587
- return { ...legalCase, absolute, protected: false, safe: false, relative: parts.join('/') };
588
- }
589
- const relative = path.relative(legalCase.root, absolute).split(path.sep).join('/');
590
- if (!relative || relative.startsWith('../')) return null;
591
- return { ...legalCase, absolute, protected: false, safe: SAFE_EXTENSIONS.has(path.extname(relative).toLowerCase()), relative };
592
- }
593
-
594
567
  async function safeCaseFiles(caseRoot) {
595
568
  const startedAt = performance.now();
596
569
  const root = await fsp.realpath(caseRoot);
@@ -1668,7 +1641,6 @@ module.exports = {
1668
1641
  logPerformance,
1669
1642
  listHistory,
1670
1643
  listHistoryPeriod,
1671
- locateCaseFile,
1672
1644
  parseCommitTrailers,
1673
1645
  originalFilesOverview,
1674
1646
  repositoryOverview,
@@ -25,8 +25,6 @@ import path from 'node:path';
25
25
 
26
26
  export const HOME_DIR = path.join(os.homedir(), '.piecemaker');
27
27
  export const CONFIG_FILE = path.join(HOME_DIR, 'config.json');
28
- export const BILLING_DIR = path.join(HOME_DIR, 'billing');
29
- export const SYNTHESIS_DIR = path.join(BILLING_DIR, 'synthese');
30
28
 
31
29
  /** Upper bound on waiting for stdout to drain before exiting anyway. */
32
30
  const FLUSH_TIMEOUT_MS = 2000;
@@ -236,14 +234,3 @@ export function ensureDirSafe(dir) {
236
234
  return false;
237
235
  }
238
236
  }
239
-
240
- /** Append one line to a JSONL file. Single appendFileSync call, never throws. */
241
- export function appendJsonl(filePath, obj) {
242
- try {
243
- ensureDirSafe(path.dirname(filePath));
244
- fs.appendFileSync(filePath, `${JSON.stringify(obj)}\n`, 'utf8');
245
- return true;
246
- } catch {
247
- return false;
248
- }
249
- }
@@ -9,9 +9,9 @@
9
9
  *
10
10
  * On ne débranche pas la détection : GLiNER continue de les trouver. C'est au
11
11
  * moment de bâtir le mapping qu'elles sont écartées — `normalizeMappingDocument`
12
- * (mapping.cjs) appelle `isInstitutionalEntity` sur chaque entité, à la lecture
13
- * comme à l'écriture. Une entité bannie ne persiste donc jamais dans
14
- * `mapping_default.json` et n'est jamais substituée par les hooks.
12
+ * (mapping.cjs) appelle `isInstitutionalEntity` sur chaque entité lue, et le
13
+ * magasin du dossier les retire de la base. Une entité bannie n'est donc jamais
14
+ * substituée par les hooks.
15
15
  *
16
16
  * La liste est globale (tous dossiers confondus), éditable depuis les paramètres
17
17
  * de l'administration, et la comparaison est insensible à la casse et aux
@@ -47,8 +47,7 @@ function readJsonFile(file, fallback = null) {
47
47
  * Tous les mappings présents, sous-dossier `WORKSPACE_SUBDIR` **et** racine
48
48
  * (lecture tolérante pendant la migration). Le fichier canonique du sous-dossier
49
49
  * passe en dernier : c'est lui qui gagne quand `readCaseMapping` fusionne, tandis
50
- * que les copies legacy (racine ou `mapping_<id>.json`) ne servent qu'à ne rien
51
- * perdre avant le prochain enregistrement.
50
+ * que les copies legacy (racine ou `mapping_<id>.json`) complètent la lecture.
52
51
  */
53
52
  function existingMappingFiles(caseRoot) {
54
53
  const dirs = [
@@ -80,113 +79,27 @@ function existingMappingFiles(caseRoot) {
80
79
  }
81
80
 
82
81
  /**
83
- * L'unique cible d'écriture par dossier vit désormais dans le sous-dossier
84
- * `WORKSPACE_SUBDIR`, pour garder la racine propre. Les anciens
85
- * `mapping_dossier.json` / `mapping_<id>.json` et un `mapping_default.json` resté
86
- * à la racine sont lus pour migration (voir `existingMappingFiles`), mais toute
87
- * écriture converge vers `<dossier>/<WORKSPACE_SUBDIR>/mapping_default.json`.
82
+ * Emplacement historique du mapping JSON d'un dossier, lu seulement : plus
83
+ * aucun code ne l'écrit, le mapping vit en base.
88
84
  */
89
85
  function caseMappingFile(caseRoot) {
90
86
  return path.join(caseRoot, WORKSPACE_SUBDIR, CANONICAL_MAPPING_FILE);
91
87
  }
92
88
 
93
- function cleanMappingString(value) {
94
- return String(value || '').trim();
95
- }
96
-
97
- function normalizePartyAssignments(value) {
98
- if (!Array.isArray(value)) return [];
99
- return value.map((raw) => {
100
- const assignment = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
101
- const variants = [...new Set((Array.isArray(assignment.variants) ? assignment.variants : [])
102
- .map(cleanMappingString).filter(Boolean))];
103
- return {
104
- field: cleanMappingString(assignment.field),
105
- code: cleanMappingString(assignment.code),
106
- original_code: cleanMappingString(assignment.original_code),
107
- category: cleanMappingString(assignment.category),
108
- principal: cleanMappingString(assignment.principal),
109
- variants,
110
- };
111
- }).filter((assignment) => assignment.code && assignment.variants.length);
112
- }
113
-
114
- function normalizeProcedureParty(raw, side) {
115
- const party = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
116
- const type = party.type === 'societe' ? 'societe' : 'personne_physique';
117
- const allowedPositions = new Set(['demandeur', 'defendeur', 'appelant', 'intime', 'requerant', 'mis_en_cause', 'intervenant', 'autre']);
118
- const position = allowedPositions.has(party.position) ? party.position : side === 'client' ? 'demandeur' : 'defendeur';
119
- return {
120
- type,
121
- position,
122
- position_libelle: position === 'autre' ? cleanMappingString(party.position_libelle) : '',
123
- civilite: type === 'personne_physique' ? cleanMappingString(party.civilite) : '',
124
- nom: type === 'personne_physique' ? cleanMappingString(party.nom) : '',
125
- date_naissance: type === 'personne_physique' ? cleanMappingString(party.date_naissance) : '',
126
- lieu_naissance: type === 'personne_physique' ? cleanMappingString(party.lieu_naissance) : '',
127
- adresse: type === 'personne_physique' ? cleanMappingString(party.adresse) : '',
128
- societe_nom: type === 'societe' ? cleanMappingString(party.societe_nom) : '',
129
- forme_sociale: type === 'societe' ? cleanMappingString(party.forme_sociale) : '',
130
- pays: type === 'societe' ? cleanMappingString(party.pays) || 'France' : '',
131
- siren: type === 'societe' ? cleanMappingString(party.siren) : '',
132
- siege_social: type === 'societe' ? cleanMappingString(party.siege_social) : '',
133
- representant: type === 'societe' ? cleanMappingString(party.representant) : '',
134
- mapping_assignments: normalizePartyAssignments(party.mapping_assignments),
135
- };
136
- }
137
-
138
- function relationshipId(source, target, role) {
139
- return `relation:${source}\u0000${target}\u0000${role}`;
140
- }
141
-
142
- function normalizeProfileRelationships(value) {
143
- if (!Array.isArray(value)) return [];
144
- const ids = new Set();
145
- const relationships = new Set();
146
- return value.flatMap((raw) => {
147
- const relationship = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
148
- const source = cleanMappingString(relationship.source);
149
- const original_source = cleanMappingString(relationship.original_source);
150
- const target = cleanMappingString(relationship.target);
151
- const role = cleanMappingString(relationship.role);
152
- const id = cleanMappingString(relationship.id) || relationshipId(source, target, role);
153
- const key = `${source}\u0000${target}\u0000${role}`;
154
- if (!source || !target || !role || source === target || ids.has(id) || relationships.has(key)) return [];
155
- ids.add(id);
156
- relationships.add(key);
157
- return [{ id, source, original_source, target, role }];
158
- });
159
- }
160
-
161
- function normalizeProcedureInfo(raw) {
162
- const info = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
163
- return {
164
- parties_clientes: (Array.isArray(info.parties_clientes) ? info.parties_clientes : [])
165
- .map((party) => normalizeProcedureParty(party, 'client')),
166
- parties_adverses: (Array.isArray(info.parties_adverses) ? info.parties_adverses : [])
167
- .map((party) => normalizeProcedureParty(party, 'adversaire')),
168
- relations: normalizeProfileRelationships(info.relations),
169
- };
170
- }
171
-
172
89
  function normalizeMappingDocument(raw) {
173
90
  const document = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
174
91
  const mapping = {};
175
92
  const reverse = {};
176
93
  const source = document.mapping && typeof document.mapping === 'object' ? document.mapping : {};
177
94
  // Les entités institutionnelles (juridictions, registres, publications
178
- // officielles…) sont écartées ici même : c'est le point de passage unique de la
179
- // lecture comme de l'écriture, si bien qu'une entité bannie ne persiste jamais
180
- // dans `mapping_default.json` et n'est jamais substituée par les hooks. GLiNER
181
- // continue de les détecter — on ne débranche que le codage.
95
+ // officielles…) sont écartées ici même, à la lecture : une entité bannie n'est
96
+ // jamais substituée par les hooks. GLiNER continue de les détecter — on ne
97
+ // débranche que le codage.
182
98
  for (const [entity, code] of Object.entries(source)) {
183
99
  const from = String(entity || '').trim();
184
100
  const to = String(code || '').trim();
185
101
  if (from && to && !isInstitutionalEntity(from)) mapping[from] = to;
186
102
  }
187
- const ignored = [...new Set((Array.isArray(document.ignored) ? document.ignored : [])
188
- .map((entity) => String(entity || '').trim())
189
- .filter(Boolean))];
190
103
  const reverseSource = document.reverse_mapping && typeof document.reverse_mapping === 'object' ? document.reverse_mapping : {};
191
104
  for (const [code, value] of Object.entries(reverseSource)) {
192
105
  const key = String(code || '').trim();
@@ -202,36 +115,10 @@ function normalizeMappingDocument(raw) {
202
115
  if (!reverse[code]) reverse[code] = [entity];
203
116
  else if (!reverse[code].includes(entity)) reverse[code].push(entity);
204
117
  }
205
- // `extracted_data` est écrit par `convert_and_scan_pipeline.py` : c'est lui
206
- // qui porte les variants d'une entité et l'analyse des adresses, dont la
207
- // dé-anonymisation partielle a besoin (anonymization-server.cjs).
208
- // L'administration ne l'édite pas, mais elle ne doit surtout pas le détruire —
209
- // seules les entrées dont le code a disparu du mapping sont retirées.
210
- const extracted = {};
211
- const extractedSource = document.extracted_data && typeof document.extracted_data === 'object'
212
- && !Array.isArray(document.extracted_data) ? document.extracted_data : {};
213
- for (const [category, codes] of Object.entries(extractedSource)) {
214
- if (!codes || typeof codes !== 'object' || Array.isArray(codes)) continue;
215
- extracted[category] = Object.fromEntries(
216
- Object.entries(codes).filter(([code]) => reverse[code])
217
- );
218
- }
219
- return {
220
- mapping,
221
- reverse_mapping: reverse,
222
- extracted_data: extracted,
223
- ignored: ignored.filter((entity) => !mapping[entity]),
224
- informations_dossier: normalizeProcedureInfo(document.informations_dossier),
225
- };
118
+ return { mapping, reverse_mapping: reverse };
226
119
  }
227
120
 
228
121
  /** L'ordre d'écriture suit `byDescendingEntityLength`. */
229
- function sortedMapping(mapping) {
230
- return Object.fromEntries(
231
- Object.entries(mapping).sort((a, b) => b[0].length - a[0].length || a[0].localeCompare(b[0], 'fr'))
232
- );
233
- }
234
-
235
122
  function readCaseMapping(caseRoot) {
236
123
  const file = caseMappingFile(caseRoot);
237
124
  const sourceFiles = existingMappingFiles(caseRoot);
@@ -239,15 +126,10 @@ function readCaseMapping(caseRoot) {
239
126
  return { file, sourceFiles: [], exists: false, ...normalizeMappingDocument(null) };
240
127
  }
241
128
 
242
- // Migration non destructive à la lecture : tous les anciens mappings sont
243
- // réunis en mémoire. Le canonique passe en dernier et gagne donc si une même
244
- // entité a été recodée. Le prochain enregistrement écrira l'ensemble dans le
245
- // seul `mapping_default.json`.
129
+ // Tous les anciens mappings sont réunis en mémoire. Le canonique passe en
130
+ // dernier et gagne donc si une même entité a été recodée.
246
131
  const mapping = {};
247
132
  const preferredVariants = {};
248
- const extracted_data = {};
249
- const ignored = [];
250
- let informations_dossier = normalizeProcedureInfo();
251
133
  const readableFiles = [];
252
134
  for (const sourceFile of sourceFiles) {
253
135
  const raw = readJsonFile(sourceFile, null);
@@ -255,18 +137,9 @@ function readCaseMapping(caseRoot) {
255
137
  readableFiles.push(sourceFile);
256
138
  const document = normalizeMappingDocument(raw);
257
139
  Object.assign(mapping, document.mapping);
258
- ignored.push(...document.ignored);
259
140
  for (const [code, variants] of Object.entries(document.reverse_mapping)) {
260
141
  preferredVariants[code] = [...new Set([...(preferredVariants[code] || []), ...variants])];
261
142
  }
262
- for (const [category, codes] of Object.entries(document.extracted_data)) {
263
- extracted_data[category] = { ...(extracted_data[category] || {}), ...codes };
264
- }
265
- if (path.basename(sourceFile) === CANONICAL_MAPPING_FILE
266
- || document.informations_dossier.parties_clientes.length
267
- || document.informations_dossier.parties_adverses.length) {
268
- informations_dossier = document.informations_dossier;
269
- }
270
143
  }
271
144
 
272
145
  const reverse_mapping = {};
@@ -281,7 +154,7 @@ function readCaseMapping(caseRoot) {
281
154
  file,
282
155
  sourceFiles: readableFiles,
283
156
  exists: readableFiles.length > 0,
284
- ...normalizeMappingDocument({ mapping, reverse_mapping, extracted_data, ignored, informations_dossier }),
157
+ ...normalizeMappingDocument({ mapping, reverse_mapping }),
285
158
  };
286
159
  }
287
160
 
@@ -302,15 +175,9 @@ module.exports = {
302
175
  applyMapping,
303
176
  buildEntityRegex,
304
177
  byDescendingEntityLength,
305
- caseMappingFile,
306
178
  escapeWithVariants,
307
- normalizeMappingDocument,
308
- normalizeProcedureInfo,
309
179
  readCaseMapping,
310
- readJsonFile,
311
180
  resolveProjectCaseMapping,
312
181
  resolveMappedPath,
313
182
  revertMapping,
314
- sortedMapping,
315
- CANONICAL_MAPPING_FILE,
316
183
  };
@@ -30,7 +30,7 @@ const PROTECTION_FILE = 'protection.json';
30
30
  const PROTECTION_BYPASS_FILE = 'protection-bypass.json';
31
31
 
32
32
  /**
33
- * Sous-dossier technique historique et emplacement du `mapping_default.json`.
33
+ * Sous-dossier technique historique (ancien emplacement du mapping JSON).
34
34
  * Le Markdown courant vit dans les sous-dossiers de conversion de
35
35
  * Correspondance et Data Room ; ce repli reste nécessaire aux anciens dossiers
36
36
  * et aux espaces de travail OOXML. `mapping.cjs` et `commits.cjs` l'importent.
@@ -1,66 +1,3 @@
1
- /**
2
- * Session timing helpers shared by the commit and billing hooks.
3
- *
4
- * A hook runs once per event with no memory of the session's start. The
5
- * transcript is the only durable clock: its first entry's `timestamp` is the
6
- * session start. `commit-track.mjs` needs that start on every Write/Edit to
7
- * stamp the elapsed session time into the commit, so reading the whole
8
- * transcript on each tool call would be wasteful — we only need the first
9
- * timestamp, so we scan a bounded head of the file.
10
- *
11
- * Everything here fails soft: an unreadable/empty/malformed transcript yields
12
- * `null`, and the caller simply omits the time trailer.
13
- */
14
- const fs = require('node:fs');
15
-
16
- // Enough to comfortably contain the first JSONL record even with a large
17
- // system prompt echoed into it; we stop at the first line that parses.
18
- const HEAD_BYTES = 256 * 1024;
19
-
20
- /** First ISO timestamp in the transcript (session start), or null. */
21
- function readSessionStartTimestamp(transcriptPath) {
22
- if (!transcriptPath) return null;
23
- let fd;
24
- try {
25
- fd = fs.openSync(transcriptPath, 'r');
26
- const stat = fs.fstatSync(fd);
27
- const length = Math.min(stat.size, HEAD_BYTES);
28
- if (length === 0) return null;
29
- const buffer = Buffer.allocUnsafe(length);
30
- const bytesRead = fs.readSync(fd, buffer, 0, length, 0);
31
- const text = buffer.subarray(0, bytesRead).toString('utf8');
32
- for (const line of text.split('\n')) {
33
- const trimmed = line.trim();
34
- if (!trimmed) continue;
35
- // The last line of a partial head read may be truncated; ignore parse
36
- // failures and keep looking for the first fully-formed record.
37
- let entry;
38
- try {
39
- entry = JSON.parse(trimmed);
40
- } catch {
41
- continue;
42
- }
43
- const ts = entry?.timestamp || entry?.message?.timestamp;
44
- if (ts && Number.isFinite(Date.parse(ts))) return ts;
45
- }
46
- return null;
47
- } catch {
48
- return null;
49
- } finally {
50
- if (fd !== undefined) {
51
- try { fs.closeSync(fd); } catch {}
52
- }
53
- }
54
- }
55
-
56
- /** Milliseconds elapsed between the session's first entry and `now`. */
57
- function sessionElapsedMs(transcriptPath, now = Date.now()) {
58
- const start = readSessionStartTimestamp(transcriptPath);
59
- if (!start) return null;
60
- const ms = now - Date.parse(start);
61
- return Number.isFinite(ms) && ms >= 0 ? ms : null;
62
- }
63
-
64
1
  /** Compact French duration, e.g. "12 min 30 s", "1 h 05 min", "8 s". */
65
2
  function formatDurationFr(ms) {
66
3
  if (!Number.isFinite(ms) || ms < 0) return null;
@@ -75,6 +12,4 @@ function formatDurationFr(ms) {
75
12
 
76
13
  module.exports = {
77
14
  formatDurationFr,
78
- readSessionStartTimestamp,
79
- sessionElapsedMs,
80
15
  };