mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
fix(qlik): ne pas amputer les faits avec une Période partielle + valider chaque expression
Deux constats de la dernière sync.
1. AUCUNE valeur de « Période » ne porte 12 mois glissants — c'est mesuré :
« juin 2026 » 1/12, « Année à date » 6/12, « Mois à date » 0/12,
« Semaine 2026/30 » 0/12
Ce sont 4 contextes relatifs à aujourd'hui, pas des types de période. Les
master measures ne pourront donc jamais couvrir la fenêtre, et l'agrégation
directe des faits devient obligatoire.
D'où un bug du commit précédent : garder la meilleure Période (6/12) RESTREINT
les faits à 2026-01→06 et ampute l'agrégation directe, qui sait pourtant lire
tout l'historique. choisirPeriode() efface désormais la sélection quand la
couverture est incomplète, et ne la garde que si elle couvre les 12 mois.
2. Le cube à 5 mesures calculait 26 s puis rendait 0 ligne, alors que
Sum(quantite) seul donnait 35 424 324 : signature d'une mesure invalide qui
invalide tout l'hypercube, sans qu'aucun message ne dise laquelle.
validerExpression() teste maintenant chaque expression isolément sur un petit
cube [Mois] avant de construire le gros. Une expression invalide est remplacée
par la colonne neutre 0 — le cube reste exploitable et le log nomme la
coupable. Seule la quantité est bloquante.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
2 files changed
+127
-29
No files matched your search
+36
-13
@@ -103,24 +103,47 @@ Tout ce qu'on observait en découle :
|
||||
| Janvier→juillet de l'année précédente renseignés | Ce sont les mois que « Quantité COMP » sait atteindre |
|
||||
| `Sum(quantite)` brut inexploitable | Il additionne les lignes N **et** COMP : il double compte |
|
||||
|
||||
La solution n'est donc pas de contourner le modèle mais de **le piloter** :
|
||||
`choisirPeriode()` essaie chaque valeur de `Période`, mesure sur un cube
|
||||
`[Mois] × Quantité N` combien de mois de la fenêtre elle rend réellement
|
||||
disponibles, et garde la meilleure. Aucun libellé n'est deviné — c'est la
|
||||
couverture mesurée qui décide :
|
||||
### Mesuré : aucune `Période` ne porte 12 mois glissants
|
||||
|
||||
`choisirPeriode()` essaie chaque valeur et mesure la couverture réelle sur un
|
||||
cube `[Mois] × Quantité N`. Résultat en production :
|
||||
|
||||
```
|
||||
[qlik-pw][diag] valeurs de « Période » : ["Année en cours","12 mois glissants","Mois","Semaine"]
|
||||
[qlik-pw][diag] Période « Année en cours » → 7/12 mois de la fenêtre [...]
|
||||
[qlik-pw][diag] Période « 12 mois glissants » → 12/12 mois de la fenêtre [...]
|
||||
[qlik-pw][diag] Période retenue : « 12 mois glissants » (12/12 mois)
|
||||
valeurs de « Période » : ["juin 2026","Année à date","Mois à date","Semaine 2026/30"]
|
||||
« juin 2026 » → 1/12 ["2026-06"]
|
||||
« Année à date » → 6/12 ["2026-01".."2026-06"]
|
||||
« Mois à date » → 0/12
|
||||
« Semaine 2026/30 » → 0/12
|
||||
```
|
||||
|
||||
Si aucune valeur ne couvre les 12 mois, l'extraction le dit et les mois manquants
|
||||
restent **absents** du cache — jamais remplis d'une valeur plausible.
|
||||
Ce ne sont pas des types de période mais **4 contextes relatifs à aujourd'hui**.
|
||||
Aucun ne porte 12 mois glissants : les master measures ne pourront **jamais**
|
||||
couvrir la fenêtre. Ce n'est plus une hypothèse, c'est une mesure.
|
||||
|
||||
`QLIK_USE_EXPR=1` réactive l'ancien essai par agrégation directe des faits ; il
|
||||
est désactivé par défaut puisque `Sum(quantite)` double compte.
|
||||
Conséquence directe : **l'agrégation directe des faits est obligatoire**, et une
|
||||
`Période` partielle est pire que pas de sélection — « Année à date » restreint les
|
||||
faits à 2026-01→06 et amputerait d'autant l'agrégation, qui sait lire tout
|
||||
l'historique. `choisirPeriode()` **efface** donc la sélection quand la couverture
|
||||
est incomplète, et ne la garde que si elle couvre les 12 mois.
|
||||
|
||||
### Chaque expression est validée séparément
|
||||
|
||||
Une seule mesure invalide suffit à faire renvoyer **zéro ligne** à tout un
|
||||
hypercube, sans qu'aucun message ne dise laquelle : observé en production, le
|
||||
cube à 5 mesures calculait 26 s puis rendait 0 ligne, alors que `Sum(quantite)`
|
||||
seul donnait 35 424 324.
|
||||
|
||||
`validerExpression()` teste donc chaque expression isolément sur un petit cube
|
||||
`[Mois]` avant de construire le gros :
|
||||
|
||||
```
|
||||
[qlik-pw][diag] validation des expressions, une par une :
|
||||
[qlik-pw][diag] mesure « quantité » = Sum(quantite) → 12 mois, total=33945285
|
||||
[qlik-pw][diag] mesure « magasins » = Count(DISTINCT [Magasin Code]) → INVALIDE (cube vide)
|
||||
```
|
||||
|
||||
Une expression invalide est remplacée par la colonne neutre `0` : le cube reste
|
||||
exploitable et le log nomme la coupable. Seule la quantité est bloquante.
|
||||
|
||||
## ⚠️ Les master measures « N » ignorent la sélection Date
|
||||
|
||||
|
||||
+91
-16
@@ -529,6 +529,10 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// les champs de faits, qui eux respectent les sélections.
|
||||
const PAGEX = 1200; // 7 colonnes × 1200 = 8400 cellules < 10000
|
||||
const fetchMonthCubeExpr = async (
|
||||
eCa: string,
|
||||
eQte: string,
|
||||
eNbMag: string,
|
||||
eMarge: string,
|
||||
onRow: (code: string, mois: string, ca: number, qte: number, nbMag: number, marge: number, qteMaster: number) => void,
|
||||
timings?: Record<string, number>,
|
||||
onCode15Retry?: () => void,
|
||||
@@ -536,10 +540,10 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
const { value: obj } = await timed("createCubeExpr", "CreateSessionObject", () => rpcWithRetry("createCubeExpr", "CreateSessionObject", { qProp: { qInfo: { qType: "cf-net-mois-expr" }, qHyperCubeDef: {
|
||||
qDimensions: [{ qLibraryId: dim }, { qLibraryId: moisDimId }],
|
||||
qMeasures: [
|
||||
{ qDef: { qDef: exprCa } },
|
||||
{ qDef: { qDef: exprQte } },
|
||||
{ qDef: { qDef: exprNbMag } },
|
||||
{ qDef: { qDef: exprMarge } },
|
||||
{ qDef: { qDef: eCa } },
|
||||
{ qDef: { qDef: eQte } },
|
||||
{ qDef: { qDef: eNbMag } },
|
||||
{ qDef: { qDef: eMarge } },
|
||||
// Master « Quantité N », uniquement pour le calibrage : sur les
|
||||
// mois de l'année en cours les deux doivent coïncider.
|
||||
{ qLibraryId: rQte },
|
||||
@@ -829,10 +833,24 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
if (couverts.length >= fenetre.size) break; // couverture complète, inutile de continuer
|
||||
}
|
||||
|
||||
if (!meilleure || meilleure.couverts.length === 0) {
|
||||
diag("aucune valeur de « Période » ne couvre la fenêtre — extraction sur la période par défaut");
|
||||
return 0;
|
||||
const couverture = meilleure ? meilleure.couverts.length : 0;
|
||||
|
||||
// Une `Période` partielle est PIRE que pas de sélection : elle
|
||||
// restreint les faits au contexte choisi (« Année à date » ne
|
||||
// porte que 2026-01→06) et ampute d'autant l'agrégation
|
||||
// directe, qui elle sait lire tout l'historique. On ne garde
|
||||
// donc la sélection QUE si elle couvre toute la fenêtre.
|
||||
if (!meilleure || couverture < fenetre.size) {
|
||||
await rpc("ClearField", { qFieldName: "Période" }, doc)
|
||||
.catch(() => rpc("Clear", {}, lo!.qHandle));
|
||||
await sleep(qlikSettleMs);
|
||||
diag(
|
||||
"aucune valeur de « Période » ne couvre les " + fenetre.size + " mois (meilleure = " +
|
||||
couverture + ") → sélection Période EFFACÉE pour ne pas amputer les faits",
|
||||
);
|
||||
return couverture;
|
||||
}
|
||||
|
||||
await rpc("SelectListObjectValues", {
|
||||
qPath: "/qListObjectDef",
|
||||
qValues: [meilleure.elem],
|
||||
@@ -840,14 +858,8 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
qSoftLock: true,
|
||||
}, lo.qHandle);
|
||||
await sleep(qlikSettleMs);
|
||||
diag("Période retenue : « " + meilleure.texte + " » (" + meilleure.couverts.length + "/" + fenetre.size + " mois)");
|
||||
if (meilleure.couverts.length < fenetre.size) {
|
||||
diag(
|
||||
"⚠ couverture incomplète : les mois manquants resteront ABSENTS du cache. " +
|
||||
"Aucune valeur de « Période » ne porte 12 mois glissants dans cette app.",
|
||||
);
|
||||
}
|
||||
return meilleure.couverts.length;
|
||||
diag("Période retenue : « " + meilleure.texte + " » (" + couverture + "/" + fenetre.size + " mois)");
|
||||
return couverture;
|
||||
} catch (e) {
|
||||
diag("choix de « Période » impossible : " + String((e as Error)?.message || e));
|
||||
return 0;
|
||||
@@ -858,6 +870,56 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Teste UNE expression isolément sur un petit cube `[Mois]`.
|
||||
*
|
||||
* Une seule mesure invalide suffit à faire renvoyer **zéro ligne** à
|
||||
* tout un hypercube — observé en production : le cube à 5 mesures
|
||||
* calculait 26 s puis rendait 0 ligne, alors que `Sum(quantite)` seul
|
||||
* donnait 35 424 324. Impossible de savoir laquelle sans les isoler.
|
||||
*
|
||||
* Renvoie l'expression si elle produit des lignes, sinon `"0"` — une
|
||||
* colonne neutre qui préserve les indices sans casser le cube.
|
||||
*/
|
||||
const validerExpression = async (nom: string, expr: string): Promise<string> => {
|
||||
let ret: { qHandle: number; qGenericId?: string; qId?: string } | null = null;
|
||||
try {
|
||||
const o = await rpc("CreateSessionObject", { qProp: {
|
||||
qInfo: { qType: "cf-test-expr" },
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [{ qLibraryId: moisDimId }],
|
||||
qMeasures: [{ qDef: { qDef: expr } }],
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 2, qHeight: 30 }],
|
||||
},
|
||||
} }, doc);
|
||||
ret = o.qReturn as { qHandle: number; qGenericId?: string; qId?: string };
|
||||
const lay = await rpc("GetLayout", {}, ret.qHandle);
|
||||
const hc = (lay.qLayout as { qHyperCube?: { qSize?: { qcy?: number }; qError?: unknown; qDataPages?: Array<{ qMatrix?: Array<Array<{ qText?: string; qNum?: number }>> }> } }).qHyperCube;
|
||||
const matrix = hc?.qDataPages?.[0]?.qMatrix ?? [];
|
||||
const lignes = Number(hc?.qSize?.qcy) || 0;
|
||||
let total = 0;
|
||||
let texteErreur = "";
|
||||
for (const r of matrix) {
|
||||
total += Number(r[1]?.qNum) || 0;
|
||||
const t = String(r[1]?.qText ?? "");
|
||||
if (!texteErreur && /error|erreur|garbage|^-$/i.test(t)) texteErreur = t;
|
||||
}
|
||||
if (hc?.qError) texteErreur = texteErreur || JSON.stringify(hc.qError);
|
||||
if (lignes === 0 || texteErreur) {
|
||||
diag(" mesure « " + nom + " » = " + expr + " → INVALIDE (" + (texteErreur || "cube vide") + ")");
|
||||
return "0";
|
||||
}
|
||||
diag(" mesure « " + nom + " » = " + expr + " → " + lignes + " mois, total=" + Math.round(total));
|
||||
return expr;
|
||||
} catch (e) {
|
||||
diag(" mesure « " + nom + " » = " + expr + " → ERREUR " + String((e as Error)?.message || e));
|
||||
return "0";
|
||||
} finally {
|
||||
const id = String(ret?.qGenericId ?? ret?.qId ?? "");
|
||||
if (id) { try { await rpc("DestroySessionObject", { qId: id }, doc); } catch { /* noop */ } }
|
||||
}
|
||||
};
|
||||
|
||||
const moisParExpressions = async (): Promise<boolean> => {
|
||||
const timings: Record<string, number> = {};
|
||||
const marqueOut = out.length;
|
||||
@@ -937,6 +999,19 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}
|
||||
diag("champ date retenu : « " + champDate + " »");
|
||||
|
||||
// Chaque expression est validée SÉPARÉMENT : une seule mesure
|
||||
// invalide fait rendre zéro ligne à tout l'hypercube, sans
|
||||
// qu'aucun message ne dise laquelle.
|
||||
diag("validation des expressions, une par une :");
|
||||
const vQte = await validerExpression("quantité", exprQte);
|
||||
if (vQte === "0") {
|
||||
diag("ÉCHEC : l'expression quantité est inutilisable, rien à extraire");
|
||||
return false;
|
||||
}
|
||||
const vCa = await validerExpression("CA", exprCa);
|
||||
const vNbMag = await validerExpression("magasins", exprNbMag);
|
||||
const vMarge = await validerExpression("marge", exprMarge);
|
||||
|
||||
// Seuls les mois de la fenêtre nous intéressent ; la dimension
|
||||
// Mois peut en exposer d'autres si une sélection déborde.
|
||||
let totalQte = 0;
|
||||
@@ -944,7 +1019,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
let horsFenetre = 0;
|
||||
let echantillon = false;
|
||||
|
||||
const got = await timed("cube", "expr", () => fetchMonthCubeExpr((code, mois, ca, qte, nbMag, marge, qteMaster) => {
|
||||
const got = await timed("cube", "expr", () => fetchMonthCubeExpr(vCa, vQte, vNbMag, vMarge, (code, mois, ca, qte, nbMag, marge, qteMaster) => {
|
||||
if (!code || code === "-") return;
|
||||
const mm = normMois(mois);
|
||||
if (!echantillon) {
|
||||
|
||||
Reference in new issue
Block a user