@tricoteuses/senat 3.2.0 → 3.2.2

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 (112) hide show
  1. package/LICENSE.md +32 -32
  2. package/README.md +326 -326
  3. package/lib/src/config.d.ts +43 -0
  4. package/lib/src/config.js +37 -0
  5. package/lib/src/conversion_textes.d.ts +11 -0
  6. package/lib/src/conversion_textes.js +320 -0
  7. package/lib/src/databases_postgres.d.ts +4 -0
  8. package/lib/src/databases_postgres.js +23 -0
  9. package/lib/src/datasets.d.ts +38 -0
  10. package/lib/src/datasets.js +247 -0
  11. package/lib/src/git.d.ts +27 -0
  12. package/lib/src/git.js +251 -0
  13. package/lib/src/index.d.ts +1 -1
  14. package/lib/src/loaders.d.ts +52 -0
  15. package/lib/src/loaders.js +260 -0
  16. package/lib/src/model/agenda.d.ts +6 -0
  17. package/lib/src/model/agenda.js +148 -0
  18. package/lib/src/model/ameli.d.ts +67 -0
  19. package/lib/src/model/ameli.js +150 -0
  20. package/lib/src/model/commission.d.ts +19 -0
  21. package/lib/src/model/commission.js +269 -0
  22. package/lib/src/model/debats.d.ts +39 -0
  23. package/lib/src/model/debats.js +112 -0
  24. package/lib/src/model/documents.d.ts +32 -0
  25. package/lib/src/model/documents.js +182 -0
  26. package/lib/src/model/dosleg.d.ts +144 -0
  27. package/lib/src/model/dosleg.js +468 -0
  28. package/lib/src/model/index.d.ts +7 -0
  29. package/lib/src/model/index.js +7 -0
  30. package/lib/src/model/questions.d.ts +54 -0
  31. package/lib/src/model/questions.js +91 -0
  32. package/lib/src/model/scrutins.d.ts +48 -0
  33. package/lib/src/model/scrutins.js +121 -0
  34. package/lib/src/model/seance.d.ts +3 -0
  35. package/lib/src/model/seance.js +267 -0
  36. package/lib/src/model/sens.d.ts +112 -0
  37. package/lib/src/model/sens.js +385 -0
  38. package/lib/src/model/util.d.ts +1 -0
  39. package/lib/src/model/util.js +15 -0
  40. package/lib/src/parsers/plf/amendement.d.ts +3 -0
  41. package/lib/src/parsers/plf/amendement.js +152 -0
  42. package/lib/src/raw_types/ameli.d.ts +1762 -0
  43. package/lib/src/raw_types/ameli.js +1074 -0
  44. package/lib/src/raw_types/debats.d.ts +380 -0
  45. package/lib/src/raw_types/debats.js +266 -0
  46. package/lib/src/raw_types/dosleg.d.ts +2954 -0
  47. package/lib/src/raw_types/dosleg.js +2005 -0
  48. package/lib/src/raw_types/questions.d.ts +699 -0
  49. package/lib/src/raw_types/questions.js +493 -0
  50. package/lib/src/raw_types/sens.d.ts +7843 -0
  51. package/lib/src/raw_types/sens.js +4691 -0
  52. package/lib/src/raw_types_schemats/ameli.d.ts +541 -0
  53. package/lib/src/raw_types_schemats/ameli.js +2 -0
  54. package/lib/src/raw_types_schemats/debats.d.ts +127 -0
  55. package/lib/src/raw_types_schemats/debats.js +2 -0
  56. package/lib/src/raw_types_schemats/dosleg.d.ts +977 -0
  57. package/lib/src/raw_types_schemats/dosleg.js +2 -0
  58. package/lib/src/raw_types_schemats/questions.d.ts +237 -0
  59. package/lib/src/raw_types_schemats/questions.js +2 -0
  60. package/lib/src/raw_types_schemats/sens.d.ts +2709 -0
  61. package/lib/src/raw_types_schemats/sens.js +2 -0
  62. package/lib/src/rich_types/ameli.d.ts +12 -0
  63. package/lib/src/scripts/debug_dosleg_query.d.ts +6 -0
  64. package/lib/src/scripts/debug_dosleg_query.js +50 -0
  65. package/lib/src/scripts/retrieve_collaborateurs.js +6 -3
  66. package/lib/src/scripts/retrieve_open_data.js +2 -1
  67. package/lib/src/scripts/shared/incremental_import_sql.js +866 -866
  68. package/lib/src/scripts/shared/schema_version.js +85 -84
  69. package/lib/src/scripts/shared/staging_metadata_sql.js +214 -214
  70. package/lib/src/scripts/validate_prefixed_tables.js +12 -12
  71. package/lib/src/server/ameli.js +18 -11
  72. package/lib/src/server/conversion_textes.js +106 -106
  73. package/lib/src/server/datasets.js +1 -0
  74. package/lib/src/server/debats.js +10 -10
  75. package/lib/src/server/documents.js +22 -22
  76. package/lib/src/server/dosleg.js +33 -33
  77. package/lib/src/server/questions.js +10 -10
  78. package/lib/src/server/scrutins.js +3 -3
  79. package/lib/src/server/sens.js +19 -19
  80. package/lib/src/types/agenda.d.ts +45 -0
  81. package/lib/src/types/ameli.d.ts +5 -0
  82. package/lib/src/types/ameli.js +1 -0
  83. package/lib/src/types/compte_rendu.d.ts +83 -0
  84. package/lib/src/types/compte_rendu.js +1 -0
  85. package/lib/src/types/debats.d.ts +2 -0
  86. package/lib/src/types/debats.js +1 -0
  87. package/lib/src/types/dosleg.d.ts +70 -0
  88. package/lib/src/types/dosleg.js +1 -0
  89. package/lib/src/types/questions.d.ts +2 -0
  90. package/lib/src/types/questions.js +1 -0
  91. package/lib/src/types/sens.d.ts +8 -0
  92. package/lib/src/types/sens.js +1 -0
  93. package/lib/src/types/sessions.d.ts +6 -0
  94. package/lib/src/types/sessions.js +19 -0
  95. package/lib/src/types/texte.d.ts +72 -0
  96. package/lib/src/types/texte.js +15 -0
  97. package/lib/src/validators/config.d.ts +9 -0
  98. package/lib/src/validators/config.js +10 -0
  99. package/lib/src/videos/match.js +4 -4
  100. package/lib/tests/parsers/plf/amendement.test.d.ts +1 -0
  101. package/lib/tests/parsers/plf/amendement.test.js +153 -0
  102. package/lib/tests/parsers/plf/credits.test.js +72 -72
  103. package/lib/tests/parsers/plf/test_utils.js +13 -13
  104. package/lib/tests/parsers/texte.test_utils.js +22 -22
  105. package/lib/tests/videoMatching.test.js +1 -1
  106. package/package.json +123 -123
  107. package/lib/src/scripts/extract_plf_fixtures.d.ts +0 -1
  108. package/lib/src/scripts/extract_plf_fixtures.js +0 -101
  109. package/lib/tests/plf/credits.test.js +0 -458
  110. package/lib/tests/plf/test_utils.d.ts +0 -20
  111. package/lib/tests/plf/test_utils.js +0 -85
  112. /package/lib/{tests/plf/credits.test.d.ts → src/types/agenda.js} +0 -0
@@ -0,0 +1,83 @@
1
+ export interface CompteRendu {
2
+ uid: string;
3
+ seanceRef: string;
4
+ sessionRef: string;
5
+ metadonnees: Metadonnees;
6
+ contenu: Contenu;
7
+ }
8
+ export interface Metadonnees {
9
+ dateSeance: string;
10
+ dateSeanceJour: string;
11
+ numSeanceJour: string;
12
+ numSeance: string;
13
+ typeAssemblee: "AN" | "SN";
14
+ legislature: string;
15
+ session: string;
16
+ nomFichierJo: string;
17
+ validite: string;
18
+ etat: string;
19
+ diffusion: string;
20
+ version: string;
21
+ environnement: string;
22
+ heureGeneration: Date;
23
+ sommaire?: Sommaire;
24
+ }
25
+ export interface Contenu {
26
+ quantiemes: Quantiemes;
27
+ ouvertureSeance?: Point[] | Point;
28
+ point: Point[] | Point;
29
+ finSeance?: FinSeance;
30
+ paragraphe?: Point[];
31
+ }
32
+ export interface FinSeance {
33
+ point: Point;
34
+ }
35
+ export interface Quantiemes {
36
+ journee: string;
37
+ session: string;
38
+ }
39
+ export interface Point {
40
+ ordre_absolu_seance: string;
41
+ code_grammaire: string;
42
+ roledebat?: string;
43
+ orateurs?: {
44
+ orateur: {
45
+ nom: string;
46
+ id: string;
47
+ qualite: string;
48
+ };
49
+ };
50
+ texte: {
51
+ _: string;
52
+ };
53
+ code_style?: string;
54
+ }
55
+ export interface Texte {
56
+ _?: string;
57
+ id_syceron?: string;
58
+ stime?: string;
59
+ sup?: string;
60
+ lienAdt?: Texte[] | Texte;
61
+ }
62
+ export interface Sommaire {
63
+ presidentSeance: Texte;
64
+ sommaire1: SommaireElement[] | SommaireElement;
65
+ sommaire3?: SommaireElement[] | SommaireElement;
66
+ sommaire2?: SommaireElement[] | SommaireElement;
67
+ para?: Texte[] | Texte;
68
+ }
69
+ export interface SommaireElement {
70
+ valeur_pts_odj: string | undefined;
71
+ titreStruct: TitreStruct;
72
+ para?: Array<Texte | string> | Texte;
73
+ sommaire2?: SommaireElement[] | SommaireElement;
74
+ sommaire3?: SommaireElement[] | SommaireElement;
75
+ presidentSeance?: Texte[] | Texte;
76
+ type_debat?: string;
77
+ }
78
+ export interface TitreStruct {
79
+ id_syceron: string;
80
+ intitule?: string;
81
+ sousIntitule?: string;
82
+ type_debat?: string;
83
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { debats as Debat, lecassdeb as LecAssDeb } from "../raw_types_schemats/debats";
2
+ export type { Debat, LecAssDeb };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { Ass, Aud as AudRaw, Auteur as AuteurRaw, DateSeance as DateSeanceRaw, Deccoc, Denrap, Docatt as DocattRaw, Ecr as EcrRaw, Etaloi, Lecass as LecassRaw, Lecassrap as LecassrapRaw, Lecture as LectureRaw, Loi as LoiRaw, Org, Oritxt, Qua, Rap as RapRaw, Raporg, Scr, Texte as TexteRaw, Typatt, Typlec, Typloi, Typtxt, Typurl } from "../raw_types/dosleg.js";
2
+ import { TxtAmeli } from "../raw_types/ameli.js";
3
+ import { Debats } from "../raw_types/debats.js";
4
+ import { TxtAmeliCustom } from "./ameli.js";
5
+ export type { Deccoc as DecCoc, Denrap as DenRap, Etaloi as EtaLoi, Oritxt as OriTxt, Raporg as RapOrg, Typatt as TypAtt, Typlec as TypLec, Typloi as TypLoi, Typtxt as TypTxt, Typurl as TypUrl, };
6
+ export interface Aud extends AudRaw {
7
+ org?: Org;
8
+ }
9
+ export interface Auteur extends AuteurRaw {
10
+ qua?: Qua;
11
+ }
12
+ export interface DateSeance extends DateSeanceRaw {
13
+ debat?: Debats;
14
+ scrids?: string[];
15
+ scrs?: Scr[];
16
+ }
17
+ export interface DocAtt extends DocattRaw {
18
+ rap?: RapRaw;
19
+ typatt?: Typatt;
20
+ }
21
+ export interface Ecr extends EcrRaw {
22
+ aut?: Auteur;
23
+ }
24
+ export interface LecAss extends LecassRaw {
25
+ ass?: Ass;
26
+ auds?: Aud[];
27
+ audcles?: AudRaw["audcle"][];
28
+ datesSeances?: DateSeance[];
29
+ datesSeancesCodes?: DateSeanceRaw["code"][];
30
+ debatdatseas: Date[];
31
+ lecassraps?: LecAssRap[];
32
+ lecassrapids?: string[];
33
+ org?: Org;
34
+ texcods: TexteRaw["texcod"][];
35
+ textes?: Texte[];
36
+ }
37
+ export interface LecAssRap extends LecassrapRaw {
38
+ rap?: RapRaw;
39
+ }
40
+ export interface Lecture extends LectureRaw {
41
+ typlec?: Typlec;
42
+ lecassidts?: LecassRaw["lecassidt"][];
43
+ lecasss?: LecAss[];
44
+ }
45
+ export interface Loi extends LoiRaw {
46
+ deccoc?: Deccoc;
47
+ etaloi?: Etaloi;
48
+ lecidts?: LectureRaw["lecidt"][];
49
+ lectures?: Lecture[];
50
+ typloi?: Typloi;
51
+ }
52
+ export interface Rap extends RapRaw {
53
+ denrap?: Denrap;
54
+ docattcles?: DocattRaw["docattcle"][];
55
+ docatts?: DocAtt[];
56
+ ecrnums?: EcrRaw["ecrnum"][];
57
+ ecrs?: Ecr[];
58
+ orgcods?: Raporg["orgcod"][];
59
+ orgs?: Org[];
60
+ }
61
+ export interface Texte extends TexteRaw {
62
+ ecrs?: Ecr[];
63
+ ecrnums?: EcrRaw["ecrnum"][];
64
+ libtypurl?: Typurl["libtypurl"];
65
+ org?: Org;
66
+ oritxt: Oritxt;
67
+ typtxt?: Typtxt;
68
+ txtAmeli?: TxtAmeliCustom;
69
+ txtAmeliId: TxtAmeli["id"];
70
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { TamQuestions } from "../raw_types/questions.js";
2
+ export type { TamQuestions as Question };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export interface Photo {
2
+ chemin: string;
3
+ cheminMosaique: string;
4
+ hauteur: number;
5
+ largeur: number;
6
+ xMosaique: number;
7
+ yMosaique: number;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export declare const UNDEFINED_SESSION = 0;
2
+ export declare const sessionOptions: readonly [1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026];
3
+ export declare const sessionOptionsOrAll: readonly [0, 1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026];
4
+ export type Session = (typeof sessionOptions)[number];
5
+ export type SessionOrAll = (typeof sessionOptionsOrAll)[number];
6
+ export declare function getSessionsFromStart(startSession: SessionOrAll): (1958 | 1959 | 1960 | 1961 | 1962 | 1963 | 1964 | 1965 | 1966 | 1967 | 1968 | 1969 | 1970 | 1971 | 1972 | 1973 | 1974 | 1975 | 1976 | 1977 | 1978 | 1979 | 1980 | 1981 | 1982 | 1983 | 1984 | 1985 | 1986 | 1987 | 1988 | 1989 | 1990 | 1991 | 1992 | 1993 | 1994 | 1995 | 1996 | 1997 | 1998 | 1999 | 2000 | 2001 | 2002 | 2003 | 2004 | 2005 | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026)[];
@@ -0,0 +1,19 @@
1
+ export const UNDEFINED_SESSION = 0;
2
+ export const sessionOptions = [
3
+ 1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976,
4
+ 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995,
5
+ 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014,
6
+ 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026,
7
+ // TO COMPLETE EVERY YEAR :)
8
+ ];
9
+ export const sessionOptionsOrAll = [UNDEFINED_SESSION, ...sessionOptions];
10
+ export function getSessionsFromStart(startSession) {
11
+ if (startSession === UNDEFINED_SESSION) {
12
+ return Array.from(sessionOptions);
13
+ }
14
+ const sessionIndex = sessionOptions.findIndex((session) => startSession === session);
15
+ if (sessionIndex >= 0) {
16
+ return sessionOptions.slice(sessionIndex);
17
+ }
18
+ return [];
19
+ }
@@ -0,0 +1,72 @@
1
+ import { DocumentResult } from "../loaders.js";
2
+ export declare enum DivisionType {
3
+ tome = 1,
4
+ part = 2,
5
+ book = 3,
6
+ title = 4,
7
+ subtitle = 5,
8
+ chapter = 6,
9
+ section = 7,
10
+ subsection = 8,
11
+ paragraph = 9,
12
+ article = 10,
13
+ alinea = 11,
14
+ division = 12
15
+ }
16
+ export type DivisionTag = keyof typeof DivisionType;
17
+ export interface DocumentMetadata {
18
+ name: string;
19
+ session: number | null | undefined;
20
+ date?: string | null;
21
+ url_expose_des_motifs?: URL;
22
+ url_xml?: URL;
23
+ url_html: URL;
24
+ url_pdf: URL;
25
+ }
26
+ export interface FlatTexte extends Partial<DocumentResult> {
27
+ titre: string | null;
28
+ titre_court: string | null;
29
+ url_dossier_senat: string | null;
30
+ url_dossier_assemblee: string | null;
31
+ date_presentation: Date | null;
32
+ date_depot: Date | null;
33
+ date_publication_xml: Date | null;
34
+ version: Version | null;
35
+ divisions: Division[];
36
+ expose_motifs?: ExposeDesMotifs | null;
37
+ }
38
+ export type Version = "RECT" | "RECT_BIS" | "RECT_TER" | "RECT_QUATER" | "RECT_QUINQUIES";
39
+ export interface Step {
40
+ eId: string;
41
+ date: Date | null;
42
+ type: string | null;
43
+ session: string | null;
44
+ numero: string | null;
45
+ version: Version | null;
46
+ outcome: string | null;
47
+ }
48
+ export interface Division {
49
+ index: number;
50
+ eId: string;
51
+ tag: DivisionTag;
52
+ level: number;
53
+ headings: DivisionContent[];
54
+ }
55
+ export interface Article extends Division {
56
+ alineas: Alinea[];
57
+ }
58
+ export interface DivisionContent {
59
+ text: string | null;
60
+ html?: string | null;
61
+ }
62
+ export interface Alinea extends DivisionContent {
63
+ eId: string;
64
+ heading: DivisionContent;
65
+ pastille: string | null;
66
+ text_avec_liens?: string | null;
67
+ html_avec_liens?: string | null;
68
+ }
69
+ export interface ExposeDesMotifs {
70
+ text: string | null;
71
+ html: string | null;
72
+ }
@@ -0,0 +1,15 @@
1
+ export var DivisionType;
2
+ (function (DivisionType) {
3
+ DivisionType[DivisionType["tome"] = 1] = "tome";
4
+ DivisionType[DivisionType["part"] = 2] = "part";
5
+ DivisionType[DivisionType["book"] = 3] = "book";
6
+ DivisionType[DivisionType["title"] = 4] = "title";
7
+ DivisionType[DivisionType["subtitle"] = 5] = "subtitle";
8
+ DivisionType[DivisionType["chapter"] = 6] = "chapter";
9
+ DivisionType[DivisionType["section"] = 7] = "section";
10
+ DivisionType[DivisionType["subsection"] = 8] = "subsection";
11
+ DivisionType[DivisionType["paragraph"] = 9] = "paragraph";
12
+ DivisionType[DivisionType["article"] = 10] = "article";
13
+ DivisionType[DivisionType["alinea"] = 11] = "alinea";
14
+ DivisionType[DivisionType["division"] = 12] = "division";
15
+ })(DivisionType || (DivisionType = {}));
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ export declare const configSchema: z.ZodObject<{
3
+ db: z.ZodObject<{
4
+ host: z.ZodString;
5
+ password: z.ZodString;
6
+ user: z.ZodString;
7
+ port: z.ZodCoercedNumber<unknown>;
8
+ }, z.core.$strip>;
9
+ }, z.core.$strip>;
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ const dbSchema = z.object({
3
+ host: z.string().trim().min(1, "Must not be empty"),
4
+ password: z.string().trim().min(1, "Must not be empty"),
5
+ user: z.string().trim().min(1, "Must not be empty"),
6
+ port: z.coerce.number().int().min(0).max(65535),
7
+ });
8
+ export const configSchema = z.object({
9
+ db: dbSchema,
10
+ });
@@ -98,16 +98,16 @@ export async function matchOneReunion(args) {
98
98
  // Accept threshold (best must pass)
99
99
  if (best.score < minAccept) {
100
100
  if (!options["silent"])
101
- console.log(`[threshold] ${agenda.uid} score=${best.score.toFixed(2)}
102
- agenda title="${agenda.titre ?? ""}"
101
+ console.log(`[threshold] ${agenda.uid} score=${best.score.toFixed(2)}
102
+ agenda title="${agenda.titre ?? ""}"
103
103
  best title="${best.vtitle ?? ""}"`);
104
104
  return null;
105
105
  }
106
106
  // Margin (confidence) gate
107
107
  if (second && best.score - second.score < margin) {
108
108
  if (!options["silent"]) {
109
- console.log(`[Margin] ${agenda.uid} score=${best.score.toFixed(2)}
110
- agenda title="${agenda.titre ?? ""}" agenda date="${agenda.startTime ?? ""}"
109
+ console.log(`[Margin] ${agenda.uid} score=${best.score.toFixed(2)}
110
+ agenda title="${agenda.titre ?? ""}" agenda date="${agenda.startTime ?? ""}"
111
111
  best title="${best.vtitle ?? ""}" date="${best.epoch ? new Date(best.epoch * 1000).toISOString() : "null"}"`);
112
112
  console.log("second title=", second.vtitle ?? "", " score=", second.score.toFixed(2), "date=", second.epoch ? new Date(second.epoch * 1000).toISOString() : "null");
113
113
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalizeMissionPlfLibelle, parseProgrammesPlf } from "../../../src/parsers/plf/amendement.js";
3
+ const header = `
4
+ <tr><th>Programmes</th><th colspan="2">Autorisations d’engagement</th><th colspan="2">Crédits de paiement</th></tr>
5
+ <tr><th></th><th>+</th><th>−</th><th>+</th><th>−</th></tr>`;
6
+ describe("parseProgrammesPlf", () => {
7
+ it("parses signed amounts and aggregates repeated programmes", () => {
8
+ const html = `<body><table>${header}
9
+ <tr><td>Programme A</td><td>1\u202f000</td><td></td><td></td><td>200</td></tr>
10
+ <tr><td>Programme A</td><td></td><td>250</td><td>50</td><td></td></tr>
11
+ <tr><td>Programme B</td><td></td><td>− 300</td><td>400</td><td></td></tr>
12
+ <tr><td>dont titre 2</td><td>999</td><td></td><td>999</td><td></td></tr>
13
+ <tr><td>TOTAL</td><td>1 000</td><td></td><td></td><td>200</td></tr>
14
+ <tr><td>SOLDE</td><td>1 000</td><td></td><td></td><td>−200</td></tr>
15
+ </table></body>`;
16
+ expect(parseProgrammesPlf(html)).toEqual([
17
+ { libelle: "Programme A", autorisation_engagement: "750", credit_paiement: "-150" },
18
+ {
19
+ libelle: "Programme B",
20
+ autorisation_engagement: "-300",
21
+ credit_paiement: "400",
22
+ lignes_credits: [{ libelle: "dont titre 2", autorisation_engagement: "999", credit_paiement: "999" }],
23
+ },
24
+ ]);
25
+ });
26
+ it("parses paragraph and concatenated credit lines and preserves decimals", () => {
27
+ const html = `<table>${header}
28
+ <tr><td><p><b>Programme décimal</b></p><p>dont titre 2</p></td><td>3 580 238 879,1</td><td></td><td></td><td>711 953,5</td></tr>
29
+ <tr><td>Programme concaténé dont titre 2</td><td>10.25</td><td></td><td></td><td>2.5</td></tr>
30
+ </table>`;
31
+ expect(parseProgrammesPlf(html)).toEqual([
32
+ {
33
+ libelle: "Programme décimal",
34
+ autorisation_engagement: "3580238879.1",
35
+ credit_paiement: "-711953.5",
36
+ lignes_credits: [
37
+ { libelle: "dont titre 2", autorisation_engagement: "3580238879.1", credit_paiement: "-711953.5" },
38
+ ],
39
+ },
40
+ {
41
+ libelle: "Programme concaténé",
42
+ autorisation_engagement: "10.25",
43
+ credit_paiement: "-2.5",
44
+ lignes_credits: [{ libelle: "dont titre 2", autorisation_engagement: "10.25", credit_paiement: "-2.5" }],
45
+ },
46
+ ]);
47
+ });
48
+ it("parses distinct programme and credit-line amounts from separate paragraphs", () => {
49
+ const html = `<table>${header}
50
+ <tr>
51
+ <td><p><b>Programme multi-paragraphes</b></p><p>dont titre 2</p></td>
52
+ <td><p>120</p><p>&nbsp;</p><p>45</p></td>
53
+ <td><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></td>
54
+ <td><p>100</p><p>&nbsp;</p><p>35</p></td>
55
+ <td><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></td>
56
+ </tr>
57
+ </table>`;
58
+ expect(parseProgrammesPlf(html)).toEqual([
59
+ {
60
+ libelle: "Programme multi-paragraphes",
61
+ autorisation_engagement: "120",
62
+ credit_paiement: "100",
63
+ lignes_credits: [{ libelle: "dont titre 2", autorisation_engagement: "45", credit_paiement: "35" }],
64
+ },
65
+ ]);
66
+ });
67
+ it("does not concatenate the real multi-paragraph amounts from pjlf2026-II-455", () => {
68
+ const html = `<table>${header}
69
+ <tr>
70
+ <td><p><b>Enseignement scolaire public du premier degré</b></p><p>dont titre 2</p></td>
71
+ <td><p>93&nbsp;000&nbsp;000</p><p>&nbsp;</p><p>93&nbsp;000&nbsp;000</p></td>
72
+ <td><p>&nbsp;</p></td>
73
+ <td><p>93&nbsp;000&nbsp;000</p><p>&nbsp;</p><p>93&nbsp;000&nbsp;000</p></td>
74
+ <td><p>&nbsp;</p></td>
75
+ </tr>
76
+ <tr>
77
+ <td><p><b>Vie de l’élève</b></p><p>dont titre 2</p></td>
78
+ <td><p>&nbsp;</p></td><td><p>&nbsp;</p></td><td><p>&nbsp;</p></td><td><p>&nbsp;</p></td>
79
+ </tr>
80
+ <tr>
81
+ <td><p><b>Soutien de la politique de l’éducation nationale</b></p><p>dont titre 2</p></td>
82
+ <td><p>&nbsp;</p></td>
83
+ <td><p>190&nbsp;500 000</p><p>190&nbsp;500 000</p></td>
84
+ <td><p>&nbsp;</p></td>
85
+ <td><p>190&nbsp;500 000</p><p>190&nbsp;500 000</p></td>
86
+ </tr>
87
+ </table>`;
88
+ expect(parseProgrammesPlf(html)).toEqual([
89
+ {
90
+ libelle: "Enseignement scolaire public du premier degré",
91
+ autorisation_engagement: "93000000",
92
+ credit_paiement: "93000000",
93
+ lignes_credits: [{ libelle: "dont titre 2", autorisation_engagement: "93000000", credit_paiement: "93000000" }],
94
+ },
95
+ {
96
+ libelle: "Soutien de la politique de l’éducation nationale",
97
+ autorisation_engagement: "-190500000",
98
+ credit_paiement: "-190500000",
99
+ lignes_credits: [
100
+ { libelle: "dont titre 2", autorisation_engagement: "-190500000", credit_paiement: "-190500000" },
101
+ ],
102
+ },
103
+ ]);
104
+ });
105
+ it("parses a credit line separated from its programme by a br", () => {
106
+ const html = `<table>${header}
107
+ <tr><td><p><b>Sport<br></b><i>dont titre 2</i></p></td><td>10 450 000</td><td></td><td>10 450 000</td><td></td></tr>
108
+ </table>`;
109
+ expect(parseProgrammesPlf(html)).toEqual([
110
+ {
111
+ libelle: "Sport",
112
+ autorisation_engagement: "10450000",
113
+ credit_paiement: "10450000",
114
+ lignes_credits: [{ libelle: "dont titre 2", autorisation_engagement: "10450000", credit_paiement: "10450000" }],
115
+ },
116
+ ]);
117
+ });
118
+ it("aggregates repeated programmes and credit lines independently", () => {
119
+ const html = `<table>${header}
120
+ <tr><td><p>Programme A</p><p>dont titre 2</p></td><td>11</td><td></td><td>20</td><td></td></tr>
121
+ <tr><td><p>Programme A</p><p>dont titre 2</p></td><td></td><td>2</td><td></td><td>5</td></tr>
122
+ </table>`;
123
+ expect(parseProgrammesPlf(html)).toEqual([
124
+ {
125
+ libelle: "Programme A",
126
+ autorisation_engagement: "9",
127
+ credit_paiement: "15",
128
+ lignes_credits: [{ libelle: "dont titre 2", autorisation_engagement: "9", credit_paiement: "15" }],
129
+ },
130
+ ]);
131
+ });
132
+ it("ignores non-budgetary, empty and incomplete tables", () => {
133
+ expect(parseProgrammesPlf(null)).toEqual([]);
134
+ expect(parseProgrammesPlf("<p>Modifier cet article.</p>")).toEqual([]);
135
+ expect(parseProgrammesPlf("<table><tr><td>Programmes</td><td>Crédits de paiement</td></tr></table>")).toEqual([]);
136
+ });
137
+ it("does not infer programmes for a sub-amendment without its own table", () => {
138
+ const html = "<p>Dans le programme mentionné par l’amendement II-42, remplacer 100 par 200.</p>";
139
+ expect(parseProgrammesPlf(html)).toEqual([]);
140
+ });
141
+ });
142
+ describe("normalizeMissionPlfLibelle", () => {
143
+ it.each([
144
+ ["Mission Solidarité, insertion et égalité des chances", "Solidarité, insertion et égalité des chances"],
145
+ ["Budget annexe - Contrôle et exploitation aériens", "Contrôle et exploitation aériens"],
146
+ ["Compte spécial - Pensions", "Pensions"],
147
+ ["Compte spécial — Avances aux collectivités territoriales", "Avances aux collectivités territoriales"],
148
+ ["Économie", "Économie"],
149
+ [null, null],
150
+ ])("normalizes %s", (source, expected) => {
151
+ expect(normalizeMissionPlfLibelle(source)).toBe(expected);
152
+ });
153
+ });