@lamemind/loom-deck 0.61.0 → 0.61.1
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/dist/commit-times.js +48 -2
- package/dist/epic-hierarchy.js +43 -11
- package/dist/hooks.js +21 -28
- package/package.json +1 -1
package/dist/commit-times.js
CHANGED
|
@@ -18,6 +18,32 @@ const FORMAT_RE = /^#(\d+)$/;
|
|
|
18
18
|
// Il basename del task file, non il path intero (che cambia con la docs-root)
|
|
19
19
|
// e non `TASK_ID_RE` di tasks.ts (ancorata all'id nudo, non matcha `T136-x.md`).
|
|
20
20
|
const TASK_FILE_RE = /^(T\d+)-.*\.md$/;
|
|
21
|
+
const EMPTY_MAP = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* T153 — gate module-level: `git log` gira solo se lo sha di HEAD è cambiato
|
|
24
|
+
* dall'ultima chiamata su questa stessa coppia (tasksDir, projectRoot). Le
|
|
25
|
+
* date di ultimo commit cambiano SOLO se HEAD si muove (§Implementation
|
|
26
|
+
* Notes T153): un rev-parse fallito degrada come degradava il log, mai un
|
|
27
|
+
* throw. `key` tiene le due coppie separate — un test che passa tmpdir
|
|
28
|
+
* diversi a chiamate successive non deve leggere la cache dell'altro.
|
|
29
|
+
*/
|
|
30
|
+
let cache = null;
|
|
31
|
+
let logSpawns = 0;
|
|
32
|
+
/** T153/DLV6 — quante volte è partito lo spawn COSTOSO (`git log`), non il
|
|
33
|
+
* gate. Il gate (`rev-parse`) gira a ogni chiamata per costruzione: contarlo
|
|
34
|
+
* renderebbe il numero inutile a dimostrare il no-op. */
|
|
35
|
+
export function commitLogSpawnCount() {
|
|
36
|
+
return logSpawns;
|
|
37
|
+
}
|
|
38
|
+
async function headSha(projectRoot) {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: projectRoot });
|
|
41
|
+
return stdout.trim();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
21
47
|
/**
|
|
22
48
|
* Parsing puro dell'output `git log --format=#%ct --name-only`. Isolato da
|
|
23
49
|
* `commitTimes` perché è l'unica parte che vale la pena collaudare: invocare
|
|
@@ -54,13 +80,33 @@ export function parseCommitLog(output) {
|
|
|
54
80
|
* incompleta proprio per le task vecchie mai più toccate, quelle che sotto
|
|
55
81
|
* `desc` finiscono in coda — dove un timestamp assente e uno vecchio si
|
|
56
82
|
* confonderebbero.
|
|
83
|
+
*
|
|
84
|
+
* T153 — gate su `rev-parse HEAD` (un ordine di grandezza più economico
|
|
85
|
+
* dello spawn del log che sostituisce, cifre misurate nel Progress Log): a
|
|
86
|
+
* sha invariato torna la STESSA istanza di mappa, non una ricostruita — è
|
|
87
|
+
* ciò che rende inutile il confronto per firma in `useCommitTimes` (P1
|
|
88
|
+
* preflight).
|
|
57
89
|
*/
|
|
58
90
|
export async function commitTimes(tasksDir, projectRoot) {
|
|
91
|
+
const key = `${projectRoot}\u0000${tasksDir}`;
|
|
92
|
+
const sha = await headSha(projectRoot);
|
|
93
|
+
if (sha !== null && cache && cache.key === key && cache.sha === sha) {
|
|
94
|
+
return cache.map;
|
|
95
|
+
}
|
|
96
|
+
let map;
|
|
59
97
|
try {
|
|
98
|
+
// Il contatore sale PRIMA dell'await: conta gli spawn partiti, e uno
|
|
99
|
+
// spawn fallito costa comunque la fork del processo.
|
|
100
|
+
logSpawns++;
|
|
60
101
|
const { stdout } = await execFileAsync('git', ['log', '--format=#%ct', '--name-only', '--', tasksDir], { cwd: projectRoot });
|
|
61
|
-
|
|
102
|
+
map = parseCommitLog(stdout);
|
|
62
103
|
}
|
|
63
104
|
catch {
|
|
64
|
-
|
|
105
|
+
map = EMPTY_MAP;
|
|
65
106
|
}
|
|
107
|
+
// sha === null (git muto): non si cachea uno stato che il prossimo giro
|
|
108
|
+
// potrebbe smentire in silenzio (repo che ricompare) senza mai vedersi
|
|
109
|
+
// ricontrollato.
|
|
110
|
+
cache = sha !== null ? { key, sha, map } : null;
|
|
111
|
+
return map;
|
|
66
112
|
}
|
package/dist/epic-hierarchy.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// parentela si edita anche a mano dentro un task file già in tabella, non solo
|
|
9
9
|
// alla nascita di una riga: un trigger sulla sola firma degli id (come
|
|
10
10
|
// `useArchivable`) lascerebbe quell'edit stale fino al prossimo giro largo.
|
|
11
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { taskEpicOf, taskIsEpic } from './tasks.js';
|
|
14
14
|
// Stesso pattern di `TASK_FILE_RE` in commit-times.ts: il basename del task
|
|
@@ -17,10 +17,23 @@ import { taskEpicOf, taskIsEpic } from './tasks.js';
|
|
|
17
17
|
const TASK_FILE_RE = /^(T\d+)-.*\.md$/;
|
|
18
18
|
export const EMPTY_EPIC_HIERARCHY = { epicOf: new Map(), epics: new Set() };
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* T153 — gate module-level: il read+parse dei task file gira solo se la mtime
|
|
21
|
+
* MASSIMA fra i file `T<N>-*.md` di `tasksDir` è cambiata da ultima chiamata
|
|
22
|
+
* (un ordine di grandezza più economico del parse pieno che sostituisce,
|
|
23
|
+
* cifre misurate nel Progress Log). Sulla SOLA mtime e non su una firma
|
|
24
|
+
* degli id: deve intercettare un edit a mano di
|
|
25
|
+
* `**Parent Task**` DENTRO un file già in tabella, che non aggiunge né toglie
|
|
26
|
+
* nessun id (Testing Notes T153). A gate scattato torna la STESSA istanza —
|
|
27
|
+
* `useEpicHierarchy` non ha più bisogno di una firma propria per lo stesso
|
|
28
|
+
* motivo di `useCommitTimes` (P1 preflight).
|
|
29
|
+
*/
|
|
30
|
+
let cache = null;
|
|
31
|
+
/**
|
|
32
|
+
* Lettura SINCRONA di tutti i task file sotto `tasksDir`: costo del gate
|
|
33
|
+
* accettabile sul poll da 1,5s anche a scan pieno (mai il caso in regime).
|
|
34
|
+
* Cartella illeggibile o singolo file non apribile → entry saltata, mai un
|
|
35
|
+
* throw — un dato di rendering non può rompere il deck, stessa regola di
|
|
36
|
+
* `archivableIds`/`commitTimes`.
|
|
24
37
|
*/
|
|
25
38
|
export function scanEpicHierarchy(tasksDir) {
|
|
26
39
|
let entries;
|
|
@@ -28,15 +41,32 @@ export function scanEpicHierarchy(tasksDir) {
|
|
|
28
41
|
entries = readdirSync(tasksDir);
|
|
29
42
|
}
|
|
30
43
|
catch {
|
|
44
|
+
cache = null;
|
|
31
45
|
return EMPTY_EPIC_HIERARCHY;
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
const
|
|
47
|
+
let maxMtime = 0;
|
|
48
|
+
const files = [];
|
|
35
49
|
for (const file of entries) {
|
|
36
|
-
|
|
37
|
-
if (!m)
|
|
50
|
+
if (!TASK_FILE_RE.test(file))
|
|
38
51
|
continue;
|
|
39
|
-
|
|
52
|
+
let mtime;
|
|
53
|
+
try {
|
|
54
|
+
mtime = statSync(join(tasksDir, file)).mtimeMs;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
files.push(file);
|
|
60
|
+
if (mtime > maxMtime)
|
|
61
|
+
maxMtime = mtime;
|
|
62
|
+
}
|
|
63
|
+
if (cache && cache.key === tasksDir && cache.maxMtime === maxMtime) {
|
|
64
|
+
return cache.hierarchy;
|
|
65
|
+
}
|
|
66
|
+
const epicOf = new Map();
|
|
67
|
+
const epics = new Set();
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const id = TASK_FILE_RE.exec(file)[1];
|
|
40
70
|
let content;
|
|
41
71
|
try {
|
|
42
72
|
content = readFileSync(join(tasksDir, file), 'utf8');
|
|
@@ -50,5 +80,7 @@ export function scanEpicHierarchy(tasksDir) {
|
|
|
50
80
|
if (parent)
|
|
51
81
|
epicOf.set(id, parent);
|
|
52
82
|
}
|
|
53
|
-
|
|
83
|
+
const hierarchy = { epicOf, epics };
|
|
84
|
+
cache = { key: tasksDir, maxMtime, hierarchy };
|
|
85
|
+
return hierarchy;
|
|
54
86
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -72,28 +72,26 @@ export function useTasks(tasksPath) {
|
|
|
72
72
|
/**
|
|
73
73
|
* T136 — data dell'ultimo commit di ogni task file, per la chiave di sort
|
|
74
74
|
* `commit`. D1 (preflight): sullo STESSO poll di `tasks.md` (`POLL_MS`), non
|
|
75
|
-
* su una scala propria come `useArchivable
|
|
76
|
-
* ~33ms ed è UNA invocazione per tick, indipendentemente dal numero di task.
|
|
75
|
+
* su una scala propria come `useArchivable`.
|
|
77
76
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* `
|
|
77
|
+
* T153 — `commitTimes` gatea da sé su `rev-parse HEAD` (davanti allo spawn
|
|
78
|
+
* caro del log, vedi `commit-times.ts`) e a sha invariato torna la STESSA
|
|
79
|
+
* istanza di mappa. L'hook confronta quindi per IDENTITÀ, non più per firma
|
|
80
|
+
* ricostruita: la firma evitava solo il re-render, il gate a monte evita il
|
|
81
|
+
* lavoro. `lastMtime` in `useTasks` resta il gemello sullo stesso schema.
|
|
83
82
|
*/
|
|
84
83
|
export function useCommitTimes(tasksDir, projectRoot) {
|
|
85
84
|
const [commitAt, setCommitAt] = useState(() => new Map());
|
|
86
85
|
useEffect(() => {
|
|
87
|
-
let
|
|
86
|
+
let last = null;
|
|
88
87
|
let alive = true;
|
|
89
88
|
const reload = () => {
|
|
90
89
|
commitTimes(tasksDir, projectRoot).then((next) => {
|
|
91
90
|
if (!alive)
|
|
92
91
|
return;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
lastSig = sig;
|
|
92
|
+
if (next === last)
|
|
93
|
+
return; // stessa istanza → il gate non è scattato
|
|
94
|
+
last = next;
|
|
97
95
|
setCommitAt(next);
|
|
98
96
|
});
|
|
99
97
|
};
|
|
@@ -108,29 +106,24 @@ export function useCommitTimes(tasksDir, projectRoot) {
|
|
|
108
106
|
}
|
|
109
107
|
/**
|
|
110
108
|
* T67 — mappa cappello/figlie (`**Parent Task**`, `Size: Epic`), sullo STESSO
|
|
111
|
-
* poll di `tasks.md` e non su una scala propria
|
|
112
|
-
* per il read+parse di 106 task file, meno dei ~33 ms di `git log` che
|
|
113
|
-
* `useCommitTimes` paga già ogni tick. La parentela si edita anche a mano su
|
|
114
|
-
* un task file già in tabella — un trigger sulla sola firma degli id, come
|
|
115
|
-
* `useArchivable`, non vedrebbe quell'edit e lo lascerebbe stale fino al
|
|
116
|
-
* prossimo giro largo.
|
|
109
|
+
* poll di `tasks.md` e non su una scala propria.
|
|
117
110
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
111
|
+
* T153 — `scanEpicHierarchy` gatea da sé sulla mtime massima dei task file
|
|
112
|
+
* (davanti al read+parse caro, vedi `epic-hierarchy.ts`) e a mtime invariata
|
|
113
|
+
* torna la STESSA istanza: la parentela editata a mano dentro un file già in
|
|
114
|
+
* tabella resta vista, perché il gate è sulla mtime e non sugli id. L'hook
|
|
115
|
+
* confronta quindi per IDENTITÀ, non più per firma ricostruita, gemello di
|
|
116
|
+
* `useCommitTimes` per lo stesso motivo (P1 preflight).
|
|
121
117
|
*/
|
|
122
118
|
export function useEpicHierarchy(tasksDir) {
|
|
123
119
|
const [hierarchy, setHierarchy] = useState(EMPTY_EPIC_HIERARCHY);
|
|
124
120
|
useEffect(() => {
|
|
125
|
-
let
|
|
121
|
+
let last = null;
|
|
126
122
|
const reload = () => {
|
|
127
123
|
const next = scanEpicHierarchy(tasksDir);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (sig === lastSig)
|
|
132
|
-
return;
|
|
133
|
-
lastSig = sig;
|
|
124
|
+
if (next === last)
|
|
125
|
+
return; // stessa istanza → il gate non è scattato
|
|
126
|
+
last = next;
|
|
134
127
|
setHierarchy(next);
|
|
135
128
|
};
|
|
136
129
|
reload();
|
package/package.json
CHANGED