From e54f1afe2580cc3ade1eb07f09ab0154d18829ec Mon Sep 17 00:00:00 2001 From: RemiM Date: Wed, 9 Jun 2021 15:38:50 +0200 Subject: [PATCH] feat: move reference extraction into an external lib --- package.json | 3 +- .../__tests__/referenceExtractor.test.js | 104 -- .../__tests__/referenceExtractor.test.txt | 1110 ----------------- src/fetch-data/parseDom.js | 4 +- src/fetch-data/referenceExtractor.js | 259 ---- 5 files changed, 4 insertions(+), 1476 deletions(-) delete mode 100644 src/fetch-data/__tests__/referenceExtractor.test.js delete mode 100644 src/fetch-data/__tests__/referenceExtractor.test.txt delete mode 100644 src/fetch-data/referenceExtractor.js diff --git a/package.json b/package.json index f5d1ac4d..2dfb7d1b 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@socialgouv/cdtn-slugify": "^4.44.0", "@socialgouv/eslint-config-recommended": "^1.46.0", "@socialgouv/legi-data": "^2.45.0", + "@socialgouv/reference-article": "^1.0.1", "babel-jest": "^26.6.3", "eslint": "^7.17.0", "esm": "^3.2.25", @@ -48,7 +49,7 @@ "superagent": "^6.1.0", "talisman": "^1.1.4", "unist-util-find": "^1.0.2", - "unist-util-visit": "^2.0.3" + "unist-util-visit": "^2.0.3", }, "jest": { "roots": [ diff --git a/src/fetch-data/__tests__/referenceExtractor.test.js b/src/fetch-data/__tests__/referenceExtractor.test.js deleted file mode 100644 index bb84e717..00000000 --- a/src/fetch-data/__tests__/referenceExtractor.test.js +++ /dev/null @@ -1,104 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; - -import { classifyTokens, extractReferences } from "../referenceExtractor"; -import { resolveReferences } from "../referenceResolver"; - -const annotatedTokens = fs - .readFileSync(path.join(__dirname, "referenceExtractor.test.txt")) - .toString() - .split("\n"); - -const tokens = []; -const labels = []; - -annotatedTokens.map((line) => { - const [t, l] = line.split("\t"); - tokens.push(t); - labels.push(l); -}); - -const testCases = [ - { - input: - "les modalités fixées par les articles l. 2313‑8 et R. 2313-3 à R. 2313-6 du code du travail ainsi que le L. 1251-18", - }, - { - input: "L. 1251-23xx du code du travail", - }, - { - input: - "l’allocation de remplacement pour maternité ou paternité, prévues aux articles L. 613-19 à L.613-19-2 et L. 722-8 à 25 du code de la sécurité sociale, aux articles L. 732-10 à L. 732-12-1 du code rural et à l’article 17 de la loi n° 97-1051 du 18 novembre 1997 d’orientation sur la pêche maritime et les cultures marines", - }, - { - input: `Article D212 du code penal et article R413`, - }, - { input: `Article D212` }, - { input: `Article D-212` }, - { input: `Article D.212` }, - { input: `Article D212-3` }, - { input: `Article D-212-4` }, - { input: `Article X*212-4` }, - { input: `Article D. 212-4` }, - { input: `Article D. 212-4` }, - { input: `Article D.212-5` }, - { input: `Article D.212-5-6` }, - { input: `Article D.212-5-6-7` }, - { input: `Article XD212` }, -]; - -test("should extract article tokens in examples", () => { - expect( - testCases.map(({ input }) => extractReferences(input)) - ).toMatchSnapshot(); -}); - -test("should success with actual real life set", () => { - const predictions = classifyTokens(tokens); - // keep this to easily check prediction errors : - /* - const report = tokens.map((t, i) => { - const label = labels[i]; - const prediction = predictions[i]; - const ok = prediction == label; - return [ - `${ok ? "\x1b[0m" : "\x1b[31m"}`, - `${t}\t${label}\t${prediction}\n` - ]; - }); - console.log(...report.flat()); - */ - expect(predictions).toEqual(labels); -}); - -it("should find with code for actual real life set", () => - expect(extractReferences(tokens.join(" "))).toMatchSnapshot()); - -it("should resolve example codes", () => { - const refs0 = extractReferences(testCases[0].input); - expect(resolveReferences(refs0)).toMatchSnapshot(); - - const refs1 = extractReferences(testCases[1].input); - expect(resolveReferences(refs1)).toMatchSnapshot(); -}); - -const rangeCases = [ - "L. 1251-21 à L. 1251-23xx du code du travail", - "L. 1233‑34 à L. 1233-35-1 du code du travail", - "L. 2312-72 à 2312-77 du code du travail", - "L. 2312-72 à 2312-77 du code de l'éducation", - "D. 5132-9 à D. 5132-10-4 du code du travail", - "D. 5132-9 à D. 5132-10-4", - "L. 2315-38 à 40 du code du travail", - "L. 351-1 à L. 351-5 du code de la sécurité sociale", -]; - -it("should resolve ranges", () => { - const refs = rangeCases.map((c) => { - const extractedRefs = extractReferences(c); - const resolvedRefs = resolveReferences(extractedRefs); - return resolvedRefs; - }); - - expect(refs).toMatchSnapshot(); -}); diff --git a/src/fetch-data/__tests__/referenceExtractor.test.txt b/src/fetch-data/__tests__/referenceExtractor.test.txt deleted file mode 100644 index 5f81cee3..00000000 --- a/src/fetch-data/__tests__/referenceExtractor.test.txt +++ /dev/null @@ -1,1110 +0,0 @@ -Le O -nombre O -et O -le O -périmètre O -des O -établissements O -distincts O -sont O -déterminés O -selon O -les O -modalités O -fixées O -par O -les O -articles O -L. B-ART -2313-8 B-ART -et O -R. B-ART -2313-3 B-ART -à B-ART -R. B-ART -2313-6 B-ART -du O -code O -du O -travail O -. O -’ O -action O -, O -dans O -les O -conditions O -fixées O -par O -l O -’ O -article O -R. B-ART -2242-7 B-ART -du O -code O -du O -travail.Dans O -la O -limite O -me O -La O -protection O -contre O -les O -discriminations O -» O -.Une O -pénalité O -financière O -, O -à O -défaut O -d O -’ O -accord O -ou O -de O -plan O -d O -’ O -actionDans O -des O -conditions O -fixées O -, O -pour O -l O -’ O -essentiel O -, O -par O -l O -’ O -article O -L. B-ART -2242-8 B-ART -du O -code O -du O -travail O -et O -par O -les O -articles O -R. B-ART -2242-2 B-ART -à B-ART -R. B-ART -2242-11 B-ART -, O -à O -défaut O -d O -’ O -accord O -, O -de O -son O -plan O -d O -’ O -action O -relatif O -à O -l O -’ O -égalité O -professionnelle O -entre O -les O -femmes O -et O -les O -hommes O -est O -prévue O -par O -les O -articles O -L. B-ART -2242-9 B-ART -et O -R. B-ART -2242-9 B-ART -à B-ART -R. B-ART -2242-11 B-ART -du O -code O -du O -travail O -. O -cté O -au O -fonds O -de O -solidarité O -vieillesse O -mentionné O -à O -l O -’ O -article O -L. B-ART -135-1 B-ART -du O -code O -de O -la O -sécurité O -sociale.Cette O -pénalité O -pourra O -également O -être O -appliquée O -en O -l O -’ O -absence O -de O -publication O -, O -par O -l O -’ O -entreprise O -, O -de O -l O -’ O -« O -Index O -de O -l O -’ O -égalité O -Femmes-Hommes O -» O -( O -voir O -ci-dessus O -) O -ou O -aux O -entreprises O -qui O -n O -’ O -auront O -pas O -pris O -les O -mesures O -correctives O -en O -cas O -d O -’ O -Index O -inférieur O -à O -75 O -points O -. O -L O -’ O -« O -Index O -de O -l O -’ O -égalité O -Femmes-Hommes O -» O -est O -établi O -à O -partir O -des O -indicateurs O -figurant O -aux O -articles O -D. B-ART -1142-2 B-ART -( O -entreprises O -de O -plus O -de O -250 O -salariés O -) O -et O -D. B-ART -1142-2-1 B-ART -du O -code O -du O -travail O -( O -- O -les O -établissements O -mentionnés O -aux O -articles O -L. B-ART -4521-1 B-ART -et O -suivants O -du O -Code O -du O -travail O -( O -établissements O -comprenant O -au O -moins O -une O -installation O -nucléaire O -de O -base O -, O -classés O -Seveso O -ou O -certains O -gisements O -miniers O -) O -. O -ateurs O -figurant O -aux O -articles O -D. B-ART -1142-2 B-ART -( O -entreprises O -de O -plus O -de O -250 O -salariés O -) O -et O -D. B-ART -1142-2-1 B-ART -du O -code O -du O -travail O -( O -entreprises O -de O -50 O -à O -250 O -salariés O -) O -. O -Il O -est O -publié O -annuellement O -, O -au O -plus O -tard O -le O -1er O -mars O -de O -l O -’ O -année O -en O -cours O -, O -au O -titre O -de O -l O -’ O -année O -précédente O -, O -sur O -le O -site O -internet O -de O -l O -’ O -entreprise O -lorsqu O -’ O -il O -en O -existe O -un O -. O -À O -défaut O -, O -il O -est O -porté O -à O -la O -connaissance O -des O -salariés O -par O -tout O -moye O -Crash O -: O -es O -employeurs O -d O -’ O -au O -moins O -11 O -salariés O -( O -seuil O -applicable O -à O -compter O -du O -1er O -janvier O -2020 O -, O -précédemment O -fixé O -à O -10 O -salariés O -) O -doivent O -effectuer O -cette O -transmission O -à O -Pôle O -emploi O -par O -voie O -électronique O -( O -sauf O -impossibilité O -pour O -une O -cause O -qui O -leur O -est O -étrangère O -) O -, O -selon O -les O -modalités O -fixées O -par O -l O -’ O -arrêté O -du O -14 O -juin O -2011 O -. O -Pour O -la O -mise O -en O -œuvre O -de O -cette O -obligation O -de O -transmission O -par O -voie O -électronique O -( O -art O -. O -R. B-ART -1234-9 B-ART -du O -code O -du O -travail O -) O -, O -l O -’ O -effectif O -salarié O -et O -les O -règles O -de O -franchissement O -de O -seuils O -d O -’ O -effectif O -sont O -déterminés O -selon O -les O -modalités O -prévues O -aux O -articles O -L. B-ART -130-1 B-ART -et O -R. B-ART -130-1 B-ART -du O -code O -de O -la O -sécurité O -sociale O -, O -sous O -réserv O -La O -réponse O -intervient O -dans O -les O -30 O -jours O -qui O -suivent O -sa O -demande O -et O -contient O -les O -informations O -mentionnées O -à O -l O -’ O -article O -D. B-ART -8223-2 B-ART -du O -Code O -du O -travail O -. O -Sur O -ce O -point O -, O -on O -peut O -consulter O -les O -informations O -diffusées O -sur O -le O -site O -de O -l O -’ O -Urssaf O -Une O -copie O -des O -déclarations O -de O -détachement O -mentionnées O -aux O -articles O -R. B-ART -1263-3 B-ART -, O -R. B-ART -1263-4 B-ART -et O -R. B-ART -1263-6 B-ART -du O -code O -du O -travail O -est O -annexée O -au O -registre O -unique O -du O -personnel O -et O -rendue O -accessible O -aux O -délégués O -du O -personnel O -( O -ou O -aux O -membres O -de O -la O -délégation O -du O -personnel O -du O -comité O -social O -et O -économique O -lorsque O -cette O -instance O -de O -représentation O -du O -personnel O -aura O -été O -mise O -en O -place O -dans O -l O -’ O -entreprise O -) O -et O -aux O -fonctionnaires O -et O -agents O -chargés O -de O -veiller O -à O -l O -’ O -application O -du O -code O -du O -tra O -l O -’ O -allocation O -de O -remplacement O -pour O -maternité O -ou O -paternité O -, O -prévues O -aux O -articles O -L. B-ART -613-19 B-ART -à B-ART -L. B-ART -613-19-2 B-ART -et O -L. B-ART -722-8 B-ART -à B-ART -L. B-ART -722-8-3 B-ART -du O -code O -de O -la O -sécurité O -sociale O -, O -aux O -articles O -L. B-ART -732-10 B-ART -à B-ART -L. B-ART -732-12-1 B-ART -du O -code O -rural O -et O -à O -l O -’ O -article O -17 O -de O -la O -loi O -n° O -97-1051 O -du O -18 O -novembre O -1997 O -d O -’ O -orientation O -sur O -la O -pêche O -maritime O -et O -les O -cultures O -marines O -lui-même O -pris O -sur O -la O -base O -de O -l O -’ O -article O -L. B-ART -4412-2 B-ART -, O -issu O -de O -la O -loi O -n° O -2016-1088 O -du O -8 O -août O -2016 O -6 O -domaines O -d O -’ O -activité O -défini O -par O -le O -décret O -du O -9 O -mai O -2017 O -( O -art O -. O -R. B-ART -4412-97/II B-ART -) O -, O -la O -Direction O -générale O -du O -travail O -( O -DGT O -) O -a O -choisi O -de O -s O -’ O -appuyer O -sur O -un O -travail O -de O -normalisation O -. O -En O -l O -’ O -absence O -de O -tout O -accord O -( O -décision O -unilatérale O -de O -l O -’ O -employeur O -) O -, O -ces O -modalités O -sont O -définies O -par O -le O -règlement O -intérieur O -du O -comité O -, O -prévu O -par O -l O -’ O -article O -L. B-ART -2315-24 B-ART -du O -code O -du O -travail.Dans O -tous O -les O -cas O -, O -les O -dispositions O -d O -’ O -ordre O -public O -relatives O -à O -la O -composition O -, O -aux O -missions O -et O -au O -fonctionnement O -de O -la O -commission O -, O -prévues O -aux O -articles O -L. B-ART -2315-38 B-ART -à B-ART -40 B-ART -, O -devront O -être O -respectées O -. O -`` O -, O -Les O -effectifs O -s O -’ O -apprécient O -dans O -le O -cadre O -de O -l O -’ O -entreprise O -ou O -dans O -le O -cadre O -de O -chaque O -établissement O -distinct O -, O -conformément O -aux O -dispositions O -prévues O -par O -le O -Code O -du O -travail O -et O -Article O -L1111-2 B-ART -. O -La O -délégation O -du O -personnel O -comporte O -un O -nombre O -égal O -de O -titulaires O -et O -de O -suppléants O -. O -satisfaire O -aux O -conditions O -de O -majorité O -prévues O -aux O -articles O -L. B-ART -2314-6 B-ART -du O -code O -du O -travail O -( O -double O -condition O -de O -majorité O -, O -voir O -précisions O -ci-dessus O -) O -. O -Pour O -sa O -part O -, O -l O -’ O -accord O -d O -’ O -entreprise O -( O -ou O -de O -groupe O -) O -autorisant O -le O -recours O -au O -vote O -électronique O -( O -à O -défaut O -d O -’ O -accord O -, O -l O -’ O -employeur O -peut O -décider O -de O -ce O -recours O -) O -est O -soumis O -aux O -seules O -conditions O -de O -validité O -prévues O -par O -le O -code O -du O -code O -du O -travail O -dans O -le O -cadre O -du O -droit O -commun O -de O -la O -négociation O -collective.Conception O -et O -mise O -en O -place O -du O -dispositifLa O -conception O -et O -la O -mise O -en O -place O -du O -système O -de O -vote O -électronique O -peuvent O -être O -confiées O -à O -un O -prestataire O -choisi O -par O -l O -’ O -employeur O -sur O -la O -base O -d O -’ O -un O -cahier O -des O -charges O -respectant O -les O -dispositions O -des O -articles O -R. B-ART -2314-6 B-ART -et O -suivants O -du O -code O -du O -travail O -. O -nomique O -et O -sociale O -ainsi O -que O -les O -modalités O -de O -son O -fonctionnement O -, O -notamment O -les O -droits O -d O -’ O -accès O -, O -sont O -ceux O -mentionnés O -à O -l O -’ O -article O -L B-ART -2312-36 B-ART -du O -Code O -du O -travail O -. O -Ces O -éléments O -sont O -précisés O -: O -par O -les O -articles O -R. B-ART -2312-8 B-ART -et O -R. B-ART -2312-10 B-ART -à B-ART -R. B-ART -2312-14 B-ART -pour O -les O -entreprises O -de O -moins O -de O -300 O -salariés O -; O -par O -les O -articles O -R. B-ART -2312-9 B-ART -et O -R. B-ART -2312-10 B-ART -à B-ART -R. B-ART -2312-14 B-ART -pour O -les O -entreprises O -d O -’ O -au O -moins O -300 O -salariés O -. O -Cette O -base O -de O -données O -, O -régulièrement O -mise O -à O -jour O -, O -est O -accessible O -en O -permanence O -aux O -membres O -de O -la O -délégation O -du O -personnel O -du O -CSE O -ainsi O \ No newline at end of file diff --git a/src/fetch-data/parseDom.js b/src/fetch-data/parseDom.js index 1311567d..b54aaca5 100644 --- a/src/fetch-data/parseDom.js +++ b/src/fetch-data/parseDom.js @@ -2,8 +2,8 @@ import slugify from "@socialgouv/cdtn-slugify"; import { ParseError } from "got"; import { encode } from "../email"; -import { extractReferences } from "./referenceExtractor"; -import { resolveReferences } from "./referenceResolver"; +import { extractReferences } from "./referenceResolver"; +import { resolveReference } from "@socialgouv/reference-article"; const $$ = (node, selector) => Array.from(node.querySelectorAll(selector)); const $ = (node, selector) => node.querySelector(selector); diff --git a/src/fetch-data/referenceExtractor.js b/src/fetch-data/referenceExtractor.js deleted file mode 100644 index 03207bf7..00000000 --- a/src/fetch-data/referenceExtractor.js +++ /dev/null @@ -1,259 +0,0 @@ -/* -Extracting references is done in several steps : -1) classifyTokens : we identify valid article references (start with l/r/d then token of shape like 1234-12) => split text into sequence of tokens and give a label to each token -2) identifyCode : we search for the associated code after the ref tokens (valid options are : code du travail / code de la scurite sociale) -3) we group those to constitute structured reference of shape : - { - "article": "L. 2313-8", - "code": Object { - "id": "LEGITEXT000006072050", - "name": "code du travail", - }, - } -*/ - -import treebank from "talisman/tokenizers/words/treebank"; - -const NEGATIVE = "O"; -const ARTICLE = "B-ART"; -const CODE_PREFIX = "B-COD"; -// code du travail -const CODE_TRA = CODE_PREFIX + "_TRA"; -// code sécurité sociale -const CODE_SS = CODE_PREFIX + "_SS"; -// code any other -const CODE_OTHER = CODE_PREFIX + "_O"; - -const UNRECOGNIZED = "unrecognized"; - -const CODE_TRAVAIL = { - id: "LEGITEXT000006072050", - name: "code du travail", -}; - -const CODE_SECU = { - id: "LEGITEXT000006073189", - name: "code de la sécurité sociale", -}; - -const codesFullNames = { - [CODE_SS]: CODE_SECU, - [CODE_TRA]: CODE_TRAVAIL, -}; - -// maximum distance between code tokens and corresponding article ref -const range = 20; - -const articleRegEx = new RegExp("^(\\d{1,4}(-\\d+){0,3})\\b"); // nums 123 123-45 123-45-6 123-45-6-7 -function articleMatcher(token) { - return token.match(articleRegEx); -} - -const validPrefix = ["l", "r", "d"]; - -// returns : -// 0 if not matching -// 1 if matching prefix only (L.) -// 2 if matching prefix and valid ref (L123.12) -function prefixMatcher(token) { - const lowToken = token.toLowerCase(); - - // if starts with possible prefix - const matchingPrefix = - validPrefix.filter((p) => lowToken.startsWith(p)).length > 0; - - if (matchingPrefix) { - const residual = lowToken.slice(1); - - // case only L - if (!residual.length) { - return 1; - } else { - // case L. - if (residual == ".") { - return 1; - } else { - // case L.123-12 - if (residual.slice(0, 1) == "." && articleMatcher(residual.slice(1))) { - return 2; - } - // case L.123-12 - else if (articleMatcher(residual.slice(1))) { - return 2; - } - } - } - } - // no match - return 0; -} - -function infixMatcher(token) { - // this is quite subtle... - return ["à", "à"].includes(token); -} - -// classify sequence of tokens to identify references to articles -function classifyTokens(tokens) { - // step 1 : check for prefix matches or articles - const step1 = tokens.map((token) => { - const prefix = prefixMatcher(token); - const infix = infixMatcher(token); - const article = articleMatcher(token); - - if (prefix > 0) { - return prefix; - } else if (article) { - return 3; - } else if (infix) { - return 4; - } else { - return 0; - } - }); - - // step 2 : confirm valid sequences - // hack : we keep a buffer as last element of the accumulator - const predictions = step1.reduce( - (acc, e) => { - const buffer = acc[acc.length - 1]; - const inSequence = buffer.length > 0; - const lastElement = buffer[buffer.length - 1]; - - // case continue existing - if (e >= 1 && inSequence) { - buffer.push(e); - } - - // case finish existing - else if (e == 0 && inSequence && lastElement > 1) { - acc.pop(); - // push buffer - buffer.forEach(() => acc.push(true)); - // push for current - acc.push(false); - - acc.push([]); - } - - // case start (valid start are 1 or 2, as 3 is number only without prefix) - else if (e > 0 && e < 3 && !inSequence) { - buffer.push(e); - } - - // other cases, flush buffer and append current - else { - acc.pop(); - acc.push(...buffer.map(() => false)); - acc.push(false); - acc.push([]); - } - - return acc; - }, - [[]] - ); - // conclude - const residual = predictions.pop(); - // if ends with bigger than 1, then add residual as true - if (residual.length > 0 && residual[residual.length - 1] > 1) { - predictions.push(...residual.map(() => true)); - } else { - predictions.push(...residual.map(() => false)); - } - - return predictions.map((p) => (p ? ARTICLE : NEGATIVE)); -} - -function identifyCodes(tokens, predicitions) { - // we look for "code" tokens (starting a code reference) - const matchCode = tokens.map((token, i) => { - return token.toLowerCase() == "code" ? CODE_PREFIX : predicitions[i]; - }); - - // we search for entire code references - const resolvedCodePreds = matchCode.map((pred, i) => { - if (pred == CODE_PREFIX) { - const joinedNextTokens = tokens - .slice(i, i + 5) - .join(" ") - .toLowerCase(); - if (joinedNextTokens.startsWith(codesFullNames[CODE_SS].name)) { - return CODE_SS; - } else if (joinedNextTokens.startsWith(codesFullNames[CODE_TRA].name)) { - return CODE_TRA; - } else { - return CODE_OTHER; - } - } else { - return pred; - } - }); - - return resolvedCodePreds; -} - -// extract references from free text : tokenize and classify -function extractReferences(text) { - const tokens = treebank(text); - let predictions = classifyTokens(tokens); - predictions = identifyCodes(tokens, predictions); - - // console.log(tokens); - // console.log(predictions); - - // group continuous positives tokens and set code - // while continuous match, merge - // if code, then associate it to articles within range - return tokens - .map((token, index) => { - return { index, pred: predictions[index], token }; - }) - .reduce((acc, { token, index, pred }) => { - // case article : we start or merge - if (pred == ARTICLE) { - if (acc.length == 0) { - acc.push({ index, token }); - } else { - const last = acc[acc.length - 1]; - // case continuous : we merge - if (last.index + 1 == index) { - last.token = `${last.token} ${token}`; - last.index = index; - } else { - acc.push({ index, token }); - } - } - } - // case code, we associate it to articles within range - else if (pred.startsWith(CODE_PREFIX) && acc.length > 0) { - acc.forEach((match) => { - // if no code yet and in range - if (!match.code && match.index + range >= index) { - if (pred in codesFullNames) { - match.code = codesFullNames[pred]; - } else { - match.code = UNRECOGNIZED; - } - } - }); - } - - return acc; - }, []) - .filter(({ code }) => { - // valid cases are no code or code different than UNRECOGNIZED (for other codes : rural, education...) - return !code || (code && code != UNRECOGNIZED); - }) - .map(({ token, code }) => { - return { code, text: token }; - }); -} - -export { - classifyTokens, - CODE_SECU, - CODE_TRAVAIL, - codesFullNames, - extractReferences, -};