@sentropic/track 0.88.0 → 0.90.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/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +175 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/events/types.d.ts +1 -1
- package/dist/events/types.d.ts.map +1 -1
- package/dist/events/types.js +16 -0
- package/dist/events/types.js.map +1 -1
- package/dist/events/validate.d.ts +10 -0
- package/dist/events/validate.d.ts.map +1 -1
- package/dist/events/validate.js +81 -0
- package/dist/events/validate.js.map +1 -1
- package/dist/ingest/contract.d.ts +3 -2
- package/dist/ingest/contract.d.ts.map +1 -1
- package/dist/ingest/contract.js +33 -1
- package/dist/ingest/contract.js.map +1 -1
- package/dist/ingest/index.d.ts.map +1 -1
- package/dist/ingest/index.js +6 -5
- package/dist/ingest/index.js.map +1 -1
- package/dist/ingest/ingest.d.ts.map +1 -1
- package/dist/ingest/ingest.js +17 -0
- package/dist/ingest/ingest.js.map +1 -1
- package/dist/ingest/map.d.ts.map +1 -1
- package/dist/ingest/map.js +11 -0
- package/dist/ingest/map.js.map +1 -1
- package/dist/model/index.d.ts +1 -1
- package/dist/model/index.d.ts.map +1 -1
- package/dist/model/index.js +1 -1
- package/dist/model/index.js.map +1 -1
- package/dist/model/item.d.ts +65 -0
- package/dist/model/item.d.ts.map +1 -1
- package/dist/model/item.js +49 -0
- package/dist/model/item.js.map +1 -1
- package/dist/read/commands.d.ts +23 -11
- package/dist/read/commands.d.ts.map +1 -1
- package/dist/read/commands.js +142 -36
- package/dist/read/commands.js.map +1 -1
- package/dist/read/contract.d.ts +21 -3
- package/dist/read/contract.d.ts.map +1 -1
- package/dist/read/contract.js +26 -2
- package/dist/read/contract.js.map +1 -1
- package/dist/report/blocker-status.d.ts.map +1 -1
- package/dist/report/blocker-status.js +11 -0
- package/dist/report/blocker-status.js.map +1 -1
- package/dist/report/format.d.ts +55 -1
- package/dist/report/format.d.ts.map +1 -1
- package/dist/report/format.js +203 -72
- package/dist/report/format.js.map +1 -1
- package/dist/report/index.d.ts +0 -1
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +0 -1
- package/dist/report/index.js.map +1 -1
- package/dist/state/fold.d.ts.map +1 -1
- package/dist/state/fold.js +48 -0
- package/dist/state/fold.js.map +1 -1
- package/dist/track.d.ts +34 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +95 -2
- package/dist/track.js.map +1 -1
- package/package.json +2 -2
- package/skills/track-operation/SKILL.md +16 -4
- package/dist/report/ai-report.d.ts +0 -122
- package/dist/report/ai-report.d.ts.map +0 -1
- package/dist/report/ai-report.js +0 -727
- package/dist/report/ai-report.js.map +0 -1
- package/dist/report/html.d.ts +0 -9
- package/dist/report/html.d.ts.map +0 -1
- package/dist/report/html.js +0 -73
- package/dist/report/html.js.map +0 -1
- package/dist/report/present.d.ts +0 -28
- package/dist/report/present.d.ts.map +0 -1
- package/dist/report/present.js +0 -32
- package/dist/report/present.js.map +0 -1
package/dist/report/format.js
CHANGED
|
@@ -4,6 +4,12 @@ import { buildDirectives, decisionNeedsFocus, dispatchQueueOf, keystoneOf, } fro
|
|
|
4
4
|
// the two surfaces can never re-word apart. The terminal composes its own `<nature> (<actor>): <clause>`
|
|
5
5
|
// sentence but sources the canonical action clause + scope label from here.
|
|
6
6
|
import { directiveScopeLabelFr as directiveScopeLabel, gatePhraseFr, stepActionFr } from './friendly.js';
|
|
7
|
+
/** Reject stale JavaScript callers rather than treating an unsupported runtime value as text. */
|
|
8
|
+
export function assertReportFormat(format) {
|
|
9
|
+
if (format !== 'json' && format !== 'text' && format !== 'md') {
|
|
10
|
+
throw new Error(`unsupported report format: ${format}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
7
13
|
const BACKSLASH = String.fromCharCode(92);
|
|
8
14
|
// Markdown metacharacters escaped in `md` titles so a user title can't inject formatting.
|
|
9
15
|
const MD_META = new Set([
|
|
@@ -483,7 +489,7 @@ export function collapseLeafCohorts(leaves) {
|
|
|
483
489
|
export function directivePhrase(d) {
|
|
484
490
|
// §A6 — a directive renders a CONCRETE next move + the actor, and NAMES what blocks (the info the
|
|
485
491
|
// `sujet` column does NOT already carry). All interpolated titles/refs are RAW here — the render layer
|
|
486
|
-
// (table `esc`, inline `clean
|
|
492
|
+
// (table `esc`, inline `clean`) escapes them (§A4). A decision surfaces as a "décision"
|
|
487
493
|
// line ONLY when it genuinely blocks (mode `human-decision`); otherwise the phrase préconise a step.
|
|
488
494
|
//
|
|
489
495
|
// Unified presentation (spec 2026-07-11) — the action CLAUSE is the shared canonical `stepActionFr`, so
|
|
@@ -543,6 +549,26 @@ function recentDoneLeaves(node) {
|
|
|
543
549
|
walk(node);
|
|
544
550
|
return out.sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0));
|
|
545
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* A period is a projection over the already-folded log, not a second state fold. When one is selected,
|
|
554
|
+
* FAIT names precisely the leaves that emitted `realization.transition → done` inside it. This deliberately
|
|
555
|
+
* includes a delivery subsequently reopened: the delivery happened during the asked period even though the
|
|
556
|
+
* item's current bucket is no longer DONE.
|
|
557
|
+
*/
|
|
558
|
+
function periodDoneLeaves(node, deliveredItemIds) {
|
|
559
|
+
if (deliveredItemIds === undefined)
|
|
560
|
+
return recentDoneLeaves(node);
|
|
561
|
+
const out = [];
|
|
562
|
+
const walk = (n) => {
|
|
563
|
+
for (const leaf of n.leaves)
|
|
564
|
+
if (deliveredItemIds.has(leaf.id))
|
|
565
|
+
out.push(leaf);
|
|
566
|
+
for (const child of n.children)
|
|
567
|
+
walk(child);
|
|
568
|
+
};
|
|
569
|
+
walk(node);
|
|
570
|
+
return out.sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0));
|
|
571
|
+
}
|
|
546
572
|
const LAST_ACTIONS_SHOWN = 3;
|
|
547
573
|
/**
|
|
548
574
|
* FAIT's third column (criteria 3/4/22/26/27).
|
|
@@ -587,12 +613,34 @@ const FOCUS_ROWS = 5;
|
|
|
587
613
|
* request (`--sub-wp`).
|
|
588
614
|
*/
|
|
589
615
|
const LONG_WINDOW_DAYS = 14;
|
|
590
|
-
|
|
591
|
-
|
|
616
|
+
/**
|
|
617
|
+
* The ordering invariant of a RENDERED period — BRANCH gate: no rendered period carries reversed bounds.
|
|
618
|
+
* `periodProjection` (read/commands.ts) holds it for every RESOLVED period, which covers every report route
|
|
619
|
+
* (CLI, MCP, library `reportText`/`reportInline`). It cannot cover a caller that BUILDS a `periodWindow`
|
|
620
|
+
* itself and hands it to the public presenter exports — so the presenter asserts the same invariant here,
|
|
621
|
+
* at the single funnel every rendered view crosses. One rule, two boundaries: a later narrowing on one side
|
|
622
|
+
* cannot re-open the other.
|
|
623
|
+
*/
|
|
624
|
+
export function assertOrderedPeriodWindow(meta) {
|
|
625
|
+
const window = meta.periodWindow;
|
|
626
|
+
if (window === undefined)
|
|
627
|
+
return;
|
|
628
|
+
const fromMs = window.from === undefined ? undefined : Date.parse(window.from);
|
|
629
|
+
const toMs = window.to === undefined ? undefined : Date.parse(window.to);
|
|
630
|
+
if (fromMs === undefined || toMs === undefined || Number.isNaN(fromMs) || Number.isNaN(toMs))
|
|
631
|
+
return;
|
|
632
|
+
if (fromMs > toMs) {
|
|
633
|
+
throw new Error('resolved report period must not end before it begins');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function windowDays(meta) {
|
|
637
|
+
const from = meta.periodWindow?.from ?? meta.logFrom;
|
|
638
|
+
const to = meta.periodWindow?.to ?? meta.now ?? meta.logTo;
|
|
639
|
+
if (from === undefined || to === undefined)
|
|
592
640
|
return undefined;
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
return Number.isNaN(
|
|
641
|
+
const fromMs = Date.parse(from);
|
|
642
|
+
const toMs = Date.parse(to);
|
|
643
|
+
return Number.isNaN(fromMs) || Number.isNaN(toMs) ? undefined : (toMs - fromMs) / 86_400_000;
|
|
596
644
|
}
|
|
597
645
|
/**
|
|
598
646
|
* How much of an item's RECORDED body the `à faire` cell shows. Tight on purpose: one clause, enough to
|
|
@@ -600,9 +648,27 @@ function windowDays(period) {
|
|
|
600
648
|
* reads it as the full record — the same honesty the old `extrait` column applied.
|
|
601
649
|
*/
|
|
602
650
|
const TODO_EXCERPT_MAX = 100;
|
|
651
|
+
const ULID = /[0-9A-HJKMNP-TV-Z]{26}/gu;
|
|
652
|
+
/**
|
|
653
|
+
* Owner-facing conductor cells never print aggregate identifiers. This runs while the shared view is built,
|
|
654
|
+
* so JSON, text, and Markdown consume the same redacted cells. The machine-only handle-resolution block
|
|
655
|
+
* keeps its ids intact so the report remains actionable.
|
|
656
|
+
*/
|
|
657
|
+
function redactOwnerText(value) {
|
|
658
|
+
return value.replace(ULID, 'référence interne');
|
|
659
|
+
}
|
|
660
|
+
function redactOwnerTable(table) {
|
|
661
|
+
const ownerColumns = new Set(table.columns.map((column) => column.id));
|
|
662
|
+
return {
|
|
663
|
+
...table,
|
|
664
|
+
rows: table.rows.map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, ownerColumns.has(key) ? redactOwnerText(value) : value]))),
|
|
665
|
+
...(table.lines === undefined ? {} : { lines: table.lines.map(redactOwnerText) }),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
603
668
|
/** `undefined` for an absent/blank body — a bare title is then the HONEST render, not a gap to fill. */
|
|
604
669
|
export function todoExcerpt(body) {
|
|
605
|
-
|
|
670
|
+
// Owner-facing excerpts can mention a record, but the record's ULID belongs in the machine-only handle block.
|
|
671
|
+
const cleaned = body === undefined ? undefined : clean(redactOwnerText(body));
|
|
606
672
|
if (cleaned === undefined || cleaned === '')
|
|
607
673
|
return undefined;
|
|
608
674
|
if (cleaned.length <= TODO_EXCERPT_MAX)
|
|
@@ -644,7 +710,25 @@ function isoDate(value) {
|
|
|
644
710
|
return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString().slice(0, 10);
|
|
645
711
|
}
|
|
646
712
|
/** Criterion 21 — the period, always bounded, always read from the log (plus the caller's clock). */
|
|
647
|
-
function
|
|
713
|
+
export function reportPeriod(meta) {
|
|
714
|
+
if (meta.periodWindow !== undefined) {
|
|
715
|
+
const period = meta.periodWindow;
|
|
716
|
+
const from = isoDate(period.from);
|
|
717
|
+
const to = isoDate(period.to);
|
|
718
|
+
if (period.requested === null) {
|
|
719
|
+
const toSource = meta.now !== undefined ? 'now' : meta.logTo !== undefined ? 'last-event' : 'unknown';
|
|
720
|
+
const suffix = toSource === 'last-event' ? ' (intégralité du journal, borne haute = dernier événement)' : ' (intégralité du journal)';
|
|
721
|
+
const label = from === undefined || to === undefined
|
|
722
|
+
? 'période : journal vide (aucun événement enregistré)'
|
|
723
|
+
: `période : ${from} → ${to}${suffix}`;
|
|
724
|
+
return { ...(from !== undefined ? { from } : {}), ...(to !== undefined ? { to } : {}), toSource, label };
|
|
725
|
+
}
|
|
726
|
+
const selector = period.requested === 'all' ? 'intégralité du journal' : `sélecteur : ${period.requested ?? 'intégralité du journal'}`;
|
|
727
|
+
const label = from === undefined || to === undefined
|
|
728
|
+
? 'période : journal vide (aucun événement enregistré)'
|
|
729
|
+
: `période : ${from} → ${to} (${selector}; ${period.eventsInWindow}/${period.eventsTotal} événements)`;
|
|
730
|
+
return { ...(from !== undefined ? { from } : {}), ...(to !== undefined ? { to } : {}), toSource: 'unknown', label };
|
|
731
|
+
}
|
|
648
732
|
const from = isoDate(meta.logFrom);
|
|
649
733
|
const now = isoDate(meta.now);
|
|
650
734
|
const last = isoDate(meta.logTo);
|
|
@@ -661,6 +745,23 @@ function buildPeriod(meta) {
|
|
|
661
745
|
label,
|
|
662
746
|
};
|
|
663
747
|
}
|
|
748
|
+
/** The raw period payload mirrors `reportPeriod` without reducing instants to calendar dates. */
|
|
749
|
+
export function reportPeriodPayload(meta) {
|
|
750
|
+
if (meta.periodWindow !== undefined)
|
|
751
|
+
return meta.periodWindow;
|
|
752
|
+
const from = meta.logFrom;
|
|
753
|
+
const to = meta.now ?? meta.logTo;
|
|
754
|
+
const events = meta.journalRevision?.events ?? 0;
|
|
755
|
+
return {
|
|
756
|
+
requested: null,
|
|
757
|
+
...(from !== undefined ? { from } : {}),
|
|
758
|
+
...(to !== undefined ? { to } : {}),
|
|
759
|
+
fromRef: null,
|
|
760
|
+
toRef: null,
|
|
761
|
+
eventsInWindow: events,
|
|
762
|
+
eventsTotal: events,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
664
765
|
const OPTION_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
665
766
|
const optionLetter = (index) => OPTION_LETTERS[index] ?? `#${index + 1}`;
|
|
666
767
|
/**
|
|
@@ -684,7 +785,11 @@ function compactRefs(refs) {
|
|
|
684
785
|
}
|
|
685
786
|
return [...parts, ...other].join(' · ');
|
|
686
787
|
}
|
|
788
|
+
function shellArgument(value) {
|
|
789
|
+
return /^[A-Za-z0-9_./:-]+$/u.test(value) ? value : `'${value.replace(/'/gu, "'\"'\"'")}'`;
|
|
790
|
+
}
|
|
687
791
|
export function buildWpConductorView(tree, decisions = [], outsideRollup = [], totalScope = 'global', meta = {}) {
|
|
792
|
+
assertOrderedPeriodWindow(meta);
|
|
688
793
|
const wpName = (n) => `${n.label} · ${clean(stripWpPrefix(n.title))}`;
|
|
689
794
|
const totals = wpTotals(tree, outsideRollup);
|
|
690
795
|
const wpNodes = [];
|
|
@@ -700,8 +805,8 @@ export function buildWpConductorView(tree, decisions = [], outsideRollup = [], t
|
|
|
700
805
|
// into their root — their leaves already roll up (`openLeaves`/`recentDoneLeaves` walk children), so
|
|
701
806
|
// nothing is lost; only their row disappears. The aggregation is DECLARED in the header, like every
|
|
702
807
|
// other compression in this report.
|
|
703
|
-
const period =
|
|
704
|
-
const days = windowDays(
|
|
808
|
+
const period = reportPeriod(meta);
|
|
809
|
+
const days = windowDays(meta);
|
|
705
810
|
const subWpDetail = meta.subWp === true || (days !== undefined && days < LONG_WINDOW_DAYS);
|
|
706
811
|
const rowNodes = subWpDetail ? wpNodes : [...tree];
|
|
707
812
|
const subNodes = wpNodes.filter((n) => !rowNodes.includes(n));
|
|
@@ -741,13 +846,16 @@ export function buildWpConductorView(tree, decisions = [], outsideRollup = [], t
|
|
|
741
846
|
// of the page maps every emitted handle to its item id, and no ULID enters a column the owner reads.
|
|
742
847
|
const handles = [];
|
|
743
848
|
// ---- FAIT ---------------------------------------------------------------------------------------
|
|
744
|
-
const outsideDone =
|
|
849
|
+
const outsideDone = meta.deliveredItemIds === undefined
|
|
850
|
+
? outsideRollup.filter((r) => r.bucket === 'DONE' || r.bucket === 'DROPPED')
|
|
851
|
+
: outsideRollup.filter((r) => meta.deliveredItemIds.has(r.id));
|
|
852
|
+
const periodLeaves = (node) => periodDoneLeaves(node, meta.deliveredItemIds);
|
|
745
853
|
const doneRows = [
|
|
746
854
|
{
|
|
747
855
|
scope: totalScope,
|
|
748
856
|
progress: `${totals.done}/${totals.active} (${pctStr(totals.pct)})`,
|
|
749
857
|
lastActions: lastActionsCell(wpNodes
|
|
750
|
-
.flatMap((n) => n.leaves.filter((l) => l.bucket === 'DONE'))
|
|
858
|
+
.flatMap((n) => meta.deliveredItemIds === undefined ? n.leaves.filter((l) => l.bucket === 'DONE') : n.leaves.filter((l) => meta.deliveredItemIds.has(l.id)))
|
|
751
859
|
.sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0))
|
|
752
860
|
.map((l) => clean(l.title))),
|
|
753
861
|
},
|
|
@@ -755,17 +863,19 @@ export function buildWpConductorView(tree, decisions = [], outsideRollup = [], t
|
|
|
755
863
|
// window is short or the owner asked. `recentDoneLeaves` already walks children, so a root's row
|
|
756
864
|
// carries its sub-levels' deliveries rather than losing them.
|
|
757
865
|
...rowNodes
|
|
758
|
-
.filter((n) => n.
|
|
866
|
+
.filter((n) => periodLeaves(n).length > 0)
|
|
759
867
|
.map((n) => ({
|
|
760
868
|
scope: wpName(n),
|
|
761
869
|
progress: `${n.done}/${n.active} (${pctStr(n.pct)})`,
|
|
762
|
-
lastActions: lastActionsCell(
|
|
870
|
+
lastActions: lastActionsCell(periodLeaves(n).map((l) => clean(l.title))),
|
|
763
871
|
})),
|
|
764
872
|
...(outsideDone.length > 0
|
|
765
873
|
? [{
|
|
766
874
|
scope: 'hors WP',
|
|
767
875
|
progress: (() => {
|
|
768
|
-
const done =
|
|
876
|
+
const done = meta.deliveredItemIds === undefined
|
|
877
|
+
? outsideRollup.filter((r) => r.bucket === 'DONE').length
|
|
878
|
+
: outsideRollup.filter((r) => meta.deliveredItemIds.has(r.id)).length;
|
|
769
879
|
const active = outsideRollup.filter((r) => r.bucket !== 'DROPPED').length;
|
|
770
880
|
return `${done}/${active} (${pctStr(active === 0 ? 'n/a' : Math.round((done / active) * 100))})`;
|
|
771
881
|
})(),
|
|
@@ -838,7 +948,10 @@ export function buildWpConductorView(tree, decisions = [], outsideRollup = [], t
|
|
|
838
948
|
// A directive may target a DONE leaf with acceptance debt: name it here (with its own handle)
|
|
839
949
|
// instead of exiling it to a `cible action` column the owner never asked for.
|
|
840
950
|
for (const d of attached) {
|
|
841
|
-
|
|
951
|
+
// An engagement/blockage directive points at its own actionable ref (for example a thread),
|
|
952
|
+
// while its title deliberately names the already-listed target leaf. Adding it made a title-twin
|
|
953
|
+
// sibling row. Only an item directive can introduce a missing DONE acceptance debt.
|
|
954
|
+
if (d.target.kind !== 'item' || items.some((i) => i.id === d.target.id))
|
|
842
955
|
continue;
|
|
843
956
|
const debtLeaf = wpNodes.flatMap((node) => node.leaves).find((l) => l.id === d.target.id);
|
|
844
957
|
items.push({
|
|
@@ -972,7 +1085,7 @@ export function buildWpConductorView(tree, decisions = [], outsideRollup = [], t
|
|
|
972
1085
|
const selected = d.selectedOptionId === undefined ? undefined : letterOf.get(d.selectedOptionId);
|
|
973
1086
|
options.forEach((option, i) => {
|
|
974
1087
|
const letter = optionLetter(i);
|
|
975
|
-
const wrapped = wrapCell(`${letter} ${clean(option.title)} — ${clean(option.summary)}`, DECISION_CAPS[2]);
|
|
1088
|
+
const wrapped = wrapCell(`${letter} ${redactOwnerText(clean(option.title))} — ${redactOwnerText(clean(option.summary))}`, DECISION_CAPS[2]);
|
|
976
1089
|
const marks = [];
|
|
977
1090
|
if (letter === recommended)
|
|
978
1091
|
marks.push(letter);
|
|
@@ -1066,62 +1179,77 @@ export function buildWpConductorView(tree, decisions = [], outsideRollup = [], t
|
|
|
1066
1179
|
};
|
|
1067
1180
|
const header = {
|
|
1068
1181
|
scope: totalScope,
|
|
1182
|
+
...(meta.scopeProjection !== undefined ? { scopeProjection: meta.scopeProjection } : {}),
|
|
1183
|
+
...(meta.journalRevision !== undefined ? { journalRevision: meta.journalRevision } : {}),
|
|
1069
1184
|
progress: `${totals.done}/${totals.active} (${pctStr(totals.pct)})`,
|
|
1070
1185
|
...(meta.baselineCommit !== undefined ? { baselineCommit: meta.baselineCommit.slice(0, 12) } : {}),
|
|
1071
1186
|
// Criterion 21 — the window is measured in the log, so it is always stated, always with dates.
|
|
1072
1187
|
period,
|
|
1073
|
-
sources: [
|
|
1188
|
+
sources: [
|
|
1189
|
+
'projection déterministe du journal (track report --wp --decisions)',
|
|
1190
|
+
...(meta.journalRevision === undefined
|
|
1191
|
+
? []
|
|
1192
|
+
: [`révision du journal : ${meta.journalRevision.events} événements ; tête : ${meta.journalRevision.head ?? 'aucune'}`]),
|
|
1193
|
+
...(meta.scopeProjection === undefined
|
|
1194
|
+
? []
|
|
1195
|
+
: [
|
|
1196
|
+
`scope : ${meta.scopeProjection.label} et son sous-arbre inclus ; ${meta.scopeProjection.excludedProjectionRows} lignes hors scope exclues`,
|
|
1197
|
+
]),
|
|
1198
|
+
],
|
|
1074
1199
|
coverage,
|
|
1075
|
-
handleCommand:
|
|
1200
|
+
handleCommand: meta.scopeProjection === undefined
|
|
1201
|
+
? 'track report --resolve <handle>'
|
|
1202
|
+
: `track report --scope ${shellArgument(meta.scopeProjection.selector)} --resolve <handle>`,
|
|
1076
1203
|
};
|
|
1204
|
+
const tables = [
|
|
1205
|
+
{
|
|
1206
|
+
id: 'done',
|
|
1207
|
+
title: 'FAIT',
|
|
1208
|
+
columns: [
|
|
1209
|
+
{ id: 'scope', label: 'scope' },
|
|
1210
|
+
{ id: 'progress', label: 'avancement' },
|
|
1211
|
+
{ id: 'lastActions', label: 'dernières actions' },
|
|
1212
|
+
],
|
|
1213
|
+
rows: doneRows,
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
id: 'todo',
|
|
1217
|
+
title: 'À-FAIRE',
|
|
1218
|
+
columns: [
|
|
1219
|
+
{ id: 'wp', label: 'WP' },
|
|
1220
|
+
{ id: 'progress', label: 'av.' },
|
|
1221
|
+
{ id: 'todo', label: 'à faire' },
|
|
1222
|
+
{ id: 'blocked', label: 'bloqué' },
|
|
1223
|
+
{ id: 'nextAction', label: 'prochaine action' },
|
|
1224
|
+
],
|
|
1225
|
+
rows: todoRows,
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
id: 'decisions',
|
|
1229
|
+
title: 'DÉCISIONS',
|
|
1230
|
+
render: 'drawn',
|
|
1231
|
+
columns: [
|
|
1232
|
+
{ id: 'n', label: '#' },
|
|
1233
|
+
{ id: 'subject', label: 'sujet' },
|
|
1234
|
+
{ id: 'alternatives', label: 'alternatives' },
|
|
1235
|
+
{ id: 'preco', label: 'préco' },
|
|
1236
|
+
],
|
|
1237
|
+
rows: decisionRows,
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
id: 'recommendation',
|
|
1241
|
+
title: 'RECOMMANDATION',
|
|
1242
|
+
render: 'prose',
|
|
1243
|
+
columns: [],
|
|
1244
|
+
rows: [],
|
|
1245
|
+
lines: recommendationLines,
|
|
1246
|
+
},
|
|
1247
|
+
];
|
|
1077
1248
|
return {
|
|
1078
1249
|
kind: 'wp-conductor-report',
|
|
1079
1250
|
locale: 'fr',
|
|
1080
1251
|
header,
|
|
1081
|
-
tables:
|
|
1082
|
-
{
|
|
1083
|
-
id: 'done',
|
|
1084
|
-
title: 'FAIT',
|
|
1085
|
-
columns: [
|
|
1086
|
-
{ id: 'scope', label: 'scope' },
|
|
1087
|
-
{ id: 'progress', label: 'avancement' },
|
|
1088
|
-
{ id: 'lastActions', label: 'dernières actions' },
|
|
1089
|
-
],
|
|
1090
|
-
rows: doneRows,
|
|
1091
|
-
},
|
|
1092
|
-
{
|
|
1093
|
-
id: 'todo',
|
|
1094
|
-
title: 'À-FAIRE',
|
|
1095
|
-
columns: [
|
|
1096
|
-
{ id: 'wp', label: 'WP' },
|
|
1097
|
-
{ id: 'progress', label: 'av.' },
|
|
1098
|
-
{ id: 'todo', label: 'à faire' },
|
|
1099
|
-
{ id: 'blocked', label: 'bloqué' },
|
|
1100
|
-
{ id: 'nextAction', label: 'prochaine action' },
|
|
1101
|
-
],
|
|
1102
|
-
rows: todoRows,
|
|
1103
|
-
},
|
|
1104
|
-
{
|
|
1105
|
-
id: 'decisions',
|
|
1106
|
-
title: 'DÉCISIONS',
|
|
1107
|
-
render: 'drawn',
|
|
1108
|
-
columns: [
|
|
1109
|
-
{ id: 'n', label: '#' },
|
|
1110
|
-
{ id: 'subject', label: 'sujet' },
|
|
1111
|
-
{ id: 'alternatives', label: 'alternatives' },
|
|
1112
|
-
{ id: 'preco', label: 'préco' },
|
|
1113
|
-
],
|
|
1114
|
-
rows: decisionRows,
|
|
1115
|
-
},
|
|
1116
|
-
{
|
|
1117
|
-
id: 'recommendation',
|
|
1118
|
-
title: 'RECOMMANDATION',
|
|
1119
|
-
render: 'prose',
|
|
1120
|
-
columns: [],
|
|
1121
|
-
rows: [],
|
|
1122
|
-
lines: recommendationLines,
|
|
1123
|
-
},
|
|
1124
|
-
],
|
|
1252
|
+
tables: tables.map(redactOwnerTable),
|
|
1125
1253
|
handles,
|
|
1126
1254
|
coverage,
|
|
1127
1255
|
directives,
|
|
@@ -1172,7 +1300,7 @@ function headerLines(view, format) {
|
|
|
1172
1300
|
em(`sources : ${h.sources.join(' ; ')}`),
|
|
1173
1301
|
'',
|
|
1174
1302
|
];
|
|
1175
|
-
return lines;
|
|
1303
|
+
return lines.map(redactOwnerText);
|
|
1176
1304
|
}
|
|
1177
1305
|
/**
|
|
1178
1306
|
* Criteria 10b/10c — the machine's half of the page. It is NOT a fifth section and NOT a table the owner
|
|
@@ -1194,12 +1322,11 @@ export function resolutionLines(view) {
|
|
|
1194
1322
|
return lines;
|
|
1195
1323
|
}
|
|
1196
1324
|
function renderReportView(view, format) {
|
|
1325
|
+
assertReportFormat(format);
|
|
1197
1326
|
if (format === 'json')
|
|
1198
1327
|
return JSON.stringify(view, null, 2) + '\n';
|
|
1199
|
-
//
|
|
1200
|
-
//
|
|
1201
|
-
// is clean. The view model itself stays RAW (escaping is a render-only concern). `displayCell` keeps the
|
|
1202
|
-
// machine-generated `[n.m]` handle out of that escaped span so the three formats agree on it.
|
|
1328
|
+
// The builder already redacts owner cells for every format. Rendering only escapes Markdown metacharacters;
|
|
1329
|
+
// `displayCell` keeps the machine-generated `[n.m]` handle out of that escaped span.
|
|
1203
1330
|
const esc = (s) => displayCell(s, format);
|
|
1204
1331
|
const h = (label) => (format === 'md' ? `## ${label}` : label);
|
|
1205
1332
|
const lines = headerLines(view, format);
|
|
@@ -1258,7 +1385,7 @@ export function formatWpConductor(tree, format, decisions = [], outsideRollup =
|
|
|
1258
1385
|
}
|
|
1259
1386
|
/** Clean + hard-truncate a line to `width` with a trailing ellipsis (never a silent cut mid-report). */
|
|
1260
1387
|
function truncateLine(s, width) {
|
|
1261
|
-
const c = clean(s);
|
|
1388
|
+
const c = redactOwnerText(clean(s));
|
|
1262
1389
|
return c.length <= width ? c : `${c.slice(0, Math.max(1, width - 1))}…`;
|
|
1263
1390
|
}
|
|
1264
1391
|
/**
|
|
@@ -1271,12 +1398,12 @@ function fitMiddle(head, mid, tail, width) {
|
|
|
1271
1398
|
if (budget < 8)
|
|
1272
1399
|
return truncateLine(head + mid + tail, width);
|
|
1273
1400
|
const m = mid.length <= budget ? mid : `${mid.slice(0, Math.max(1, budget - 1))}…`;
|
|
1274
|
-
return head + m + tail;
|
|
1401
|
+
return redactOwnerText(head + m + tail);
|
|
1275
1402
|
}
|
|
1276
|
-
export function formatWpConductorInline(tree, decisions = [], opts = {}, outsideRollup = []) {
|
|
1403
|
+
export function formatWpConductorInline(tree, decisions = [], opts = {}, outsideRollup = [], meta) {
|
|
1277
1404
|
const width = Math.min(240, Math.max(40, opts.width ?? 80));
|
|
1278
1405
|
const maxDir = Math.max(1, opts.maxDirectives ?? 10);
|
|
1279
|
-
const view = buildWpConductorView(tree, decisions, outsideRollup);
|
|
1406
|
+
const view = buildWpConductorView(tree, decisions, outsideRollup, 'global', meta);
|
|
1280
1407
|
const totals = wpTotals(tree, outsideRollup);
|
|
1281
1408
|
const wpName = (n) => `${n.label} · ${clean(stripWpPrefix(n.title))}`;
|
|
1282
1409
|
const wpNodes = [];
|
|
@@ -1288,6 +1415,10 @@ export function formatWpConductorInline(tree, decisions = [], opts = {}, outside
|
|
|
1288
1415
|
};
|
|
1289
1416
|
collectWpNodes(tree);
|
|
1290
1417
|
const lines = [];
|
|
1418
|
+
// INLINE is still a report rendering. The CLI supplies meta and therefore names the exact period; direct
|
|
1419
|
+
// presenter callers that have no reporting context retain their existing compact-only contract.
|
|
1420
|
+
if (meta !== undefined)
|
|
1421
|
+
lines.push(truncateLine(view.header.period.label, width));
|
|
1291
1422
|
// FAIT — one line: global progress + the closed WPs (by label).
|
|
1292
1423
|
const closed = wpNodes.filter((n) => n.pct === 100).map((n) => n.label);
|
|
1293
1424
|
lines.push(truncateLine(`FAIT${opts.totalScope !== undefined ? ` (${opts.totalScope})` : ''} ${totals.done}/${totals.active} (${pctStr(totals.pct)})${closed.length > 0 ? ` · clos: ${closed.join(', ')}` : ''}${outsideRollup.length > 0 ? ` · hors rollup: ${outsideRollup.length}` : ''}`, width));
|