@noeva/analytics-core 0.3.3 → 0.4.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/CHANGELOG.md +29 -0
- package/contenuto/assets/agenti.js +27 -10
- package/contenuto/assets/base.css +23 -0
- package/contenuto/assets/chat.js +13 -7
- package/contenuto/assets/commento.js +52 -15
- package/contenuto/assets/risposte.js +166 -0
- package/contenuto/assets/tema.js +4 -1
- package/contenuto/assets/vista.js +252 -0
- package/contenuto/assets/voce.js +127 -0
- package/contenuto/lib/mcp-locale/cartella/CLAUDE.md +17 -31
- package/contenuto/lib/mcp-locale/costruisci.py +14 -23
- package/contenuto/lib/modello/consegna.py +10 -1
- package/contenuto/lib/modello/istruzioni.json +14 -0
- package/contenuto/lib/modello/istruzioni.py +48 -0
- package/contenuto/lib/modello/pubblica.py +8 -2
- package/contenuto/lib/modello/scheda.py +146 -0
- package/contenuto/lib/snapshot/ai.py +10 -2
- package/contenuto/lib/snapshot/esporta.py +4 -1
- package/contenuto/lib/snapshot/manifesto.py +23 -1
- package/core.js +11 -0
- package/package.json +1 -1
|
@@ -60,11 +60,17 @@ def versione_di(radice: pathlib.Path) -> str:
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def estrai_da_head(radice: pathlib.Path, dest: pathlib.Path) -> None:
|
|
63
|
-
"""I file del modello com'erano nel commit, senza toccare il disco del repository.
|
|
64
|
-
|
|
63
|
+
"""I file del modello com'erano nel commit, senza toccare il disco del repository.
|
|
64
|
+
|
|
65
|
+
Con loro quelli da cui si genera la scheda del progetto (scheda.py): analytics.json
|
|
66
|
+
e le index.html di apps/, per il titolo del progetto e delle dashboard."""
|
|
67
|
+
presenti = [p for p in [*FILE_MODELLO, "consegna.yml", "analytics.json"]
|
|
68
|
+
if git(radice, "ls-tree", "--name-only", "HEAD", p).strip()]
|
|
65
69
|
mancanti = {"wren_project.yml", "relationships.yml", "models"} - set(presenti)
|
|
66
70
|
if mancanti:
|
|
67
71
|
raise SystemExit(f"nel commit mancano {sorted(mancanti)}: non e' il repository di un progetto con un modello")
|
|
72
|
+
presenti += [p for p in git(radice, "ls-tree", "-r", "--name-only", "HEAD", "apps").splitlines()
|
|
73
|
+
if p.count("/") <= 2 and p.endswith("/index.html")]
|
|
68
74
|
archivio = subprocess.run(["git", "archive", "--format=tar", "HEAD", *presenti], cwd=radice,
|
|
69
75
|
check=True, capture_output=True).stdout
|
|
70
76
|
with tempfile.TemporaryFile() as t:
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""La scheda del progetto: la regola knowledge/rules/00-progetto.md, generata (NW-1286).
|
|
2
|
+
|
|
3
|
+
Il plugin noeva-analytics porta il metodo, uguale per tutti; il server MCP del
|
|
4
|
+
progetto porta cio' che e' vero del progetto (NW-1261). La scheda e' la parte di
|
|
5
|
+
quel vero che serve prima di ogni domanda: di chi sono i dati, cosa coprono,
|
|
6
|
+
dove stanno le dashboard, chi e' il contatto in 4D. La skill `analisi` la trova
|
|
7
|
+
con get_instructions.
|
|
8
|
+
|
|
9
|
+
Si genera dal repository del progetto, mai a mano: modello_di_consegna (consegna.py)
|
|
10
|
+
la scrive nel modello che si consegna, e da li' arriva uguale al modello pubblicato
|
|
11
|
+
su R2, al servizio locale che compila il repository aperto e alla cartella
|
|
12
|
+
consegnata. Nel repository non c'e': una scheda in git sarebbe una copia da tenere
|
|
13
|
+
allineata, e prima o poi qualcuno la ritoccherebbe.
|
|
14
|
+
|
|
15
|
+
Legge, tutti facoltativi tranne analytics.json:
|
|
16
|
+
|
|
17
|
+
analytics.json l'id del progetto
|
|
18
|
+
consegna.yml cliente, descrizione, contatto_4d, la `cosa` di ogni sorgente,
|
|
19
|
+
modello_r2.url (il contenitore, default https://analytics.noeva.ai)
|
|
20
|
+
wren_project.yml il tipo di database
|
|
21
|
+
models/ quanti modelli
|
|
22
|
+
apps/index.html il titolo del progetto, che e' quello dell'indice
|
|
23
|
+
apps/*/index.html le dashboard, col titolo della pagina
|
|
24
|
+
|
|
25
|
+
Il registro dei progetti (lib/rilascio/progetti.json) sta nel monorepo di 4D e il
|
|
26
|
+
repository di un cliente non lo vede (il pacchetto non nomina clienti, ADR 0005):
|
|
27
|
+
la scheda dice solo cio' che il repository del progetto sa di se'.
|
|
28
|
+
|
|
29
|
+
Nessuna data e nessuna versione dentro: la stessa sorgente da' la stessa scheda,
|
|
30
|
+
byte per byte, da qualunque strada passi.
|
|
31
|
+
|
|
32
|
+
python3 lib/modello/scheda.py <repository> stampa la scheda
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import html
|
|
37
|
+
import json
|
|
38
|
+
import pathlib
|
|
39
|
+
import re
|
|
40
|
+
import sys
|
|
41
|
+
|
|
42
|
+
import yaml
|
|
43
|
+
|
|
44
|
+
FILE = "00-progetto.md"
|
|
45
|
+
CONTENITORE = "https://analytics.noeva.ai"
|
|
46
|
+
# Le cartelle di apps/ che non sono dashboard: il motore e i moduli di pagina.
|
|
47
|
+
NON_DASHBOARD = {"assets", "dati", "vendor"}
|
|
48
|
+
TITOLO = re.compile(r"<title>(.*?)</title>", re.S | re.I)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _leggi_yaml(p: pathlib.Path) -> dict:
|
|
52
|
+
return (yaml.safe_load(p.read_text(encoding="utf-8")) or {}) if p.is_file() else {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _titolo(p: pathlib.Path) -> str | None:
|
|
56
|
+
if not p.is_file():
|
|
57
|
+
return None
|
|
58
|
+
m = TITOLO.search(p.read_text(encoding="utf-8", errors="replace"))
|
|
59
|
+
return " ".join(html.unescape(m.group(1)).split()) if m else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def identificativo(sorgente: pathlib.Path) -> str:
|
|
63
|
+
"""L'id del progetto: analytics.json, o il nome della cartella project-<id> del monorepo."""
|
|
64
|
+
f = sorgente / "analytics.json"
|
|
65
|
+
if f.is_file():
|
|
66
|
+
return json.loads(f.read_text(encoding="utf-8"))["progetto"]
|
|
67
|
+
return sorgente.name.removeprefix("project-")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def dashboard(sorgente: pathlib.Path) -> list[tuple[str, str]]:
|
|
71
|
+
"""(cartella, titolo) delle dashboard in apps/, in ordine di cartella."""
|
|
72
|
+
apps = sorgente / "apps"
|
|
73
|
+
if not apps.is_dir():
|
|
74
|
+
return []
|
|
75
|
+
esito = []
|
|
76
|
+
for d in sorted(p for p in apps.iterdir() if p.is_dir() and p.name not in NON_DASHBOARD):
|
|
77
|
+
t = _titolo(d / "index.html")
|
|
78
|
+
if t:
|
|
79
|
+
esito.append((d.name, t))
|
|
80
|
+
return esito
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def scheda(sorgente: pathlib.Path, conf: dict | None = None) -> str:
|
|
84
|
+
"""Il testo di 00-progetto.md per il repository in `sorgente`.
|
|
85
|
+
|
|
86
|
+
`conf` e' consegna.yml intero (default: quello di `sorgente`); chi costruisce
|
|
87
|
+
con un'altra configurazione (costruisci.py --config) la passa qui."""
|
|
88
|
+
conf = _leggi_yaml(sorgente / "consegna.yml") if conf is None else conf
|
|
89
|
+
ident = identificativo(sorgente)
|
|
90
|
+
progetto = _leggi_yaml(sorgente / "wren_project.yml")
|
|
91
|
+
titolo = _titolo(sorgente / "apps" / "index.html")
|
|
92
|
+
cliente = conf.get("cliente") or titolo or ident
|
|
93
|
+
base = f"{((conf.get('modello_r2') or {}).get('url') or CONTENITORE).rstrip('/')}/{ident}/"
|
|
94
|
+
modelli = sorted(p.parent.name for p in (sorgente / "models").glob("*/metadata.yml"))
|
|
95
|
+
sorgenti = [(k, s.get("cosa")) for k, s in (conf.get("sorgenti") or {}).items() if s.get("cosa")]
|
|
96
|
+
|
|
97
|
+
r = [
|
|
98
|
+
f"# Il progetto: {cliente}",
|
|
99
|
+
"",
|
|
100
|
+
"Scheda generata dal repository del progetto insieme al modello che si consegna:",
|
|
101
|
+
"non si scrive a mano e si rigenera con il modello. Dice di chi sono i dati, cosa",
|
|
102
|
+
"coprono, dove stanno le dashboard e chi e' il contatto in 4D; cosa significano i",
|
|
103
|
+
"numeri lo dicono le altre regole.",
|
|
104
|
+
"",
|
|
105
|
+
f"- **Cliente**: {cliente}",
|
|
106
|
+
f"- **Progetto NOEVA Analytics**: `{ident}`" + (f", «{titolo}»" if titolo and titolo != cliente else ""),
|
|
107
|
+
]
|
|
108
|
+
if conf.get("descrizione"):
|
|
109
|
+
r.append(f"- **In breve**: {' '.join(str(conf['descrizione']).split())}")
|
|
110
|
+
r += ["", "## Cosa coprono i dati", ""]
|
|
111
|
+
if len(sorgenti) == 1:
|
|
112
|
+
r.append(f"- {sorgenti[0][1]}")
|
|
113
|
+
else:
|
|
114
|
+
r += [f"- `{k}`: {cosa}" for k, cosa in sorgenti]
|
|
115
|
+
db = progetto.get("data_source")
|
|
116
|
+
r.append(f"- Il modello: {len(modelli)} modelli" + (f" su {db}" if db else "")
|
|
117
|
+
+ "; l'elenco con list_models, il significato con describe_model.")
|
|
118
|
+
if not sorgenti:
|
|
119
|
+
r.append("- La copertura per sorgente non e' descritta (`cosa` in consegna.yml): la dicono "
|
|
120
|
+
"le regole e la descrizione dei modelli.")
|
|
121
|
+
|
|
122
|
+
r += ["", "## Le dashboard", "",
|
|
123
|
+
"Dietro il login NOEVA: le apre chi ha il progetto nel suo workspace.", "",
|
|
124
|
+
f"- Indice: {base}"]
|
|
125
|
+
r += [f"- {t}: {base}{cartella}/" for cartella, t in dashboard(sorgente)]
|
|
126
|
+
|
|
127
|
+
r += ["", "## Il contatto in 4D", ""]
|
|
128
|
+
contatto = conf.get("contatto_4d")
|
|
129
|
+
if contatto:
|
|
130
|
+
r.append(f"- {' '.join(str(contatto).split())}")
|
|
131
|
+
else:
|
|
132
|
+
r.append("- Non indicato (`contatto_4d` in consegna.yml).")
|
|
133
|
+
r.append("- Per scrivere a 4D (una quadratura che non torna, una modifica) c'e' la Richiesta a 4D.")
|
|
134
|
+
return "\n".join(r) + "\n"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def scrivi(sorgente: pathlib.Path, rules: pathlib.Path, conf: dict | None = None) -> pathlib.Path:
|
|
138
|
+
"""Scrive la scheda in `rules`/00-progetto.md, sopra un'eventuale copia a mano."""
|
|
139
|
+
rules.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
f = rules / FILE
|
|
141
|
+
f.write_text(scheda(sorgente, conf), encoding="utf-8")
|
|
142
|
+
return f
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
sys.stdout.write(scheda(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()))
|
|
@@ -40,12 +40,18 @@ def carica_commento(progetto: pathlib.Path):
|
|
|
40
40
|
modulo = importlib.util.module_from_spec(spec)
|
|
41
41
|
sys.path.insert(0, str(progetto))
|
|
42
42
|
spec.loader.exec_module(modulo)
|
|
43
|
-
for nome in ("
|
|
43
|
+
for nome in ("conoscenza", "LINGUE"):
|
|
44
44
|
if not hasattr(modulo, nome):
|
|
45
45
|
raise SystemExit(f"{progetto.name}/commento.py non ha {nome}: non e' allineato allo standard")
|
|
46
46
|
return modulo
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def ha_tour(c) -> bool:
|
|
50
|
+
"""Il commento guidato e' facoltativo: un progetto con la sola chat ha in
|
|
51
|
+
commento.py la conoscenza ma non istruzioni, modi e schema del tour."""
|
|
52
|
+
return all(hasattr(c, nome) for nome in ("istruzioni", "MODI", "SCHEMA"))
|
|
53
|
+
|
|
54
|
+
|
|
49
55
|
def istruzioni_sito(chat, lingua: str) -> str:
|
|
50
56
|
"""Le istruzioni della chat per il sito: li' ha solo run_sql sullo snapshot, non recall_queries,
|
|
51
57
|
quindi senza la riga della memoria delle domande confermate (GUIDA_MEMORIA di chat.py)."""
|
|
@@ -84,7 +90,7 @@ def main(argv: list[str]) -> int:
|
|
|
84
90
|
uscita.mkdir(parents=True, exist_ok=True)
|
|
85
91
|
adesso = dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds")
|
|
86
92
|
scritti = 0
|
|
87
|
-
for pagina in pagine(progetto):
|
|
93
|
+
for pagina in pagine(progetto) if ha_tour(c) else []:
|
|
88
94
|
for lingua in c.LINGUE:
|
|
89
95
|
sistema = c.istruzioni(lingua) + "\n\n# Knowledge base del progetto\n\n" + c.conoscenza(pagina)
|
|
90
96
|
(uscita / f"commento-{pagina}.{lingua}.json").write_text(json.dumps(
|
|
@@ -104,6 +110,8 @@ def main(argv: list[str]) -> int:
|
|
|
104
110
|
"generato": adesso},
|
|
105
111
|
ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
|
106
112
|
n_chat += 1
|
|
113
|
+
if not scritti and not n_chat:
|
|
114
|
+
raise SystemExit(f"{progetto.name}: ne' commento guidato ne' chat, nessun prompt da scrivere")
|
|
107
115
|
print(f" apps/dati/ai: {scritti} prompt del commento, {n_chat} della chat ({len(c.LINGUE)} lingue)")
|
|
108
116
|
return 0
|
|
109
117
|
|
|
@@ -101,6 +101,8 @@ import pyarrow as pa
|
|
|
101
101
|
import pyarrow.parquet as pq
|
|
102
102
|
import yaml
|
|
103
103
|
|
|
104
|
+
from manifesto import id_progetto
|
|
105
|
+
|
|
104
106
|
ROMA = ZoneInfo("Europe/Rome")
|
|
105
107
|
|
|
106
108
|
|
|
@@ -403,7 +405,8 @@ def main(argv: list[str]) -> int:
|
|
|
403
405
|
adesso = dt.datetime.now(ROMA).isoformat(timespec="seconds")
|
|
404
406
|
costanti = {**(conf.get("costanti") or {}), **opt["costanti"]}
|
|
405
407
|
manifesto = {
|
|
406
|
-
|
|
408
|
+
# l'id nel contenitore (`<id>`, non la cartella): la Vista della pagina lo dichiara (standard §5)
|
|
409
|
+
"progetto": id_progetto(progetto),
|
|
407
410
|
"generato": adesso,
|
|
408
411
|
"data_del_dato": data_del_dato,
|
|
409
412
|
"sorgente": sorgente,
|
|
@@ -19,6 +19,27 @@ import pathlib
|
|
|
19
19
|
import sys
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def id_progetto(cartella: pathlib.Path) -> str:
|
|
23
|
+
"""L'id del progetto nel contenitore (`<id>`), non il nome della cartella.
|
|
24
|
+
|
|
25
|
+
Il repository di un progetto (ADR 0005) lo scrive in analytics.json, alla sua
|
|
26
|
+
radice: si cerca risalendo da `cartella` (il progetto, o la sua apps/dati).
|
|
27
|
+
Senza, il nome della cartella del progetto senza il prefisso: `project-<id>`
|
|
28
|
+
nel monorepo, `noeva-analytics-<id>` per un clone del repository, `<id>` per
|
|
29
|
+
il pacchetto del worker."""
|
|
30
|
+
cartella = cartella.resolve()
|
|
31
|
+
for d in (cartella, *cartella.parents):
|
|
32
|
+
f = d / "analytics.json"
|
|
33
|
+
if f.is_file():
|
|
34
|
+
voce = json.loads(f.read_text(encoding="utf-8")).get("progetto")
|
|
35
|
+
if voce:
|
|
36
|
+
return voce
|
|
37
|
+
break
|
|
38
|
+
if cartella.name == "dati" and cartella.parent.name == "apps":
|
|
39
|
+
cartella = cartella.parent.parent
|
|
40
|
+
return cartella.name.removeprefix("project-").removeprefix("noeva-analytics-")
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
def main(argv: list[str]) -> int:
|
|
23
44
|
if len(argv) < 2:
|
|
24
45
|
print(__doc__)
|
|
@@ -36,7 +57,8 @@ def main(argv: list[str]) -> int:
|
|
|
36
57
|
generato = json.loads(f.read_text(encoding="utf-8")).get("generato")
|
|
37
58
|
data_del_dato = str(generato)[:10] if generato else None
|
|
38
59
|
generato = dt.datetime.fromtimestamp(piu_recente).astimezone().isoformat(timespec="seconds")
|
|
39
|
-
manifesto = {"
|
|
60
|
+
manifesto = {"progetto": id_progetto(cartella), "generato": generato,
|
|
61
|
+
"data_del_dato": data_del_dato or generato[:10], "file": voci}
|
|
40
62
|
(cartella / "snapshot.json").write_text(json.dumps(manifesto, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
41
63
|
print(f" snapshot.json: {len(voci)} file, dato al {manifesto['data_del_dato']}")
|
|
42
64
|
return 0
|
package/core.js
CHANGED
|
@@ -247,6 +247,17 @@ export function verifica(radice, config) {
|
|
|
247
247
|
errori.push(`file gestito ${d.stato}: ${d.percorso} — lo riscrive \`analytics upgrade\`, o va in non_gestiti`);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// Le risposte alle domande si registrano in un modo solo (standard §6.4): la
|
|
251
|
+
// pagina del progetto che mostra le domande usa risposte.js, che salva sul Mac,
|
|
252
|
+
// online dietro il cancello, e dove non si salva esporta su file.
|
|
253
|
+
const paginaProgetto = join(radice, 'apps', 'progetto');
|
|
254
|
+
if (existsSync(paginaProgetto)) {
|
|
255
|
+
const testi = fileSotto(paginaProgetto).filter((f) => /\.(html|js)$/.test(f)).map((f) => readFileSync(f, 'utf8'));
|
|
256
|
+
if (testi.some((x) => x.includes('lista-domande')) && !testi.some((x) => x.includes('montaRisposte'))) {
|
|
257
|
+
errori.push('apps/progetto mostra le domande ma non registra le risposte con montaRisposte di apps/assets/risposte.js (standard §6.4)');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
250
261
|
const tracciati = trackedDaGit(radice);
|
|
251
262
|
if (tracciati) {
|
|
252
263
|
for (const f of tracciati) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noeva/analytics-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Gli strumenti di un progetto NOEVA Analytics nel suo repository: moduli di pagina, snapshot, verifica dello standard e kit dell'agente (ADR 0005).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|