@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 +13 -0
- package/llms.txt +21 -0
- package/package.json +31 -0
- package/src/index.js +95 -0
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 };
|