@mostajs/aop-classify 0.1.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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @mostajs/aop-classify
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
4
+
5
+ Classification d'**appels d'offres** — **logique pure** (zéro I/O), extraite d'AgoraScope.
6
+ Température `hot/warm/cold/frozen` (par échéance), **score d'urgence** et **score de pertinence** multi-facteurs.
7
+
8
+ ```js
9
+ import { classify, temperatureEmoji } from '@mostajs/aop-classify';
10
+ const c = classify({ deadline, value, procedure, views, bookmarks });
11
+ // { temperature:'hot', urgency:80, score:74, scoreTemperature:'warm' }
12
+ ```
13
+ Tests/§12 : `npm install && npm test`. Stack : `mosta-aop-stack`.
package/llms.txt ADDED
@@ -0,0 +1,21 @@
1
+ # @mostajs/aop-classify — fiche LLM
2
+ > Classification d'appels d'offres (LOGIQUE PURE, zéro I/O) : température, urgence, score de pertinence.
3
+
4
+ - Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
5
+ - Stack: mosta-aop-stack · Extrait d'AgoraScope (temperature-classifier.ts + Tender.calculateScore)
6
+
7
+ ## RÔLE
8
+ Brique PURE (aucune dépendance, aucune I/O) consommée par aop-core / aop-ingestion / aop-ui-html.
9
+
10
+ ## EXPORTS
11
+ - classifyTemperature(deadline, now?, thresholds?) -> 'hot'|'warm'|'cold'|'frozen' (par échéance ; null→cold, passée→frozen, <30j hot, <90j warm)
12
+ - urgencyScore(deadline, now?) -> 0..100 (inverse des jours restants)
13
+ - computeScore({deadline,value,procedure,views,bookmarks}, now?) -> 0..100 (multi-facteurs)
14
+ - scoreToTemperature(score) -> Temperature (≥80 hot, ≥60 warm, ≥30 cold, sinon frozen)
15
+ - classify(t, now?, thresholds?) -> { temperature, urgency, score, scoreTemperature }
16
+ - temperatureColor/Emoji/Label/Description(t) ; DEFAULT_THRESHOLDS {hot:30,warm:90}
17
+
18
+ ## PIÈGES
19
+ - DEUX notions de température : par ÉCHÉANCE (classifyTemperature, canonique) vs dérivée du SCORE (scoreToTemperature).
20
+ - Seuils EXCLUSIFS : 30 j → warm (pas hot), 90 j → cold.
21
+ - procedure ∈ open|restricted|negotiated|competitive_dialogue|innovation_partnership.
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@mostajs/aop-classify",
3
+ "version": "0.1.0",
4
+ "description": "Classification d'appels d'offres (logique pure) : température hot/warm/cold/frozen, score d'urgence, score de pertinence multi-facteurs. Extrait d'AgoraScope.",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md",
15
+ "llms.txt"
16
+ ],
17
+ "keywords": [
18
+ "appel-offres",
19
+ "tender",
20
+ "classification",
21
+ "temperature",
22
+ "scoring",
23
+ "mostajs"
24
+ ],
25
+ "devDependencies": {
26
+ "@mostajs/mjs-unit": "^0.3.0"
27
+ },
28
+ "scripts": {
29
+ "test": "node test-scripts/unit/aop-classify.test.mjs && node examples/run.mjs"
30
+ }
31
+ }
package/src/index.js ADDED
@@ -0,0 +1,95 @@
1
+ // @mostajs/aop-classify — classification d'appels d'offres (LOGIQUE PURE, zéro I/O).
2
+ // Extrait d'AgoraScope (src/lib/ingestion/temperature-classifier.ts + Tender.calculateScore).
3
+ // Température hot/warm/cold/frozen (par échéance), score d'urgence, score de pertinence multi-facteurs.
4
+ // @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
5
+
6
+ /** @typedef {'hot'|'warm'|'cold'|'frozen'} Temperature */
7
+
8
+ const DAY_MS = 86400000;
9
+ export const DEFAULT_THRESHOLDS = { hot: 30, warm: 90 }; // jours
10
+
11
+ const toDate = (d) => (d == null ? null : d instanceof Date ? d : new Date(d));
12
+ const daysUntil = (deadline, now) => Math.floor((deadline.getTime() - now.getTime()) / DAY_MS);
13
+
14
+ /**
15
+ * Température d'un AO selon l'échéance.
16
+ * pas d'échéance → cold · échéance passée → frozen · <hot j → hot · <warm j → warm · sinon cold.
17
+ * @param {Date|string|null} deadline
18
+ * @param {Date} [now]
19
+ * @param {{hot:number,warm:number}} [thresholds]
20
+ * @returns {Temperature}
21
+ */
22
+ export function classifyTemperature(deadline, now = new Date(), thresholds = DEFAULT_THRESHOLDS) {
23
+ const d = toDate(deadline);
24
+ if (!d || isNaN(d.getTime())) return 'cold';
25
+ if (d < now) return 'frozen';
26
+ const days = daysUntil(d, now);
27
+ if (days < thresholds.hot) return 'hot';
28
+ if (days < thresholds.warm) return 'warm';
29
+ return 'cold';
30
+ }
31
+
32
+ /** Score d'urgence 0..100 (inverse du nb de jours restants ; 0 si échéance absente/passée). */
33
+ export function urgencyScore(deadline, now = new Date()) {
34
+ const d = toDate(deadline);
35
+ if (!d || isNaN(d.getTime()) || d < now) return 0;
36
+ const days = daysUntil(d, now);
37
+ if (days <= 1) return 100;
38
+ if (days <= 7) return 90;
39
+ if (days <= 14) return 80;
40
+ if (days <= 30) return 70;
41
+ if (days <= 60) return 50;
42
+ if (days <= 90) return 30;
43
+ return 10;
44
+ }
45
+
46
+ const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
47
+
48
+ /**
49
+ * Score de pertinence 0..100 (multi-facteurs : échéance, valeur, procédure, engagement).
50
+ * @param {object} t { deadline, value?, procedure?, views?, bookmarks? }
51
+ * @param {Date} [now]
52
+ */
53
+ export function computeScore(t = {}, now = new Date()) {
54
+ const { deadline, value = 0, procedure = '', views = 0, bookmarks = 0 } = t;
55
+ let score = 0;
56
+ // Échéance (fenêtre de réponse)
57
+ const d = toDate(deadline);
58
+ const days = d && !isNaN(d.getTime()) ? daysUntil(d, now) : -1;
59
+ if (days > 30) score += 25; else if (days > 15) score += 20; else if (days > 7) score += 15; else if (days > 3) score += 10; else score += 5;
60
+ // Valeur estimée
61
+ if (value > 500000) score += 25; else if (value > 100000) score += 20; else if (value > 50000) score += 15; else if (value > 10000) score += 10; else score += 5;
62
+ // Procédure
63
+ score += ({ open: 25, restricted: 20, competitive_dialogue: 15, negotiated: 10, innovation_partnership: 20 }[procedure]) || 0;
64
+ // Engagement
65
+ score += Math.min(25, views * 0.1 + bookmarks * 2);
66
+ return clamp(Math.round(score), 0, 100);
67
+ }
68
+
69
+ /** Température dérivée d'un score de pertinence (≥80 hot, ≥60 warm, ≥30 cold, sinon frozen). */
70
+ export function scoreToTemperature(score) {
71
+ if (score >= 80) return 'hot';
72
+ if (score >= 60) return 'warm';
73
+ if (score >= 30) return 'cold';
74
+ return 'frozen';
75
+ }
76
+
77
+ const COLORS = { hot: '#EF4444', warm: '#F97316', cold: '#3B82F6', frozen: '#9CA3AF' };
78
+ const EMOJIS = { hot: '🔴', warm: '🟠', cold: '🔵', frozen: '⬜' };
79
+ export const temperatureColor = (t) => COLORS[t] || '#6B7280';
80
+ export const temperatureEmoji = (t) => EMOJIS[t] || '⚪';
81
+ export const temperatureLabel = (t) => `tender.temperature.${t}`; // clé i18n
82
+ export const temperatureDescription = (t) => `tender.temperature.${t}.description`;
83
+
84
+ /** Classification complète d'un AO : { temperature, urgency, score, scoreTemperature }. */
85
+ export function classify(t = {}, now = new Date(), thresholds = DEFAULT_THRESHOLDS) {
86
+ const score = computeScore(t, now);
87
+ return {
88
+ temperature: classifyTemperature(t.deadline, now, thresholds),
89
+ urgency: urgencyScore(t.deadline, now),
90
+ score,
91
+ scoreTemperature: scoreToTemperature(score),
92
+ };
93
+ }
94
+
95
+ export default { classifyTemperature, urgencyScore, computeScore, scoreToTemperature, classify, temperatureColor, temperatureEmoji, temperatureLabel, temperatureDescription, DEFAULT_THRESHOLDS };