feat: Effektiver Status — Knotenfarbe zeigt ihn, Diskrepanz-Marke unten links (SPEC §4/§9, D39)
Jeder Status bekommt einen Fortschritts-Rang entlang der Ergebnis-Skala (D5); effektiv ist das Minimum über die Abhängigkeits-Hülle (Fixpunkt — Zyklen teilen ihr Minimum, wörtlich das „wird gemeinsam fertig" aus D34). Neutral und [-] zählen als 0, [!] als 1; bei doppelter ID gilt die erste Vergabe. Entschieden (Nutzer): Die Knotenfarbe zeigt den effektiven Status — das Diagramm beantwortet „wie weit ist das wirklich?". Wo der eigene Status weiter ist, sitzt unten links die eigene Statusbox als Marke in den eigenen §4-Farben (auch im Export/Druck); Tooltip und a11yEffective benennen beides. XOR-„realisiert" und „Was ist neu?" bleiben bewusst intrinsisch. i18n in allen 9 Sprachen; 12 neue Tests (tests/effective.test.js). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cbe798d746
commit
15c3e85e26
@@ -1834,3 +1834,43 @@ Stiel-Messung, Treppe und Pfadlinie sind damit ohne Zusatzcode konsistent
|
||||
per CSS zu verstecken). Die Kennzeichnung „▸ n“ ist Teil des Knotentexts und
|
||||
wandert von selbst in den SVG-Export; das ▾ offener Knoten wird dort und im
|
||||
Druck entfernt — es ist Bedienelement, keine Aussage über den Plan.
|
||||
|
||||
## D39 — Effektiver Status: die Farbe sagt die Wahrheit, die Marke die Box
|
||||
Mit den Abhängigkeiten (D37) gibt es zwei Aussagen je Knoten: was in der
|
||||
Statusbox steht (intrinsisch) und wie weit er wirklich ist (effektiv, §4).
|
||||
Erwogen waren drei Darstellungen — Diskrepanz-Kennzeichnung bei intrinsischer
|
||||
Farbe (die ursprüngliche Empfehlung), ein Umschalter, oder Farbe = effektiv.
|
||||
**Entschieden (Nutzer): Die Knotenfarbe zeigt den effektiven Status; wo der
|
||||
eigene Status weiter ist, kommt eine Diskrepanz-Marke dazu.** Das ist die
|
||||
stärkere Wahl: Das Diagramm beantwortet „wie weit ist das wirklich?“ — genau
|
||||
die Frage, für die man auf einen Plan schaut. Ein grünes `[x]`, das auf ein
|
||||
rosé `[~]` wartet, wäre die hübschere Lüge.
|
||||
|
||||
**Die Rechenregel ist ein Minimum über die Abhängigkeits-Hülle.** Jeder Status
|
||||
bekommt einen Fortschritts-Rang entlang der Ergebnis-Skala (D5); effektiv ist
|
||||
das Minimum des intrinsischen Rangs über den Knoten selbst und alles, was er
|
||||
direkt oder mittelbar braucht. Diese Form hat zwei angenehme Folgen: **Zyklen
|
||||
brauchen keine Sonderregel** — alle Knoten eines Zyklus teilen ihr Minimum,
|
||||
und das ist wörtlich das „wird gemeinsam fertig“ aus D34; und die Rechnung ist
|
||||
eine schlichte Fixpunkt-Iteration (Ränge sinken nur, Abbruch garantiert).
|
||||
Außerhalb der Skala: neutral und `[-]` zählen als 0 — wer auf etwas
|
||||
Verworfenes oder nie Begonnenes zeigt, ist effektiv am Anfang, und genau das
|
||||
soll auffallen —, `[!]` als 1 (die Absicht-ohne-Investition-Gruppe aus D35).
|
||||
Bei doppelter ID gilt die **erste** Vergabe — das löst das in D36 offen
|
||||
gelassene „wer gewinnt bei Verweisen“ konsistent zur `duplicateId`-Warnung.
|
||||
|
||||
**Die Diskrepanz-Marke ist die eigene Statusbox in den eigenen Farben.**
|
||||
Unten links (die letzte freie Knoten-Ecke: oben rechts Größe, unten rechts
|
||||
Tags, oben links ⚠) sitzt ein kleines `[x]`-Etikett in den §4-Farben des
|
||||
intrinsischen Status — die Notation kennzeichnet sich mit ihrem eigenen
|
||||
Vokabular, niemand lernt ein neues Symbol, und die Farben tragen die ganze
|
||||
Geschichte: Knoten rosé (effektiv in Arbeit), Etikett grün (selbst fertig).
|
||||
Tooltip („effektiv … — selbst schon …, wartet auf Abhängigkeiten“) und
|
||||
`a11yEffective` sagen es in Worten. Marke und Färbung gehören in Export und
|
||||
Druck — sie sind eine Aussage über den Plan, nicht über den Betrachter.
|
||||
|
||||
**Was beim intrinsischen Status bleibt:** die XOR-Regel (§3) — „realisiert“
|
||||
heißt Kosten investiert, und investiert ist investiert, auch wenn
|
||||
Abhängigkeiten den Knoten zurückhalten; und „Was ist neu?“ (D28) — der gelbe
|
||||
Kranz meldet das `[^]` im Text, also den Deploy des Knotens selbst. Beide
|
||||
Prüfungen laufen im Parser bzw. auf dem Text und bleiben unberührt.
|
||||
|
||||
+32
-11
@@ -190,6 +190,24 @@ Codiert als Checkbox nach dem Zeichen (Erweiterung der Markdown-Task-Syntax):
|
||||
verloren, Folgezeilen bleiben unberührt. (Eine mehrzeichige Klammer wie
|
||||
`[xyz]` ist keine Statusbox und bleibt Teil des Labels.)
|
||||
|
||||
**Intrinsischer und effektiver Status (Abhängigkeiten, §1).** Der Status in
|
||||
der Box ist der **intrinsische** — der Bearbeitungsstand des Knotens selbst.
|
||||
Der **effektive** Status berücksichtigt die Abhängigkeiten: Ein Knoten kann
|
||||
effektiv nicht weiter sein als das, was er braucht.
|
||||
|
||||
- **Fortschritts-Rang** entlang der Ergebnis-Skala (D5):
|
||||
`[?]` 0 · `[ ]` 1 · `[~]` 2 · `[/]` 3 · `[x]` 4 · `[^]` 5. Außerhalb der
|
||||
Skala zählen neutrale Knoten und `[-]` als Rang 0 („nichts Anrechenbares“),
|
||||
`[!]` als Rang 1 (Absicht ohne Investition, D35).
|
||||
- **Effektiver Rang = Minimum des intrinsischen Rangs über die
|
||||
Abhängigkeits-Hülle**: der Knoten selbst plus alles, was er per `:#…` direkt
|
||||
oder mittelbar braucht. Zyklen brauchen keine Sonderregel — alle Knoten
|
||||
eines Zyklus teilen so von selbst ihr Minimum („wird gemeinsam fertig“, §1).
|
||||
Unbekannte IDs zählen nicht (sie sind schon gewarnt); bei doppelter ID gilt
|
||||
die **erste** Vergabe (D36).
|
||||
- Der effektive Status wird **gerechnet, nie geschrieben** (D14: der Text ist
|
||||
die eine Quelle der Wahrheit). Darstellung: §9; Begründung: D39.
|
||||
|
||||
## 5. Aufwand (T-Shirt-Größen)
|
||||
|
||||
- Werte: `XS < S < M < L < XL < XXL`, notiert in Klammern, z. B. `(L)`.
|
||||
@@ -241,6 +259,17 @@ Stück zwischen Elternknoten und erstem Abzweig — eine kleine **„1“-Plaket
|
||||
(weißer Kreis mit grauem Rand, graue Ziffer): „genau eine“. Sie erscheint auch
|
||||
im Grafikexport. Siehe D35.
|
||||
|
||||
**Die Knotenfarbe zeigt den effektiven Status (§4)**, nicht den intrinsischen —
|
||||
das Diagramm beantwortet „wie weit ist das wirklich?“. Wo der eigene Status
|
||||
**weiter** ist als der effektive (der Knoten wird von Abhängigkeiten
|
||||
zurückgehalten), sitzt unten links eine kleine **Status-Marke** mit der
|
||||
eigenen Statusbox (z. B. `[x]`) in den §4-Farben des intrinsischen Status;
|
||||
Tooltip und `aria-label` benennen beide. Ohne Diskrepanz ändert sich nichts —
|
||||
und für die XOR-Regel (§3) und „Was ist neu?“ (unten) zählt weiterhin der
|
||||
**intrinsische** Status: Investiert ist investiert, und `[^]` im Text bleibt
|
||||
die Deploy-Aussage. Marke und Färbung erscheinen auch im Grafikexport und im
|
||||
Druck. Siehe D39.
|
||||
|
||||
**Optionale Knoten (`+`, §3):** Sie hängen an der normalen all-of-Zerlegung,
|
||||
die Anordnung bleibt unverändert. Zwei Kennzeichen, beide auch im Grafikexport:
|
||||
|
||||
@@ -661,17 +690,9 @@ die Closure-Rechnung des günstigsten Pfads (alle drei unten).
|
||||
|
||||
### Intrinsischer und effektiver Status
|
||||
|
||||
- Der **intrinsische** Status ist der, der in der Statusbox steht (§4) — der
|
||||
Bearbeitungsstand des Knotens selbst.
|
||||
- Der **effektive** Status berücksichtigt zusätzlich die Abhängigkeiten: Ein
|
||||
intrinsisch fertiger Knoten ist effektiv **nicht** fertig, solange eine
|
||||
Abhängigkeit es nicht ist.
|
||||
- Der effektive Status wird **gerechnet, nie geschrieben**. Im Notationstext
|
||||
steht ausschließlich der intrinsische — sonst gäbe es zwei Quellen der
|
||||
Wahrheit für dieselbe Aussage.
|
||||
- **Offen ist die Darstellung.** Die Knotenfarbe zeigt heute den intrinsischen
|
||||
Status (§4); beide zugleich brauchen entweder einen zweiten visuellen Kanal
|
||||
oder einen Umschalter.
|
||||
**Umgesetzt** — Rechenregel in §4 (Fortschritts-Rang, Minimum über die
|
||||
Abhängigkeits-Hülle), Darstellung in §9 (Knotenfarbe = effektiver Status,
|
||||
Diskrepanz-Marke unten links). Siehe D39.
|
||||
|
||||
### Querverbindungen im Diagramm
|
||||
|
||||
|
||||
+6
-1
@@ -83,8 +83,13 @@ entscheiden, **bevor** Code entsteht.
|
||||
`deps` als ID-Strings am Knoten, Warnung `unknownDep`, Zyklen bewusst
|
||||
ungeprüft; sichtbar als `→ #a, #b` im Tooltip + `a11yDeps`;
|
||||
SPEC-§11-Teil nach §1 überführt; `tests/deps.test.js`.
|
||||
- [ ] Effektiven Status rechnen (intrinsisch + Abhängigkeiten); Darstellung
|
||||
- [x] Effektiven Status rechnen (intrinsisch + Abhängigkeiten); Darstellung
|
||||
entscheiden — die Knotenfarbe zeigt heute den intrinsischen Status.
|
||||
→ Entschieden und umgesetzt (D39): Fortschritts-Rang (SPEC §4), effektiv
|
||||
= Minimum über die Abhängigkeits-Hülle (Fixpunkt, Zyklen teilen ihr
|
||||
Minimum); **Knotenfarbe = effektiver Status**, bei Diskrepanz die eigene
|
||||
Statusbox als Marke unten links in den eigenen Farben (auch im Export);
|
||||
Tooltip + `a11yEffective`; `tests/effective.test.js`.
|
||||
- [ ] Günstigsten Pfad auf die Dependency Closure umstellen (gemeinsame
|
||||
Abhängigkeiten nur einmal zählen). Erweitert D18; die gierige Wahl je
|
||||
Alternativgruppe ist damit nicht mehr optimal — Verfahren wählen und
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
- [x] Node IDs (#auth) (S) %% often just the ticket number, see SPEC §11
|
||||
+ [?] Free tags, &tag (M) %% only together with a consumer, see D34
|
||||
- [?] A lens: highlight every node tagged &x (S)
|
||||
- [~] Dependencies across the tree, (:#auth,#api) (M)
|
||||
- [?] They constrain the status, not when work may start (S)
|
||||
- [?] Effective status: intrinsic, plus what it depends on (S)
|
||||
- [x] Dependencies across the tree, (:#auth,#api) (M)
|
||||
- [x] They constrain the status, not when work may start (S)
|
||||
- [x] Effective status: intrinsic, plus what it depends on (S)
|
||||
- [x] Cycles are legal — they mean "finished together" (XS)
|
||||
- [x] An id with no node behind it is a warning (XS)
|
||||
- [x] XOR, = — exactly one alternative, not at least one (S)
|
||||
|
||||
@@ -292,6 +292,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
||||
Knoten-Referenzen; der Parser prüft nur Existenz (`unknownDep`), Zyklen
|
||||
werden bewusst nicht einmal erkannt (zulässig, „gemeinsam fertig"). Keine
|
||||
Diagramm-Darstellung — nur Tooltip (`→ #a, #b`) und `a11yDeps`.
|
||||
- Effektiver Status (SPEC §4/§9, D39): `effectiveStatus(roots)` in model.js
|
||||
liefert NUR die Diskrepanzen (Map Knoten → effektiver Status-Key; Minimum
|
||||
des Fortschritts-Rangs über die Abhängigkeits-Hülle, Fixpunkt — Zyklen
|
||||
teilen ihr Minimum, erste ID-Vergabe gewinnt). Der Renderer färbt dann
|
||||
`st-<effKey>` statt des intrinsischen und hängt die Marke
|
||||
`.chip.ownst.st-<ownKey>` (eigene Statusbox, eigene Farben) unten links an —
|
||||
im Export via `drawBadge`, aus dem Label-Klon entfernen (`.ownst`). XOR-
|
||||
„realisiert" (Parser) und „Was ist neu?" (`[^]` im Text) bleiben bewusst
|
||||
**intrinsisch**.
|
||||
- XOR-Gruppen `=` (SPEC §3/D35): Der Parser setzt `type:'xor'` (nur mit
|
||||
folgendem Leerraum — `=SUMME(…)` bleibt Label); der Renderer gibt
|
||||
`<ul class="or xor">` aus, damit die **gesamte** any-of-Geometrie (alle drei
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten.
|
||||
Knoten-ID mit <code>#name</code> — erscheint im Tooltip des Knotens.
|
||||
Abhängigkeiten mit <code>:#name,#name</code> — erscheinen im Tooltip.
|
||||
Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.
|
||||
Falten: <code>- > [x] …</code> startet eingeklappt, <code><</code> holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+29
-12
@@ -1,6 +1,6 @@
|
||||
import './style.css';
|
||||
import { parse } from './parser.js';
|
||||
import { computeCheapSet, freshProdSet, initialCollapsed, nodeKeys } from './model.js';
|
||||
import { computeCheapSet, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus } from './model.js';
|
||||
import { esc, renderTreeHtml } from './render.js';
|
||||
import { formatWarning } from './warnings.js';
|
||||
import { padUrls } from './remote.js';
|
||||
@@ -112,7 +112,8 @@ function render(){
|
||||
foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0});
|
||||
});
|
||||
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet,
|
||||
freshSet, collapsedSet});
|
||||
freshSet, collapsedSet,
|
||||
effStatus: effectiveStatus(roots)});
|
||||
out.innerHTML = r.html;
|
||||
warnings = warnings.concat(r.warnings);
|
||||
}
|
||||
@@ -402,7 +403,7 @@ function diagramToSvg(){
|
||||
§9/D38 — das Bild darf keine Vollständigkeit behaupten); das ▾ offener
|
||||
Knoten ist Bedienelement und fällt weg. */
|
||||
const stripFold = node.classList.contains('folded') ? '' : ',.fold';
|
||||
clone.querySelectorAll('.size,.tags,.ext,.risk' + stripFold).forEach(e => e.remove());
|
||||
clone.querySelectorAll('.size,.tags,.ext,.risk,.ownst' + stripFold).forEach(e => e.remove());
|
||||
const label = clone.textContent.replace(/\s+/g,' ').trim();
|
||||
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
|
||||
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`);
|
||||
@@ -418,6 +419,12 @@ function diagramToSvg(){
|
||||
const t = getComputedStyle(tg);
|
||||
drawBadge(tg, t.backgroundColor, t.color, t.borderTopColor);
|
||||
});
|
||||
/* Diskrepanz-Marke des effektiven Status (D39) — Aussage über den Plan,
|
||||
gehört in den Export wie Größe und Tags. */
|
||||
node.querySelectorAll('.ownst').forEach(c => {
|
||||
const s = getComputedStyle(c);
|
||||
drawBadge(c, s.backgroundColor, s.color, s.borderTopColor);
|
||||
});
|
||||
});
|
||||
|
||||
/* 3a) Optionale Knoten: hohler Kreis auf der Kante (nach den Knoten) */
|
||||
@@ -1170,7 +1177,7 @@ const I18N = {
|
||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
|
||||
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
|
||||
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
||||
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
||||
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
||||
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
||||
hint_xor:"Alternative, genau eine",
|
||||
@@ -1184,6 +1191,7 @@ const I18N = {
|
||||
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.",
|
||||
hint_id:"Knoten-ID mit #name — erscheint im Tooltip des Knotens.",
|
||||
hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.",
|
||||
hint_eff:"Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.",
|
||||
hint_fold:"Falten: - > [x] … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).",
|
||||
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild."
|
||||
},
|
||||
@@ -1241,7 +1249,7 @@ const I18N = {
|
||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
|
||||
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
|
||||
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
||||
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
||||
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
||||
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
||||
hint_xor:"alternative, exactly one",
|
||||
@@ -1255,6 +1263,7 @@ const I18N = {
|
||||
hint_people:"People with @name — shown at the bottom-right of the node.",
|
||||
hint_id:"Node ID with #name — shown in the node's tooltip.",
|
||||
hint_deps:"Dependencies with :#name,#name — shown in the tooltip.",
|
||||
hint_eff:"Node colour shows the effective status (with dependencies); if its own is further along, it appears as a mark at the bottom left.",
|
||||
hint_fold:"Folding: - > [x] … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).",
|
||||
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view."
|
||||
},
|
||||
@@ -1312,7 +1321,7 @@ const I18N = {
|
||||
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
||||
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
|
||||
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
|
||||
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
||||
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
||||
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
||||
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
||||
hint_xor:"alternativa, exactamente una",
|
||||
@@ -1326,6 +1335,7 @@ const I18N = {
|
||||
hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.",
|
||||
hint_id:"ID de nodo con #nombre — visible en el tooltip del nodo.",
|
||||
hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.",
|
||||
hint_eff:"El color del nodo muestra el estado efectivo (con dependencias); si el propio va más adelante, aparece como marca abajo a la izquierda.",
|
||||
hint_fold:"Plegado: - > [x] … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).",
|
||||
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista."
|
||||
},
|
||||
@@ -1383,7 +1393,7 @@ const I18N = {
|
||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
|
||||
sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
|
||||
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
||||
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
||||
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
||||
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
||||
hint_xor:"alternative, exactement une",
|
||||
@@ -1397,6 +1407,7 @@ const I18N = {
|
||||
hint_people:"Personnes avec @nom — affichées en bas à droite du nœud.",
|
||||
hint_id:"ID de nœud avec #nom — visible dans l’infobulle du nœud.",
|
||||
hint_deps:"Dépendances avec :#nom,#nom — visibles dans l’infobulle.",
|
||||
hint_eff:"La couleur du nœud montre le statut effectif (avec dépendances) ; si le sien est plus avancé, il apparaît en marque en bas à gauche.",
|
||||
hint_fold:"Pliage : - > [x] … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).",
|
||||
hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran."
|
||||
},
|
||||
@@ -1454,7 +1465,7 @@ const I18N = {
|
||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
|
||||
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
|
||||
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
||||
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
||||
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
||||
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
||||
hint_xor:"alternatywa, dokładnie jedna",
|
||||
@@ -1468,6 +1479,7 @@ const I18N = {
|
||||
hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła.",
|
||||
hint_id:"ID węzła przez #nazwa — widoczne w podpowiedzi węzła.",
|
||||
hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.",
|
||||
hint_eff:"Kolor węzła pokazuje status efektywny (z zależnościami); jeśli własny jest dalej, widnieje jako znacznik u dołu po lewej.",
|
||||
hint_fold:"Zwijanie: - > [x] … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).",
|
||||
hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie."
|
||||
},
|
||||
@@ -1525,7 +1537,7 @@ const I18N = {
|
||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
||||
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
|
||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||
hint_xor:"альтернатива, ровно одна",
|
||||
@@ -1539,6 +1551,7 @@ const I18N = {
|
||||
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
||||
hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.",
|
||||
hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.",
|
||||
hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.",
|
||||
hint_fold:"Сворачивание: - > [x] … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).",
|
||||
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
|
||||
},
|
||||
@@ -1596,7 +1609,7 @@ const I18N = {
|
||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
||||
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
|
||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||
hint_xor:"विकल्प, ठीक एक",
|
||||
@@ -1610,6 +1623,7 @@ const I18N = {
|
||||
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
||||
hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।",
|
||||
hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।",
|
||||
hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।",
|
||||
hint_fold:"फ़ोल्डिंग: - > [x] … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।",
|
||||
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
|
||||
},
|
||||
@@ -1667,7 +1681,7 @@ const I18N = {
|
||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
||||
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
|
||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||
hint_xor:"备选项,恰好一个",
|
||||
@@ -1681,6 +1695,7 @@ const I18N = {
|
||||
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
||||
hint_id:"用 #名称 指定节点 ID——显示在节点提示中。",
|
||||
hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。",
|
||||
hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。",
|
||||
hint_fold:"折叠:- > [x] … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。",
|
||||
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
|
||||
},
|
||||
@@ -1738,7 +1753,7 @@ const I18N = {
|
||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
||||
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
|
||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
||||
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
||||
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
||||
hint_xor:"選択肢、ちょうど1つ",
|
||||
@@ -1752,6 +1767,7 @@ const I18N = {
|
||||
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
||||
hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。",
|
||||
hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。",
|
||||
hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。",
|
||||
hint_fold:"折りたたみ:- > [x] … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。",
|
||||
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
|
||||
}
|
||||
@@ -1787,6 +1803,7 @@ function buildHint(){
|
||||
${esc(t('hint_people'))}
|
||||
${esc(t('hint_id'))}
|
||||
${esc(t('hint_deps'))}
|
||||
${esc(t('hint_eff'))}
|
||||
${esc(t('hint_fold'))}
|
||||
<code>!!!</code> ${esc(t('hint_focus'))}
|
||||
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
|
||||
|
||||
@@ -78,6 +78,51 @@ export function cheapCls(n, cheapSet){
|
||||
return leaf ? 'cheap cheap-leaf' : 'cheap';
|
||||
}
|
||||
|
||||
/* ---------- Effektiver Status (SPEC §4/§9, D39) ----------
|
||||
Fortschritts-Rang entlang der Ergebnis-Skala (D5). Außerhalb der Skala:
|
||||
neutrale und verworfene Knoten zählen als 0 („nichts Anrechenbares"),
|
||||
High Risk als 1 (Absicht ohne Investition, D35). */
|
||||
export const PROGRESS_RANK = { idee:0, geplant:1, highrisk:1, arbeit:2, durchstich:3, fertig:4, prod:5 };
|
||||
const RANK_STATUS_KEY = ['idee', 'geplant', 'arbeit', 'durchstich', 'fertig', 'prod'];
|
||||
export function progressRank(n){
|
||||
if(!n.status) return 0;
|
||||
const r = PROGRESS_RANK[n.status.key];
|
||||
return r === undefined ? 0 : r; /* verworfen -> 0 */
|
||||
}
|
||||
/* Diskrepanzen des effektiven Status: Map Knoten -> effektiver Status-KEY,
|
||||
nur für Knoten, deren eigener Status weiter ist. Effektiver Rang = Minimum
|
||||
des intrinsischen Rangs über die Abhängigkeits-Hülle (Knoten selbst plus
|
||||
alles per `:#…` Erreichbare), als Fixpunkt-Iteration — Ränge sinken nur,
|
||||
Zyklen teilen so von selbst ihr Minimum („wird gemeinsam fertig", §1).
|
||||
Unbekannte IDs zählen nicht (schon gewarnt, `unknownDep`); bei doppelter
|
||||
ID gilt die ERSTE Vergabe (D36/D39). */
|
||||
export function effectiveStatus(roots){
|
||||
const nodes = [], byId = new Map();
|
||||
(function walk(ns){
|
||||
for(const n of ns){
|
||||
nodes.push(n);
|
||||
if(n.id != null && !byId.has(n.id)) byId.set(n.id, n);
|
||||
walk(n.children);
|
||||
}
|
||||
})(roots);
|
||||
const eff = new Map(nodes.map(n => [n, progressRank(n)]));
|
||||
let changed = true;
|
||||
while(changed){
|
||||
changed = false;
|
||||
for(const n of nodes){
|
||||
let v = eff.get(n);
|
||||
for(const d of n.deps || []){
|
||||
const target = byId.get(d);
|
||||
if(target && eff.get(target) < v) v = eff.get(target);
|
||||
}
|
||||
if(v < eff.get(n)){ eff.set(n, v); changed = true; }
|
||||
}
|
||||
}
|
||||
const map = new Map();
|
||||
eff.forEach((r, n) => { if(r < progressRank(n)) map.set(n, RANK_STATUS_KEY[r]); });
|
||||
return map;
|
||||
}
|
||||
|
||||
/* ---------- Was ist neu? (D28) ----------
|
||||
„Neu" heißt hier bewusst nicht „Zeile hinzugefügt", sondern **neu in
|
||||
Produktion**: ein Knoten, der jetzt `[^]` trägt und es in der zuletzt
|
||||
|
||||
+11
-10
@@ -6,17 +6,18 @@
|
||||
/* T-Shirt-Größen (SPEC §5), aufsteigend geordnet. */
|
||||
export const SIZE_RANK = { XS: 0, S: 1, M: 2, L: 3, XL: 4, XXL: 5 };
|
||||
|
||||
/* Status-Vokabular (SPEC §4): Checkbox-Code -> {key, name}.
|
||||
`name` ist der deutsche Anzeigename (Quellsprache). */
|
||||
/* Status-Vokabular (SPEC §4): Checkbox-Code -> {code, key, name}.
|
||||
`name` ist der deutsche Anzeigename (Quellsprache); `code` das kanonische
|
||||
Box-Zeichen (für die Diskrepanz-Marke des effektiven Status, D39). */
|
||||
export const STATUS_BY_CODE = {
|
||||
'?': {key:'idee', name:'Idee'},
|
||||
' ': {key:'geplant', name:'geplant'},
|
||||
'~': {key:'arbeit', name:'in Arbeit'},
|
||||
'/': {key:'durchstich', name:'Durchstich – funktionsbereit, Feinarbeiten offen'},
|
||||
'x': {key:'fertig', name:'fertig'},
|
||||
'^': {key:'prod', name:'in Produktion'},
|
||||
'-': {key:'verworfen', name:'verworfen'},
|
||||
'!': {key:'highrisk', name:'High Risk – Aufwand unklar'}
|
||||
'?': {code:'?', key:'idee', name:'Idee'},
|
||||
' ': {code:' ', key:'geplant', name:'geplant'},
|
||||
'~': {code:'~', key:'arbeit', name:'in Arbeit'},
|
||||
'/': {code:'/', key:'durchstich', name:'Durchstich – funktionsbereit, Feinarbeiten offen'},
|
||||
'x': {code:'x', key:'fertig', name:'fertig'},
|
||||
'^': {code:'^', key:'prod', name:'in Produktion'},
|
||||
'-': {code:'-', key:'verworfen', name:'verworfen'},
|
||||
'!': {code:'!', key:'highrisk', name:'High Risk – Aufwand unklar'}
|
||||
};
|
||||
|
||||
/* Status, die als „realisiert" zählen (XOR-Regel, SPEC §3/D35): Kosten sind
|
||||
|
||||
+21
-3
@@ -11,6 +11,8 @@
|
||||
cheapSet, // Set der nötigen Knoten (leer, wenn Pfad aus)
|
||||
freshSet, // optional: Knoten, die neu in Produktion sind (D28)
|
||||
collapsedSet, // optional: eingeklappte Knoten (Faltung, SPEC §9/D38)
|
||||
effStatus, // optional: Map Knoten -> effektiver Status-Key, nur
|
||||
// Diskrepanzen (effectiveStatus() in model.js, D39)
|
||||
} */
|
||||
|
||||
import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
|
||||
@@ -52,6 +54,10 @@ function nodeAria(n, opts, fold){
|
||||
Ansage wüsste ein Screenreader nicht, dass hier etwas verborgen ist. */
|
||||
if(fold && fold.collapsed) parts.push(t('a11yFolded', {n: fold.count}));
|
||||
if(n.status) parts.push(t('a11yStatus', {status: t('st_' + n.status.key)}));
|
||||
/* Diskrepanz (D39): die Farbe zeigt den effektiven Status — der Screenreader
|
||||
bekommt beide, wie der Tooltip. */
|
||||
const effKey = opts.effStatus ? opts.effStatus.get(n) : undefined;
|
||||
if(effKey) parts.push(t('a11yEffective', {status: t('st_' + effKey)}));
|
||||
if(n.size) parts.push(t('a11ySize', {size: n.size}));
|
||||
else if(cheapPath) parts.push(t('a11ySizeImplicit'));
|
||||
if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')}));
|
||||
@@ -72,8 +78,12 @@ function nodeAria(n, opts, fold){
|
||||
function nodeHtml(n, extra, opts, fold){
|
||||
const { t, cheapPath } = opts;
|
||||
const need = needsBreakdown(n);
|
||||
/* Die Knotenfarbe zeigt den EFFEKTIVEN Status (SPEC §9/D39); bei Diskrepanz
|
||||
trägt die Marke unten links die eigene Statusbox in den eigenen Farben. */
|
||||
const effKey = opts.effStatus ? opts.effStatus.get(n) : undefined;
|
||||
const cls = ['node', extra || '', fold && fold.collapsed ? 'folded' : '',
|
||||
n.status ? 'st-' + n.status.key : '']
|
||||
effKey ? 'held' : '',
|
||||
n.status ? 'st-' + (effKey || n.status.key) : '']
|
||||
.filter(Boolean).join(' ');
|
||||
/* Zeilennummer am Knoten (D25): Grundlage für den Sprung ins Textfeld und
|
||||
für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im
|
||||
@@ -81,7 +91,9 @@ function nodeHtml(n, extra, opts, fold){
|
||||
const lineAttr = n.line ? ` data-line="${n.line}"` : '';
|
||||
const tip = [n.id ? '#' + n.id : '',
|
||||
n.deps && n.deps.length ? '→ ' + n.deps.map(d => '#' + d).join(', ') : '',
|
||||
n.status ? t('st_' + n.status.key) : '',
|
||||
effKey
|
||||
? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)})
|
||||
: (n.status ? t('st_' + n.status.key) : ''),
|
||||
n.optional ? t('a11yOptional') : '', t('jumpHint')]
|
||||
.filter(Boolean).join(' · ');
|
||||
const title = ` title="${attr(tip)}"`;
|
||||
@@ -104,12 +116,18 @@ function nodeHtml(n, extra, opts, fold){
|
||||
? `<span class="fold" aria-hidden="true">${fold.collapsed ? '▸ ' + fold.count : '▾'}</span>`
|
||||
: '';
|
||||
const expanded = fold ? ` aria-expanded="${!fold.collapsed}"` : '';
|
||||
/* Diskrepanz-Marke: die eigene Statusbox in den eigenen §4-Farben —
|
||||
„selbst schon [x], wartet auf Abhängigkeiten" (D39). */
|
||||
const ownChip = effKey
|
||||
? `<span class="chip ownst st-${n.status.key}" aria-hidden="true">[${n.status.code}]</span>`
|
||||
: '';
|
||||
const inner = foldHtml +
|
||||
esc(n.label) +
|
||||
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
|
||||
riskMark +
|
||||
sizeBadge +
|
||||
tagsHtml;
|
||||
tagsHtml +
|
||||
ownChip;
|
||||
const aria = ` aria-label="${attr(nodeAria(n, opts, fold))}"`;
|
||||
const html = n.url
|
||||
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${aria}${expanded}${title}>${inner}</a>`
|
||||
|
||||
@@ -644,6 +644,14 @@
|
||||
position:absolute;right:-9px;bottom:-9px;
|
||||
display:flex;gap:3px;
|
||||
}
|
||||
/* Diskrepanz-Marke des effektiven Status (SPEC §9, D39): die eigene
|
||||
Statusbox in den eigenen §4-Farben (nutzt die .chip-Farbregeln der
|
||||
Legende), unten links — die letzte freie Knoten-Ecke. */
|
||||
.node .chip.ownst{
|
||||
position:absolute;left:-9px;bottom:-9px;
|
||||
font-size:.58rem;font-weight:500;letter-spacing:.02em;
|
||||
padding:1px 5px;line-height:1.4;border-radius:999px;
|
||||
}
|
||||
.tag{
|
||||
font-family:'IBM Plex Mono',monospace;
|
||||
font-size:.58rem;font-weight:500;letter-spacing:.02em;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parse } from '../src/parser.js';
|
||||
import { effectiveStatus, progressRank } from '../src/model.js';
|
||||
import { renderTreeHtml } from '../src/render.js';
|
||||
|
||||
const t = key => key;
|
||||
const roots = txt => parse(txt).roots;
|
||||
/* Map Label -> effektiver Status-Key (nur Diskrepanzen). */
|
||||
const disc = txt => {
|
||||
const m = effectiveStatus(roots(txt));
|
||||
const out = {};
|
||||
m.forEach((key, n) => { out[n.label] = key; });
|
||||
return out;
|
||||
};
|
||||
const render = txt => {
|
||||
const r = roots(txt);
|
||||
return renderTreeHtml(r, {t, showDiscarded: false, cheapPath: false,
|
||||
cheapSet: new Set(), effStatus: effectiveStatus(r)});
|
||||
};
|
||||
|
||||
/* Effektiver Status (SPEC §4/§9, D39): Minimum des Fortschritts-Rangs über
|
||||
die Abhängigkeits-Hülle; gemeldet werden nur Diskrepanzen. */
|
||||
describe('effectiveStatus — Minimum über die Abhängigkeits-Hülle', () => {
|
||||
it('hält einen fertigen Knoten auf dem Rang seiner Abhängigkeit', () => {
|
||||
expect(disc(`[~] API #api\n[x] Deploy :#api`))
|
||||
.toEqual({Deploy: 'arbeit'});
|
||||
});
|
||||
|
||||
it('reicht das Minimum über Ketten durch', () => {
|
||||
expect(disc(`[ ] C #c\n[/] B #b :#c\n[x] A :#b`))
|
||||
.toEqual({A: 'geplant', B: 'geplant'});
|
||||
});
|
||||
|
||||
it('meldet nichts, wenn die Abhängigkeit gleichauf oder weiter ist', () => {
|
||||
expect(disc(`[x] API #api\n[x] Deploy :#api`)).toEqual({});
|
||||
expect(disc(`[^] API #api\n[x] Deploy :#api`)).toEqual({});
|
||||
});
|
||||
|
||||
it('lässt Zyklen ihr Minimum teilen — „wird gemeinsam fertig"', () => {
|
||||
expect(disc(`[x] A #a :#b\n[x] B #b :#a`)).toEqual({});
|
||||
expect(disc(`[x] A #a :#b\n[~] B #b :#a`)).toEqual({A: 'arbeit'});
|
||||
expect(disc(`[x] A #a :#a`)).toEqual({}); /* Selbst-Abhängigkeit */
|
||||
});
|
||||
|
||||
it('ignoriert unbekannte IDs (dafür gibt es die unknownDep-Warnung)', () => {
|
||||
expect(disc(`[x] A :#gibtsnicht`)).toEqual({});
|
||||
});
|
||||
|
||||
it('zählt neutrale und verworfene Ziele als Rang 0, High Risk als 1', () => {
|
||||
expect(disc(`Neutral #n\n[x] A :#n`)).toEqual({A: 'idee'});
|
||||
expect(disc(`[-] Weg #w\n[x] A :#w`)).toEqual({A: 'idee'});
|
||||
expect(disc(`[!] Risiko #r\n[x] A :#r`)).toEqual({A: 'geplant'});
|
||||
});
|
||||
|
||||
it('macht neutrale und verworfene Knoten selbst nie zur Diskrepanz', () => {
|
||||
expect(disc(`[?] Ziel #z\nNeutral :#z\n`)).toEqual({});
|
||||
expect(disc(`[?] Ziel #z\n[-] Weg :#z`)).toEqual({});
|
||||
expect(progressRank(roots(`[-] Weg`)[0])).toBe(0);
|
||||
});
|
||||
|
||||
it('löst Verweise auf die ERSTE Vergabe einer doppelten ID auf (D36/D39)', () => {
|
||||
expect(disc(`[~] Erste #x\n[x] Zweite #x\n[x] A :#x`)).toEqual({A: 'arbeit'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Darstellung — Farbe effektiv, Marke intrinsisch', () => {
|
||||
const TXT = `[~] API #api (M)\n[x] Deploy :#api`;
|
||||
|
||||
it('färbt den Knoten mit dem effektiven Status und markiert ihn als held', () => {
|
||||
const {html} = render(TXT);
|
||||
expect(html).toContain('class="node root-node held st-arbeit" tabindex="0" data-line="2"');
|
||||
expect(html).not.toContain('st-fertig" tabindex="0" data-line="2"');
|
||||
});
|
||||
|
||||
it('hängt die eigene Statusbox als Marke in den eigenen Farben an', () => {
|
||||
const {html} = render(TXT);
|
||||
expect(html).toContain('<span class="chip ownst st-fertig" aria-hidden="true">[x]</span>');
|
||||
});
|
||||
|
||||
it('benennt beide Status in Tooltip und aria-label', () => {
|
||||
const {html} = render(TXT);
|
||||
expect(html).toContain('title="→ #api · heldTooltip · jumpHint"');
|
||||
expect(html).toContain('aria-label="Deploy, a11yStatus, a11yEffective, a11yDeps"');
|
||||
});
|
||||
|
||||
it('ändert Knoten ohne Diskrepanz nicht', () => {
|
||||
const {html} = render(`[x] API #api\n[x] Deploy :#api`);
|
||||
expect(html).not.toContain('ownst');
|
||||
expect(html).not.toContain('held');
|
||||
expect((html.match(/st-fertig/g) || []).length).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user