create-pmos 0.21.0 → 0.23.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/package.json +5 -3
- package/payload/AGENTS.md +7 -0
- package/payload/KIT-VERSION +1 -1
- package/payload/README.md +16 -1
- package/payload/scripts/anchor-resolve.js +277 -0
- package/payload/scripts/delivery-index.js +278 -0
- package/payload/scripts/workflow-state.js +1139 -0
- package/payload/scripts/workflow-sweep.js +764 -0
- package/workspace/lib/console-state.js +39 -18
- package/workspace/lib/engine/delivery-index.js +278 -0
- package/workspace/lib/engine/workflow-state.js +165 -21
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +3 -3
- package/workspace/public/pm/data.js +12 -2
- package/workspace/public/pm/panel-repos.js +10 -1
- package/workspace/server.js +159 -61
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.23.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "PMOS Terminal Workspace
|
|
5
|
+
"description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"pmos": "bin/pmos.js"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"playwright-core": "^1.61.1",
|
|
30
30
|
"ws": "^8.18.0"
|
|
31
31
|
},
|
|
32
|
-
"//devDependencies": "design-parity's pixel axis only. Kept OUT of dependencies deliberately: `create-pmos workspace` installs this package with `npm install -g`, which takes dependencies and not devDependencies, so an adopter installing a local PM tool does not also install a screenshot differ. design-parity.js requires both optionally and degrades to text+style+geometry when they are absent, so the parity check still runs
|
|
32
|
+
"//devDependencies": "design-parity's pixel axis only. Kept OUT of dependencies deliberately: `create-pmos workspace` installs this package with `npm install -g`, which takes dependencies and not devDependencies, so an adopter installing a local PM tool does not also install a screenshot differ. design-parity.js requires both optionally and degrades to text+style+geometry when they are absent, so the parity check still runs — it just stops measuring pixels, and says so.",
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"pixelmatch": "^7.1.0",
|
|
35
35
|
"pngjs": "^7.0.0"
|
|
@@ -766,6 +766,11 @@ export async function loadRepos() {
|
|
|
766
766
|
name,
|
|
767
767
|
kind: prod && prod.kind === 'github' ? 'github' : 'local',
|
|
768
768
|
count: prod && prod.count !== null ? prod.count : null,
|
|
769
|
+
// The board's OTHER population, published beside the count rather than folded into it — see
|
|
770
|
+
// `prod.unrecorded` in loadBoard and the sub-line in panel-repos.js#toTuple. A row that
|
|
771
|
+
// reports only the spine's number is accurate about itself and silent about the rest of the
|
|
772
|
+
// panel; the two together are what make panel 1 and panel 2 unable to disagree.
|
|
773
|
+
unrecorded: prod && typeof prod.unrecorded === 'number' ? prod.unrecorded : 0,
|
|
769
774
|
};
|
|
770
775
|
});
|
|
771
776
|
}
|
|
@@ -824,12 +829,17 @@ export async function addRepo({ source, kind }) {
|
|
|
824
829
|
publishBoards();
|
|
825
830
|
const prod = products.get(p.name);
|
|
826
831
|
return {
|
|
827
|
-
repo: { name: p.name, kind: 'local', count: prod.count },
|
|
832
|
+
repo: { name: p.name, kind: 'local', count: prod.count, unrecorded: prod.unrecorded || 0 },
|
|
828
833
|
log: [
|
|
829
834
|
{ t: 'registered ' + w.root, c: 'var(--muted-foreground)' },
|
|
830
835
|
prod.error
|
|
831
836
|
? { t: 'the initiative spine could not be read — ' + prod.error, c: 'var(--destructive)' }
|
|
832
|
-
|
|
837
|
+
// The log says WHERE the records were read from, because "0 initiative record(s)" against
|
|
838
|
+
// a repo that has them is the defect this initiative exists to close — a line naming
|
|
839
|
+
// `planning/` on a kit repo that keeps them in `pmos/state/` would have described a
|
|
840
|
+
// directory the server never opened.
|
|
841
|
+
: { t: 'read ' + (prod.count === 1 ? '1 initiative record' : (prod.count || 0) + ' initiative records')
|
|
842
|
+
+ (prod.unrecorded ? ' · ' + prod.unrecorded + ' artifact group(s) with no record' : ''), c: 'var(--muted-foreground)' },
|
|
833
843
|
{ t: 'done — ' + p.name + ' added', c: 'var(--success)' },
|
|
834
844
|
],
|
|
835
845
|
};
|
|
@@ -57,7 +57,16 @@ function toTuple(r) {
|
|
|
57
57
|
* read (a folder with no `planning/`, a tree that errored) rendered "local · " — a sub-line that
|
|
58
58
|
* trails off mid-sentence and reads as a bug in the row rather than as "PMOS has not counted
|
|
59
59
|
* this". The dash is the design's own vocabulary for a value that is not there. */
|
|
60
|
-
|
|
60
|
+
/* …AND THE ROW MAY NOT STATE A NUMBER THE BOARD CONTRADICTS. The headline count is the SPINE's —
|
|
61
|
+
* that ruling stands, it is what keeps `176 initiatives` off a repository holding 138 records and
|
|
62
|
+
* 38 orphans. What it did not cover is the other side of the same coin: with no records found and
|
|
63
|
+
* artifacts grouped anyway, the row printed a confident `0 initiatives` over a board the PM could
|
|
64
|
+
* see was not empty, and on this repo it silently dropped 20 groups from a headline of 177. The
|
|
65
|
+
* second clause names that population in the board's own vocabulary ("artifacts with no
|
|
66
|
+
* initiative record"), counted separately and never added in. Absent when there are none. */
|
|
67
|
+
const un = typeof r.unrecorded === 'number' && r.unrecorded > 0 ? r.unrecorded : 0;
|
|
68
|
+
const sub = r.sub || (n === null ? '—'
|
|
69
|
+
: n + (n === 1 ? ' initiative' : ' initiatives') + (un ? ' · ' + un + ' unrecorded' : ''));
|
|
61
70
|
const kind = (r.kind || r.source) === 'local' ? 'local' : (r.kind || r.source) === 'github' ? 'github'
|
|
62
71
|
: (r.remote || r.origin) ? 'github' : 'local';
|
|
63
72
|
return [String(id), sub, kind];
|
package/workspace/server.js
CHANGED
|
@@ -495,11 +495,97 @@ function findDirsNamed(root, name, maxDepth) {
|
|
|
495
495
|
return found;
|
|
496
496
|
}
|
|
497
497
|
function planningRoots(root) { return findDirsNamed(root, 'planning', BUNDLE_MAX_DEPTH); }
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
|
|
499
|
+
/*
|
|
500
|
+
* ── WHERE A REPOSITORY KEEPS ITS INITIATIVE RECORDS ──────────────────────────────────────────
|
|
501
|
+
*
|
|
502
|
+
* THE ONE ANSWER. `surfaceSpine` and `surfaceOkr` both used `findDirsNamed(root,'initiatives',2)`
|
|
503
|
+
* directly, and that search reaches `planning/initiatives/` at the repo root and nothing deeper —
|
|
504
|
+
* one level above where the scaffolder THIS PROJECT PUBLISHES puts records:
|
|
505
|
+
*
|
|
506
|
+
* planning/initiatives/ depth 2 pmos-self … found before
|
|
507
|
+
* pmos/planning/initiatives/ depth 3 a bundle vendored under pmos/ … MISSED
|
|
508
|
+
* pmos/state/initiatives/ depth 3 every create-pmos kit … MISSED
|
|
509
|
+
*
|
|
510
|
+
* So a kit adopter's repository reported `ok:true, empty:true` — a SUCCESSFUL read of nothing —
|
|
511
|
+
* and panel 1 printed a confident `0 initiatives` over a repo full of records. `planningRoots`
|
|
512
|
+
* finds `pmos/planning` (that name IS at depth 2), so the library lens saw the same repo's
|
|
513
|
+
* documents while the spine saw no records at all: two panels, one repository, contradictory.
|
|
514
|
+
*
|
|
515
|
+
* The fact was already in this process twice. `stateDirFor` resolves — and will create —
|
|
516
|
+
* `pmos/state/`, and `scripts/workflow-state.js#layout`, the engine every ladder derives through,
|
|
517
|
+
* branches on `pmos/state/initiatives` by name. Only the READ path had never been told.
|
|
518
|
+
*
|
|
519
|
+
* DERIVED FROM BUNDLE ROOTS, NOT FROM A DEEPER WALK. Raising BUNDLE_MAX_DEPTH would have found
|
|
520
|
+
* these directories and also widened `prototype`, `okf`, `skills` and `build-skills` discovery,
|
|
521
|
+
* for which this change has no finding. Instead each bundle root — the repo root, plus the parent
|
|
522
|
+
* of every discovered `planning/` and `state/` directory — is asked for the two layouts PMOS
|
|
523
|
+
* ships. That is a fixed pair of existence checks per bundle root over walks already performed,
|
|
524
|
+
* and it cannot start proposing records out of a directory nobody vendored.
|
|
525
|
+
*/
|
|
526
|
+
function isDirSafe(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
|
527
|
+
|
|
528
|
+
function bundleRoots(root) {
|
|
529
|
+
const out = [root];
|
|
530
|
+
const seen = new Set([path.resolve(root)]);
|
|
531
|
+
const add = (d) => { const r = path.resolve(d); if (!seen.has(r)) { seen.add(r); out.push(d); } };
|
|
532
|
+
for (const p of planningRoots(root)) add(path.dirname(p));
|
|
533
|
+
for (const d of findDirsNamed(root, 'state', BUNDLE_MAX_DEPTH)) add(path.dirname(d));
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Every directory this repository keeps initiative RECORDS in. Existence-checked, de-duplicated. */
|
|
538
|
+
function initiativeDirs(root) {
|
|
539
|
+
const out = [];
|
|
540
|
+
const seen = new Set();
|
|
541
|
+
const add = (d) => {
|
|
542
|
+
const r = path.resolve(d);
|
|
543
|
+
if (seen.has(r) || !isDirSafe(d)) return;
|
|
544
|
+
seen.add(r); out.push(d);
|
|
545
|
+
};
|
|
546
|
+
// The original search first, so no layout that worked before can stop working.
|
|
547
|
+
for (const d of findDirsNamed(root, 'initiatives', BUNDLE_MAX_DEPTH)) add(d);
|
|
548
|
+
for (const b of bundleRoots(root)) {
|
|
549
|
+
add(path.join(b, 'planning', 'initiatives')); // pmos-self, at the root or in a bundle
|
|
550
|
+
add(path.join(b, 'state', 'initiatives')); // the create-pmos kit (kit/build-kit.sh)
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/*
|
|
556
|
+
* The health-budget registry, read from the bundle root that HAS one.
|
|
557
|
+
*
|
|
558
|
+
* `anchors.readBudgets(root)` is a fixed `<root>/planning/okrs/health-budgets.md`. A kit vendors it
|
|
559
|
+
* to `pmos/planning/okrs/health-budgets.md` (build-kit.sh writes it on every fresh vendor), so in
|
|
560
|
+
* a kit repo it was never read — and every `parent_kr: health:<id>` resolved against an empty
|
|
561
|
+
* registry into `unanchored`, with a reason that blamed the record. Finding the records without
|
|
562
|
+
* this would have replaced an invisible initiative with a misfiled one.
|
|
563
|
+
*
|
|
564
|
+
* A repo with no registry ANYWHERE still gets the root-relative `{ok:false}` — absence is reported,
|
|
565
|
+
* never faked, and `budgets_readable` stays false so the surface can say so.
|
|
566
|
+
*/
|
|
567
|
+
function budgetsFor(A, root) {
|
|
568
|
+
const first = A.readBudgets(root);
|
|
569
|
+
if (first && first.ok) return first;
|
|
570
|
+
for (const b of bundleRoots(root)) {
|
|
571
|
+
if (path.resolve(b) === path.resolve(root)) continue;
|
|
572
|
+
const r = A.readBudgets(b);
|
|
573
|
+
if (r && r.ok) return { ...r, path: path.relative(root, path.join(b, 'planning', 'okrs', 'health-budgets.md')) };
|
|
574
|
+
// The engine's kit path for the same registry (workflow-state.js#layout).
|
|
575
|
+
const kit = path.join(b, 'state', 'health-budgets.md');
|
|
576
|
+
try { return { ok: true, text: fs.readFileSync(kit, 'utf8'), path: path.relative(root, kit) }; }
|
|
577
|
+
catch { /* not here either — keep looking, then fall back to the root's own answer */ }
|
|
578
|
+
}
|
|
579
|
+
return first;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// A repo with a planning/ bundle (at any shallow depth), initiative records in a layout PMOS ships,
|
|
583
|
+
// or a DECISIONS.md gets the initiative lens + curated knowledge; anything else is a plain document
|
|
584
|
+
// tree. Detection is nested-aware so a repo that vendors a PMOS bundle under a sub-folder still
|
|
585
|
+
// shows its initiatives (the reported defect) — including a kit whose records are its whole bundle.
|
|
501
586
|
function wsShape(root) {
|
|
502
|
-
return planningRoots(root).length ||
|
|
587
|
+
return planningRoots(root).length || initiativeDirs(root).length
|
|
588
|
+
|| fs.existsSync(path.join(root, 'DECISIONS.md')) ? 'pmos' : 'plain';
|
|
503
589
|
}
|
|
504
590
|
|
|
505
591
|
// ── Library: the file tree the sidebar browses. ───────────────────────────────────────
|
|
@@ -739,12 +825,37 @@ function walkMd(dir, acc) {
|
|
|
739
825
|
}
|
|
740
826
|
return acc;
|
|
741
827
|
}
|
|
828
|
+
/*
|
|
829
|
+
* Initiative RECORDS that live outside every planning root — `pmos/state/initiatives/` in a
|
|
830
|
+
* create-pmos kit. `walkMd` over the planning roots cannot reach them, so in a kit repo the one
|
|
831
|
+
* document that carries the lane and the anchor was absent from its own initiative's artifact list
|
|
832
|
+
* even once the spine could see it. They are read through the SAME `docMeta` as every other
|
|
833
|
+
* document (ROLE_BY_DIR maps an `initiatives/` parent to role `record`, wherever it sits), so the
|
|
834
|
+
* join, the role and the id come from one implementation rather than a second opinion.
|
|
835
|
+
*/
|
|
836
|
+
function looseRecordMetas(root, proots) {
|
|
837
|
+
const under = (d) => proots.some((p) => insideRoot(path.resolve(p), path.resolve(d)));
|
|
838
|
+
const out = [];
|
|
839
|
+
for (const dir of initiativeDirs(root)) {
|
|
840
|
+
if (under(dir)) continue; // already covered by the planning walk
|
|
841
|
+
let ents = [];
|
|
842
|
+
try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
843
|
+
for (const e of ents) {
|
|
844
|
+
if (!e.isFile() || !e.name.endsWith('.md') || e.name.startsWith('.')) continue;
|
|
845
|
+
const m = docMeta(root, path.join(dir, e.name));
|
|
846
|
+
if (m) out.push(m);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
|
|
742
852
|
function initiativeIndex(root) {
|
|
743
853
|
const proots = planningRoots(root); // planning/ at the root OR nested (e.g. IDPF/pmos/planning)
|
|
744
854
|
const specimens = specimenMetas(root); // prototype/<id>/ — outside planning/, grouped by dir
|
|
745
|
-
|
|
855
|
+
const records = looseRecordMetas(root, proots); // pmos/state/initiatives/ — outside planning/ too
|
|
856
|
+
if (!proots.length && !specimens.length && !records.length) return { initiatives: [], ungrouped: [] };
|
|
746
857
|
const metas = proots.flatMap((p) => walkMd(p, [])).map((f) => docMeta(root, f))
|
|
747
|
-
.filter(Boolean).concat(specimens);
|
|
858
|
+
.filter(Boolean).concat(specimens).concat(records);
|
|
748
859
|
// Pass 1: every id an artifact explicitly declares. Only these are real initiatives —
|
|
749
860
|
// a filename stem is promoted to a group only when it matches a declared id or when
|
|
750
861
|
// several artifacts share it, so a lone research note never invents an initiative.
|
|
@@ -811,71 +922,58 @@ function library(root) {
|
|
|
811
922
|
// ("— not tracked"), never invented (D14/D40 honesty).
|
|
812
923
|
const PHASE_OF = { planning: 'plan', building: 'deliver', blocked: 'deliver', delivered: 'report', accepted: 'report', unknown: 'plan' };
|
|
813
924
|
|
|
814
|
-
// ── Delivery evidence (board-delivery-truth R-2/R-3).
|
|
925
|
+
// ── Delivery evidence (board-delivery-truth R-2/R-3; shared reader, workflow-status-sweep R-1). ──
|
|
815
926
|
// "Has this shipped?" answered from the trunk's own history rather than from anything anyone typed.
|
|
816
927
|
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
928
|
+
// THIS DERIVATION NO LONGER LIVES HERE. It was module-private to this file, which meant the GUI's
|
|
929
|
+
// initiative board answered the question with attribution while the engine's ladder
|
|
930
|
+
// (`scripts/workflow-state.js`) answered it without any — `buildCommits` counted every build commit
|
|
931
|
+
// in a range with no idea which initiative it was answering about, and over the trunk reported the
|
|
932
|
+
// same 488 commits as evidence for all 177 initiatives. Two readers, one repository, different
|
|
933
|
+
// answers to "did this ship". `scripts/delivery-index.js` is now the single reader, in the same
|
|
934
|
+
// shape and for the same reason as `scripts/anchor-resolve.js`; its header carries the two-signal
|
|
935
|
+
// rationale that used to be written here.
|
|
820
936
|
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
function trunkRef(root) {
|
|
834
|
-
for (const c of ['origin/main', 'main', 'origin/master', 'master']) {
|
|
835
|
-
const r = gitCap(root, ['rev-parse', '--verify', '--quiet', c]);
|
|
836
|
-
if (r.ok && r.out.trim()) return { ref: c, head: r.out.trim() };
|
|
937
|
+
// Resolution follows the ANCHOR_LIB_PATHS pattern below: a repo checkout first (so a dev surface and
|
|
938
|
+
// CI cannot disagree), then the copy `scripts/vendor-workspace-engine.js` vendors into the bundle.
|
|
939
|
+
// The engine requires this module, so it is already inside the vendored closure — an adopter's
|
|
940
|
+
// install carries it without a second packaging step to remember.
|
|
941
|
+
const DELIVERY_LIB_PATHS = [
|
|
942
|
+
path.join(__dirname, '..', 'scripts', 'delivery-index.js'),
|
|
943
|
+
path.join(__dirname, 'lib', 'engine', 'delivery-index.js'),
|
|
944
|
+
];
|
|
945
|
+
function deliveryLib() {
|
|
946
|
+
for (const p of DELIVERY_LIB_PATHS) {
|
|
947
|
+
if (!fs.existsSync(p)) continue;
|
|
948
|
+
try { return require(p); } catch { return null; }
|
|
837
949
|
}
|
|
838
950
|
return null;
|
|
839
951
|
}
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
952
|
+
function isDeliveryPath(f) {
|
|
953
|
+
const D = deliveryLib();
|
|
954
|
+
// Absent library → no path can be CONFIRMED as a delivery path. Returning `true` here would let
|
|
955
|
+
// every commit count as delivery evidence in exactly the install where the evidence cannot be
|
|
956
|
+
// read, which is the false-green direction. `deliveryIndex` below reports the absence out loud.
|
|
957
|
+
return D ? D.isDeliveryPath(f) : false;
|
|
846
958
|
}
|
|
847
959
|
/**
|
|
848
960
|
* → { ok, reason?, byBranch:Set<id>, byMention:Set<id> }
|
|
849
961
|
* `ok:false` is a REAL answer and never degrades into "nothing was delivered" — the caller renders
|
|
850
962
|
* it red and marks every initiative's delivery `unknown` (R-3). A board that cannot read the
|
|
851
963
|
* history must say so; silently reporting `planning` for 110 shipped initiatives is the failure
|
|
852
|
-
* this whole initiative exists to remove.
|
|
964
|
+
* this whole initiative exists to remove. A missing library is the same class of answer as an
|
|
965
|
+
* unreadable history and takes the same `ok:false` path.
|
|
853
966
|
*/
|
|
854
967
|
function deliveryIndex(root, ids) {
|
|
855
|
-
const
|
|
856
|
-
if (!
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
const byBranch = new Set(), byMention = new Set();
|
|
866
|
-
const known = Array.isArray(ids) ? ids : [...ids];
|
|
867
|
-
for (const chunk of r.out.split('\x1e')) {
|
|
868
|
-
if (!chunk.trim()) continue;
|
|
869
|
-
const cut = chunk.indexOf('\x1f');
|
|
870
|
-
const msg = cut < 0 ? chunk : chunk.slice(0, cut);
|
|
871
|
-
const files = cut < 0 ? [] : chunk.slice(cut + 1).split('\n').map((s) => s.trim()).filter(Boolean);
|
|
872
|
-
for (const m of msg.matchAll(/initiative\/([A-Za-z0-9][\w.-]*)/g)) byBranch.add(m[1]);
|
|
873
|
-
if (!files.some(isDeliveryPath)) continue;
|
|
874
|
-
for (const id of known) if (id.length > 3 && msg.includes(id)) byMention.add(id);
|
|
875
|
-
}
|
|
876
|
-
const index = { ok: true, byBranch, byMention };
|
|
877
|
-
deliveryCache.set(root, { head: t.head, index });
|
|
878
|
-
return index;
|
|
968
|
+
const D = deliveryLib();
|
|
969
|
+
if (!D) {
|
|
970
|
+
return {
|
|
971
|
+
ok: false,
|
|
972
|
+
reason: 'the delivery reader (scripts/delivery-index.js) is not present in this install — delivery cannot be derived here',
|
|
973
|
+
byBranch: new Set(), byMention: new Set(),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
return D.deliveryIndex(root, ids);
|
|
879
977
|
}
|
|
880
978
|
/**
|
|
881
979
|
* The five states, in precedence order. Every one of them names the artifact it was read from, so
|
|
@@ -1075,10 +1173,10 @@ function surfaceOkr(root) {
|
|
|
1075
1173
|
// Anchor health, NOT the initiative↔KR join — the join is an explicit non-goal of this spec. What
|
|
1076
1174
|
// is reported is only whether each anchor names something these documents declare, because an
|
|
1077
1175
|
// anchor pointing at nothing is precisely what went unseen for two days.
|
|
1078
|
-
const budgets = A
|
|
1176
|
+
const budgets = budgetsFor(A, root);
|
|
1079
1177
|
const unresolvable = [];
|
|
1080
1178
|
let checked = 0, empty = 0;
|
|
1081
|
-
for (const dir of
|
|
1179
|
+
for (const dir of initiativeDirs(root)) {
|
|
1082
1180
|
let files = [];
|
|
1083
1181
|
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')); } catch { continue; }
|
|
1084
1182
|
for (const f of files) {
|
|
@@ -1124,7 +1222,7 @@ function surfaceSpine(root) {
|
|
|
1124
1222
|
if (!index.ok) {
|
|
1125
1223
|
return { objectives: [], budgets: [], unanchored: [], unreadable: index.error || 'the OKR documents could not be read' };
|
|
1126
1224
|
}
|
|
1127
|
-
const budgetsDoc = A
|
|
1225
|
+
const budgetsDoc = budgetsFor(A, root);
|
|
1128
1226
|
|
|
1129
1227
|
// the empty spine, keyed for O(1) attachment below
|
|
1130
1228
|
const objectives = index.objectives.map((o) => ({
|
|
@@ -1138,7 +1236,7 @@ function surfaceSpine(root) {
|
|
|
1138
1236
|
const budgets = new Map(); // health:<id> groups — peers of objectives (D59), not a footnote
|
|
1139
1237
|
const unanchored = []; // R6 — never dropped to make the tree tidy
|
|
1140
1238
|
|
|
1141
|
-
for (const dir of
|
|
1239
|
+
for (const dir of initiativeDirs(root)) {
|
|
1142
1240
|
let files = [];
|
|
1143
1241
|
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); } catch { continue; }
|
|
1144
1242
|
for (const f of files) {
|