feat(v11-b): carte-o YAML source editoriale + build script niveau/nature/statut
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
package-lock.json
generated
3
package-lock.json
generated
@@ -20,6 +20,9 @@
|
|||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
"vue": "^3.5.34"
|
"vue": "^3.5.34"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"js-yaml": "^4.1.1"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,5 +26,8 @@
|
|||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
"vue": "^3.5.34"
|
"vue": "^3.5.34"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"js-yaml": "^4.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
public/data/carte-o-source.yaml
Normal file
120
public/data/carte-o-source.yaml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Carte O AEP - source editoriale manuelle
|
||||||
|
# Edite manuellement apres chaque publication
|
||||||
|
# Build: npm run carte-o -> public/data/carte-o.json
|
||||||
|
# statut: gestation (draft/en cours) | edite (publie)
|
||||||
|
# nature: essai (texte politique) | projet (projet archi)
|
||||||
|
# niveau: 0 (centre) | 1 (concepts force) | 2 (thematiques/projets)
|
||||||
|
|
||||||
|
version: "1.1"
|
||||||
|
|
||||||
|
centre:
|
||||||
|
id: "nouveau-contrat-social"
|
||||||
|
label: "Nouveau Contrat Social"
|
||||||
|
niveau: 0
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
resume: "Assemblage de tout ce que j'ecris qui s'entrechoque - inventer un nouveau contrat social."
|
||||||
|
|
||||||
|
concepts_force:
|
||||||
|
- id: "ncs-politique"
|
||||||
|
label: "Nouveau contrat social"
|
||||||
|
niveau: 1
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
resume: "L'ecriture politique d'un futur habitable - essai central AEP."
|
||||||
|
- id: "medecine-corps-social"
|
||||||
|
label: "Medecine du corps social"
|
||||||
|
niveau: 1
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
resume: "Diagnostiquer et soigner les pathologies du corps social - concept AEP mdcs."
|
||||||
|
|
||||||
|
thematiques:
|
||||||
|
- id: "systemique"
|
||||||
|
label: "Systemique & complexite"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "pratiques-collectives"
|
||||||
|
label: "Pratiques collectives"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "art-narration"
|
||||||
|
label: "Art & narration"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "pouvoir-domination"
|
||||||
|
label: "Rapport au pouvoir"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "medias-critique"
|
||||||
|
label: "Medias & pensee critique"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "justice-securite"
|
||||||
|
label: "Justice & securite"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "sante-globale"
|
||||||
|
label: "Sante globale"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "agriculture"
|
||||||
|
label: "Agriculture"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "post-croissance"
|
||||||
|
label: "Post-croissance"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "anthropocene"
|
||||||
|
label: "Anthropocene & effondrement"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "education"
|
||||||
|
label: "Education a la transformation"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "urbanisme"
|
||||||
|
label: "Urbanisme"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "geopolitique"
|
||||||
|
label: "Geopolitique & decolonisation"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "ia-technologie"
|
||||||
|
label: "IA & technologie"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
- id: "spiritualite"
|
||||||
|
label: "Spiritualite"
|
||||||
|
niveau: 2
|
||||||
|
nature: essai
|
||||||
|
statut: gestation
|
||||||
|
|
||||||
|
projets:
|
||||||
|
- id: "tmip"
|
||||||
|
label: "TMIP"
|
||||||
|
niveau: 2
|
||||||
|
nature: projet
|
||||||
|
statut: gestation
|
||||||
|
resume: "Transport, mobilite, industrie, politique - projet archi. Exemple de projet archi relie aux thematiques AEP."
|
||||||
|
liens_thematiques:
|
||||||
|
- "urbanisme"
|
||||||
|
- "justice-securite"
|
||||||
|
- "post-croissance"
|
||||||
|
- "agriculture"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,303 +1,127 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Scrape AEP/Articles/ thematic folders -> public/data/carte-o.json
|
|
||||||
// Two frontmatter formats supported :
|
|
||||||
// 1. YAML standard between --- delimiters
|
|
||||||
// 2. Legacy "MOC : [[X]]\nSource : ...\nTags : ...\nDate : ...\n***" header
|
|
||||||
//
|
|
||||||
// Wikilinks [[X]] in body -> edges (resolved by label match against scraped nodes).
|
|
||||||
// Family inferred from theme directory name (5 AEP families).
|
|
||||||
// V1 cap : top 150 nodes by degree if scrape > 300 nodes.
|
|
||||||
|
|
||||||
import fs from 'node:fs/promises'
|
import fs from 'node:fs/promises'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import matter from 'gray-matter'
|
import yaml from 'js-yaml'
|
||||||
import { globby } from 'globby'
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename)
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
const REPO_ROOT = path.resolve(__dirname, '..')
|
const REPO_ROOT = path.resolve(__dirname, '..')
|
||||||
const ARTICLES_ROOT = 'C:/Users/jules/Dropbox/ATIS - IPCJRA/2 CASQUETTES/Penseur politique/AEP/Articles'
|
const SOURCE = path.join(REPO_ROOT, 'public/data/carte-o-source.yaml')
|
||||||
const OUTPUT = path.join(REPO_ROOT, 'public/data/carte-o.json')
|
const OUTPUT = path.join(REPO_ROOT, 'public/data/carte-o.json')
|
||||||
const NODE_CAP_V1 = 150
|
|
||||||
|
|
||||||
// 5 AEP families : palette refined after first scrape.
|
// radius par niveau + nature
|
||||||
const FAMILY_COLORS = {
|
function getRadius(niveau, nature) {
|
||||||
penseur: '#3b82f6', // blue
|
if (niveau === 0) return 28
|
||||||
concept: '#10b981', // green
|
if (niveau === 1) return 18
|
||||||
methode: '#f59e0b', // amber
|
if (niveau === 2 && nature === 'projet') return 14
|
||||||
collectif: '#ef4444', // red
|
return 10
|
||||||
ressource: '#8b5cf6', // violet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function slugify(str) {
|
// compat backward : nature -> family
|
||||||
return String(str || '')
|
function getFamily(nature) {
|
||||||
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
return nature === 'projet' ? 'ressource' : 'concept'
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '')
|
|
||||||
.slice(0, 80) || 'untitled'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferFamily(signals) {
|
// thematiques rattachees directement au centre (ni ncs-politique ni medecine-corps-social)
|
||||||
// signals = { title, theme, path, tags, content }
|
const CENTRE_THEMATIQUES = new Set([
|
||||||
const haystack = [
|
'medias-critique',
|
||||||
signals.title,
|
'justice-securite',
|
||||||
signals.theme,
|
'agriculture',
|
||||||
signals.path,
|
'urbanisme',
|
||||||
Array.isArray(signals.tags) ? signals.tags.join(' ') : signals.tags,
|
'geopolitique',
|
||||||
(signals.content || '').slice(0, 800),
|
])
|
||||||
].filter(Boolean).join(' ').toLowerCase()
|
|
||||||
|
|
||||||
// Order matters : check most specific first.
|
const NCS_THEMATIQUES = new Set([
|
||||||
// METHODE : process, outils, comment-faire
|
'systemique',
|
||||||
if (/m[ée]thode|outil|pratique|community.organizing|alinsky\b|comment\b|process|protocole|recette|guide|how.to|chantier|d[ée]marche/.test(haystack)) {
|
'pratiques-collectives',
|
||||||
return 'methode'
|
'pouvoir-domination',
|
||||||
}
|
'post-croissance',
|
||||||
// PENSEUR : noms propres, auteurs, figures
|
'education',
|
||||||
if (/penseur|auteur|figure|harari|alinsky|piven|chouard|branco|mamdani|shift|graeber|bourdieu|lordon|stiegler|d[ée]bord|illich|gorz|servigne|vidal|haupt|pisani|lalo|rosa/.test(haystack)) {
|
])
|
||||||
return 'penseur'
|
|
||||||
}
|
|
||||||
// COLLECTIF : organisations, mouvements, réseaux
|
|
||||||
if (/collectif|r[ée]seau|asso|union|coop|mouvement|piraterie|sociale|syndicat|comit[ée]|crise.de.la.profession|nyc|mamdani|chantier/.test(haystack)) {
|
|
||||||
return 'collectif'
|
|
||||||
}
|
|
||||||
// CONCEPT : notions, théories, critiques
|
|
||||||
if (/concept|notion|th[ée]orie|critique|fiction|kakistocratie|imp[ée]rialisme|robustesse|sycophan|d[ée]construction|paradoxe|dialectique|ontologie|capitalisme|n[ée]olib[ée]ral|d[ée]mocratie|biais|illusion/.test(haystack)) {
|
|
||||||
return 'concept'
|
|
||||||
}
|
|
||||||
// RESSOURCE : par défaut (articles brouillon, idées, agendas)
|
|
||||||
return 'ressource'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback parser for legacy "MOC : [[X]]\nSource : ...\nDate : ...\n***\n" headers.
|
const MDCS_THEMATIQUES = new Set([
|
||||||
function parseLegacyHeader(raw) {
|
'art-narration',
|
||||||
const lines = raw.split(/\r?\n/)
|
'sante-globale',
|
||||||
const fm = {}
|
'spiritualite',
|
||||||
let bodyStart = 0
|
'ia-technologie',
|
||||||
let foundHeader = false
|
'anthropocene',
|
||||||
|
])
|
||||||
for (let i = 0; i < Math.min(lines.length, 30); i++) {
|
|
||||||
const line = lines[i]
|
|
||||||
if (/^\s*\*\*\*\s*$/.test(line)) {
|
|
||||||
bodyStart = i + 1
|
|
||||||
foundHeader = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
const m = line.match(/^([A-Za-zÀ-ÿ ]+)\s*:\s*(.+)$/)
|
|
||||||
if (m) {
|
|
||||||
const key = m[1].trim().toLowerCase()
|
|
||||||
fm[key] = m[2].trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundHeader) return { data: {}, content: raw }
|
|
||||||
|
|
||||||
// Parse tags (space-separated #tag tokens).
|
|
||||||
if (fm.tags) {
|
|
||||||
fm.tags = fm.tags.match(/#[\w/-]+/g) || []
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
data: fm,
|
|
||||||
content: lines.slice(bodyStart).join('\n'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeParseFrontmatter(raw) {
|
|
||||||
// Try YAML first.
|
|
||||||
try {
|
|
||||||
const parsed = matter(raw)
|
|
||||||
if (Object.keys(parsed.data).length > 0) return parsed
|
|
||||||
} catch (_) {
|
|
||||||
// YAML parse failed, fall through.
|
|
||||||
}
|
|
||||||
return parseLegacyHeader(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFirstParagraph(content, maxLen = 220) {
|
|
||||||
// Skip headings, code blocks, callout/quote markers.
|
|
||||||
const cleaned = content
|
|
||||||
.replace(/^---[\s\S]*?---\n/, '')
|
|
||||||
.replace(/```[\s\S]*?```/g, '')
|
|
||||||
.split(/\n\n+/)
|
|
||||||
.map(p => p.trim())
|
|
||||||
.filter(p => p && !p.startsWith('#') && !p.startsWith('---') && !p.startsWith('|'))
|
|
||||||
// Prefer first paragraph that looks like prose (not a list).
|
|
||||||
const prose = cleaned.find(p => !/^\s*[->*•\d+\.\s]/.test(p) && p.length > 30)
|
|
||||||
const first = prose || cleaned[0] || ''
|
|
||||||
// Strip wikilinks, bold, italics, callout markers.
|
|
||||||
return first
|
|
||||||
.replace(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g, '$1')
|
|
||||||
.replace(/[*_>]+/g, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
.slice(0, maxLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractWikilinks(content) {
|
|
||||||
// Match [[Target]], [[Target|alias]], [[Target#section]]
|
|
||||||
// Skip image embeds ![[...]]
|
|
||||||
const matches = [...content.matchAll(/(?<!!)\[\[([^\]|#]+)(?:[#|][^\]]*)?\]\]/g)]
|
|
||||||
return matches.map(m => m[1].trim()).filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('[carte-o] Scraping', ARTICLES_ROOT)
|
const raw = await fs.readFile(SOURCE, 'utf-8')
|
||||||
|
const data = yaml.load(raw)
|
||||||
// Glob all .md recursively under Articles/.
|
|
||||||
const mdFiles = await globby(['**/*.md'], {
|
|
||||||
cwd: ARTICLES_ROOT,
|
|
||||||
absolute: true,
|
|
||||||
gitignore: false,
|
|
||||||
})
|
|
||||||
console.log(`[carte-o] Found ${mdFiles.length} markdown files`)
|
|
||||||
|
|
||||||
const nodes = []
|
const nodes = []
|
||||||
const edgesRaw = []
|
const edges = []
|
||||||
const themeStats = {}
|
|
||||||
|
|
||||||
for (const mdFile of mdFiles) {
|
|
||||||
let raw
|
|
||||||
try {
|
|
||||||
raw = await fs.readFile(mdFile, 'utf-8')
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`[carte-o] Skip unreadable ${mdFile}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const relPath = path.relative(ARTICLES_ROOT, mdFile).replace(/\\/g, '/')
|
|
||||||
const segments = relPath.split('/')
|
|
||||||
const baseName = path.basename(mdFile, '.md')
|
|
||||||
|
|
||||||
// Theme = first or second segment depending on structure.
|
|
||||||
// E.g. "AEP ARTICLES, BROUILLON/AEP IA/file.md" -> theme = "AEP IA"
|
|
||||||
// "AEP ARTICLES, BROUILLON/file.md" -> theme = "AEP ARTICLES, BROUILLON"
|
|
||||||
// "Livre - le nouveau contrat social/file.md" -> theme = "Livre"
|
|
||||||
let theme = segments[0]
|
|
||||||
if (segments.length >= 3 && segments[0].startsWith('AEP ARTICLES')) {
|
|
||||||
theme = segments[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: fm, content } = safeParseFrontmatter(raw)
|
|
||||||
const title = fm.titre || fm.title || baseName.replace(/^!\s*/, '').trim()
|
|
||||||
const slug = slugify(title)
|
|
||||||
const family = inferFamily({
|
|
||||||
title,
|
|
||||||
theme,
|
|
||||||
path: relPath,
|
|
||||||
tags: fm.tags,
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
const intention = fm.intention || extractFirstParagraph(content)
|
|
||||||
|
|
||||||
nodes.push({
|
|
||||||
id: slug,
|
|
||||||
label: title,
|
|
||||||
family,
|
|
||||||
intention,
|
|
||||||
slug,
|
|
||||||
theme,
|
|
||||||
path: relPath,
|
|
||||||
})
|
|
||||||
|
|
||||||
themeStats[theme] = (themeStats[theme] || 0) + 1
|
|
||||||
|
|
||||||
// Collect wikilinks.
|
|
||||||
const wikilinks = extractWikilinks(content)
|
|
||||||
for (const target of wikilinks) {
|
|
||||||
edgesRaw.push({ source: slug, targetLabel: target })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduplicate nodes by id (collisions on same slug).
|
|
||||||
const nodeById = new Map()
|
|
||||||
for (const n of nodes) {
|
|
||||||
if (!nodeById.has(n.id)) {
|
|
||||||
nodeById.set(n.id, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dedupNodes = [...nodeById.values()]
|
|
||||||
|
|
||||||
// Resolve edges : match targetLabel against node label or id.
|
|
||||||
const labelToId = new Map()
|
|
||||||
for (const n of dedupNodes) {
|
|
||||||
labelToId.set(slugify(n.label), n.id)
|
|
||||||
labelToId.set(n.label.toLowerCase(), n.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const edgesResolved = []
|
|
||||||
const edgeSet = new Set()
|
const edgeSet = new Set()
|
||||||
for (const e of edgesRaw) {
|
|
||||||
const candidates = [
|
function addEdge(source, target) {
|
||||||
slugify(e.targetLabel),
|
const key = source < target ? `${source}|${target}` : `${target}|${source}`
|
||||||
e.targetLabel.toLowerCase(),
|
if (edgeSet.has(key)) return
|
||||||
]
|
|
||||||
let targetId = null
|
|
||||||
for (const c of candidates) {
|
|
||||||
if (labelToId.has(c)) {
|
|
||||||
targetId = labelToId.get(c)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!targetId || targetId === e.source) continue
|
|
||||||
const key = e.source < targetId ? `${e.source}→${targetId}` : `${targetId}→${e.source}`
|
|
||||||
if (edgeSet.has(key)) continue
|
|
||||||
edgeSet.add(key)
|
edgeSet.add(key)
|
||||||
edgesResolved.push({ source: e.source, target: targetId })
|
edges.push({ source, target })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute degree for each node.
|
function addNode(obj) {
|
||||||
const degree = new Map()
|
nodes.push({
|
||||||
for (const e of edgesResolved) {
|
id: obj.id,
|
||||||
degree.set(e.source, (degree.get(e.source) || 0) + 1)
|
label: obj.label,
|
||||||
degree.set(e.target, (degree.get(e.target) || 0) + 1)
|
niveau: obj.niveau,
|
||||||
|
nature: obj.nature,
|
||||||
|
statut: obj.statut,
|
||||||
|
resume: obj.resume || null,
|
||||||
|
radius: getRadius(obj.niveau, obj.nature),
|
||||||
|
family: getFamily(obj.nature),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// V1 cap : if > NODE_CAP_V1 nodes, keep top N by degree.
|
const centreId = data.centre.id
|
||||||
let finalNodes = dedupNodes
|
addNode(data.centre)
|
||||||
if (dedupNodes.length > NODE_CAP_V1) {
|
|
||||||
finalNodes = [...dedupNodes]
|
for (const cf of data.concepts_force) {
|
||||||
.sort((a, b) => (degree.get(b.id) || 0) - (degree.get(a.id) || 0))
|
addNode(cf)
|
||||||
.slice(0, NODE_CAP_V1)
|
addEdge(centreId, cf.id)
|
||||||
console.log(`[carte-o] Capped from ${dedupNodes.length} to ${NODE_CAP_V1} nodes (top by degree)`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalNodeIds = new Set(finalNodes.map(n => n.id))
|
for (const th of data.thematiques) {
|
||||||
const finalEdges = edgesResolved.filter(e => finalNodeIds.has(e.source) && finalNodeIds.has(e.target))
|
addNode(th)
|
||||||
|
if (NCS_THEMATIQUES.has(th.id)) {
|
||||||
// Family distribution stats.
|
addEdge('ncs-politique', th.id)
|
||||||
const familyDist = {}
|
} else if (MDCS_THEMATIQUES.has(th.id)) {
|
||||||
for (const n of finalNodes) {
|
addEdge('medecine-corps-social', th.id)
|
||||||
familyDist[n.family] = (familyDist[n.family] || 0) + 1
|
} else if (CENTRE_THEMATIQUES.has(th.id)) {
|
||||||
|
addEdge(centreId, th.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const proj of data.projets) {
|
||||||
|
addNode(proj)
|
||||||
|
for (const thId of (proj.liens_thematiques || [])) {
|
||||||
|
addEdge(proj.id, thId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure output dir exists.
|
|
||||||
await fs.mkdir(path.dirname(OUTPUT), { recursive: true })
|
await fs.mkdir(path.dirname(OUTPUT), { recursive: true })
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
OUTPUT,
|
OUTPUT,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
meta: {
|
version: data.version,
|
||||||
generated: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
source: 'AEP/Articles',
|
nodes,
|
||||||
nodeCount: finalNodes.length,
|
edges,
|
||||||
edgeCount: finalEdges.length,
|
|
||||||
familyDistribution: familyDist,
|
|
||||||
familyColors: FAMILY_COLORS,
|
|
||||||
themeStats,
|
|
||||||
},
|
|
||||||
nodes: finalNodes,
|
|
||||||
edges: finalEdges,
|
|
||||||
}, null, 2),
|
}, null, 2),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(`[carte-o] OK : ${finalNodes.length} nodes / ${finalEdges.length} edges`)
|
console.log(`[carte-o] OK : ${nodes.length} nodes / ${edges.length} edges -> ${OUTPUT}`)
|
||||||
console.log(`[carte-o] Families :`, familyDist)
|
|
||||||
console.log(`[carte-o] Output : ${OUTPUT}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(err => {
|
main().catch(err => {
|
||||||
console.error('[carte-o] FAIL', err)
|
console.error('[carte-o] FAIL', err)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// V1 scrape vault - reactiver en V1.2 pour enrichissement automatique
|
||||||
|
// Source : scripts/build-carte-o.js@be7fc09 (scrape AEP/Articles globby + gray-matter + wikilinks edges)
|
||||||
|
|||||||
Reference in New Issue
Block a user