@cohortapp/agent-sdk 2.3.2 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/framework-features.json +30 -0
- package/lib/backlog.mjs +136 -0
- package/lib/cadences.mjs +63 -2
- package/lib/cadences.test.mjs +105 -0
- package/lib/capability/inventory.mjs +542 -0
- package/lib/capability/inventory.test.mjs +232 -0
- package/lib/capability/probe.mjs +255 -0
- package/lib/channels/contract.mjs +37 -1
- package/lib/channels/contract.test.mjs +25 -1
- package/lib/claude-bin.mjs +37 -3
- package/lib/claude-bin.test.mjs +42 -8
- package/lib/execution/disposition.mjs +501 -0
- package/lib/execution/disposition.test.mjs +482 -0
- package/lib/execution/drive.mjs +352 -0
- package/lib/execution/drive.test.mjs +270 -0
- package/lib/execution/effects.mjs +340 -0
- package/lib/execution/effects.test.mjs +193 -0
- package/lib/execution/index.mjs +152 -0
- package/lib/execution/intake.mjs +581 -0
- package/lib/execution/intake.test.mjs +343 -0
- package/lib/execution/journal.mjs +374 -0
- package/lib/execution/journal.test.mjs +261 -0
- package/lib/execution/match.mjs +331 -0
- package/lib/execution/match.test.mjs +235 -0
- package/lib/execution/pipeline.mjs +341 -0
- package/lib/execution/pipeline.test.mjs +389 -0
- package/lib/execution/route.mjs +332 -0
- package/lib/execution/route.test.mjs +186 -0
- package/lib/execution/surface-policy.mjs +446 -0
- package/lib/execution/surface-policy.test.mjs +162 -0
- package/lib/goals/admission.mjs +209 -0
- package/lib/goals/admission.test.mjs +139 -0
- package/lib/goals/classify.mjs +206 -0
- package/lib/goals/classify.test.mjs +109 -0
- package/lib/goals/collaborate.mjs +415 -0
- package/lib/goals/collaborate.test.mjs +324 -0
- package/lib/goals/gaps.mjs +111 -0
- package/lib/goals/gaps.test.mjs +284 -0
- package/lib/goals/loop.mjs +537 -0
- package/lib/goals/loop.test.mjs +719 -0
- package/lib/identity/persona.mjs +247 -0
- package/lib/identity/persona.test.mjs +117 -0
- package/lib/kpi.mjs +469 -0
- package/lib/kpi.test.mjs +244 -0
- package/lib/mandate/audit.mjs +168 -0
- package/lib/mandate/audit.test.mjs +195 -0
- package/lib/mandate/cache.mjs +162 -0
- package/lib/mandate/derive.mjs +317 -0
- package/lib/mandate/derive.test.mjs +224 -0
- package/lib/mandate/model.mjs +352 -0
- package/lib/mandate/model.test.mjs +145 -0
- package/lib/mandate/refresh.mjs +187 -0
- package/lib/mandate/refresh.test.mjs +293 -0
- package/lib/mcp/server.test.mjs +4 -4
- package/lib/org/approvals.mjs +14 -2
- package/lib/org/client.mjs +58 -22
- package/lib/org/client.test.mjs +3 -1
- package/lib/org/inbound/directedness.mjs +720 -0
- package/lib/org/inbound/directedness.test.mjs +543 -0
- package/lib/org/inbound/facts.mjs +501 -0
- package/lib/org/inbound/facts.test.mjs +375 -0
- package/lib/org/inbound/hydrate.mjs +535 -0
- package/lib/org/inbound/hydrate.test.mjs +326 -0
- package/lib/org/inbound/index.mjs +233 -0
- package/lib/org/inbound/index.test.mjs +324 -0
- package/lib/org/inbound/io.mjs +141 -0
- package/lib/org/inbound/project.mjs +201 -0
- package/lib/org/inbound/project.test.mjs +287 -0
- package/lib/org/inbound/surfaces.mjs +257 -0
- package/lib/org/knowledge.mjs +10 -1
- package/lib/org/knowledge.test.mjs +8 -1
- package/lib/org/leases.mjs +5 -0
- package/lib/org/mesh.mjs +17 -2
- package/lib/org/messaging.mjs +40 -4
- package/lib/org/messaging.test.mjs +40 -0
- package/lib/org/param-contract.mjs +694 -0
- package/lib/org/param-contract.test.mjs +451 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +8 -0
- package/lib/org/protocol.test.mjs +5 -1
- package/lib/org/push.mjs +1025 -0
- package/lib/org/push.test.mjs +690 -0
- package/lib/org/tool-surface.mjs +138 -38
- package/lib/org/tool-surface.test.mjs +13 -8
- package/lib/org/typing.mjs +341 -0
- package/lib/org/typing.test.mjs +291 -0
- package/lib/plan/compile.mjs +510 -0
- package/lib/plan/compile.test.mjs +286 -0
- package/lib/plan/emit.mjs +256 -0
- package/lib/plan/emit.test.mjs +246 -0
- package/lib/plan/explain.mjs +226 -0
- package/lib/plan/explain.test.mjs +188 -0
- package/lib/plan/schema.mjs +140 -0
- package/lib/resource-governor.mjs +47 -1
- package/lib/resource-governor.test.mjs +21 -1
- package/lib/setup/enroll-from-cohort.mjs +105 -17
- package/lib/setup/enroll-from-cohort.test.mjs +68 -1
- package/lib/setup/sections/identity.mjs +15 -4
- package/lib/setup/sections/identity.test.mjs +94 -0
- package/lib/setup/sections/inventory.mjs +178 -0
- package/lib/setup/sections/inventory.test.mjs +198 -0
- package/lib/setup/sections/mandate.mjs +392 -0
- package/lib/setup/sections/mandate.test.mjs +373 -0
- package/lib/setup/sections/subagents.mjs +427 -0
- package/lib/setup/sections/subagents.test.mjs +429 -0
- package/lib/setup/sections/verify.mjs +121 -0
- package/lib/setup/sections/verify.test.mjs +175 -0
- package/lib/setup/sot.mjs +2 -0
- package/lib/subagents/cli.mjs +463 -0
- package/lib/subagents/cli.test.mjs +389 -0
- package/lib/subagents/client.mjs +373 -0
- package/lib/subagents/client.test.mjs +309 -0
- package/lib/subagents/gap.mjs +268 -0
- package/lib/subagents/gap.test.mjs +234 -0
- package/lib/subagents/lock.mjs +296 -0
- package/lib/subagents/lock.test.mjs +248 -0
- package/lib/subagents/manifest.mjs +224 -0
- package/lib/subagents/manifest.test.mjs +175 -0
- package/lib/subagents/refs.mjs +274 -0
- package/lib/subagents/refs.test.mjs +204 -0
- package/lib/subagents/resolve.mjs +455 -0
- package/lib/subagents/resolve.test.mjs +422 -0
- package/lib/subagents/schema.mjs +467 -0
- package/lib/subagents/schema.test.mjs +306 -0
- package/package.json +8 -3
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
- package/policies/ai-disclosure.yaml +42 -2
- package/scaffold/CLAUDE.md +16 -2
- package/schedules/triggers/goal-steward.md +79 -0
- package/scripts/ci/conformance-org-api.mjs +792 -0
- package/scripts/ci/conformance-org-api.test.mjs +417 -0
- package/scripts/daemon/agent-daemon.mjs +36 -4
- package/scripts/daemon/cadence-handlers.mjs +145 -1
- package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
- package/scripts/daemon/inbox-deferral.mjs +45 -2
- package/scripts/daemon/inbox-deferral.test.mjs +56 -0
- package/scripts/daemon/inbox-wake.mjs +282 -0
- package/scripts/daemon/inbox-wake.test.mjs +199 -0
- package/scripts/daemon/prompt-builder.mjs +41 -1
- package/scripts/daemon/typing-registry.mjs +55 -2
- package/scripts/daemon/typing-registry.test.mjs +25 -0
- package/scripts/local-triggers/generate-plists.test.mjs +5 -5
- package/scripts/setup/gen-subagent-manifest.mjs +95 -0
- package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
- package/scripts/setup/generate-plan.mjs +108 -0
- package/scripts/setup/init-capability-manifest.mjs +70 -0
- package/scripts/setup/init-skill-marketplace.mjs +155 -0
- package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/goals/gaps.test.mjs — the read-only bridge between measured KPI series
|
|
3
|
+
* and the daemon's backlog ranking, plus the gap-weighted ranking itself.
|
|
4
|
+
*
|
|
5
|
+
* The invariant that matters most: **an agent with no mandate, a stale mandate,
|
|
6
|
+
* or no measurements must rank its backlog EXACTLY as it does today.** The gap
|
|
7
|
+
* term can promote goal-linked work; it must never silently reorder a fleet's
|
|
8
|
+
* seeded backlog because a cache went missing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
|
|
16
|
+
import { readLatestGaps } from "./gaps.mjs";
|
|
17
|
+
import {
|
|
18
|
+
scoreQueueItem, rankBacklog, resolveBacklogWeights,
|
|
19
|
+
DEFAULT_BACKLOG_WEIGHTS, PRIORITY_ORDER, parseQueueItems,
|
|
20
|
+
} from "../backlog.mjs";
|
|
21
|
+
|
|
22
|
+
const NOW = Date.parse("2026-08-11T09:00:00.000Z");
|
|
23
|
+
const SELF = "mem_self";
|
|
24
|
+
|
|
25
|
+
function record(over = {}) {
|
|
26
|
+
return {
|
|
27
|
+
version: 3,
|
|
28
|
+
source: "server",
|
|
29
|
+
fetchedAt: new Date(NOW - 3600_000).toISOString(),
|
|
30
|
+
body: {
|
|
31
|
+
memberId: SELF,
|
|
32
|
+
objectives: [
|
|
33
|
+
{ key: "revenue", kind: "PILLAR", state: "active", charterSectionId: "cs_1", adoptedById: "mem_boss" },
|
|
34
|
+
{
|
|
35
|
+
key: "pipeline", kind: "OBJECTIVE", parentKey: "revenue", state: "active",
|
|
36
|
+
metric: "pipeline_coverage_x", target: 4, direction: "up", tolerance: 0, cadence: "weekly",
|
|
37
|
+
sensor: { capability: "crm_list_deals", source: "method" }, adoptedById: "mem_boss",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
...over,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** An injected series reader: objectiveKey → rows. */
|
|
46
|
+
function seriesFrom(map) {
|
|
47
|
+
return (_root, key) => map[key] || [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// readLatestGaps
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
test("a measured, missed objective yields a normalized gap", () => {
|
|
55
|
+
const gaps = readLatestGaps("/tmp", {
|
|
56
|
+
cacheRecord: record(),
|
|
57
|
+
now: () => NOW,
|
|
58
|
+
readSeries: seriesFrom({
|
|
59
|
+
pipeline: [{ value: 1, window: "2026-W33", at: new Date(NOW - 3600_000).toISOString(), source: "method" }],
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
assert.ok(gaps.pipeline);
|
|
63
|
+
assert.equal(gaps.pipeline.value, 1);
|
|
64
|
+
assert.equal(gaps.pipeline.target, 4);
|
|
65
|
+
assert.equal(gaps.pipeline.gap, 3);
|
|
66
|
+
assert.equal(gaps.pipeline.normalizedGap, 0.75);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("NO MANDATE: an empty map, logged at info — the ranking degrades, it does not break", () => {
|
|
70
|
+
const logs = [];
|
|
71
|
+
const gaps = readLatestGaps("/tmp", { cacheRecord: null, now: () => NOW, log: (l, m) => logs.push(`${l}:${m}`) });
|
|
72
|
+
assert.deepEqual(gaps, {});
|
|
73
|
+
assert.ok(logs.some((l) => /no mandate cache/.test(l)), "the degradation must be visible");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("CORRUPT MANDATE: an empty map, logged at WARN", () => {
|
|
77
|
+
const logs = [];
|
|
78
|
+
const gaps = readLatestGaps("/tmp", {
|
|
79
|
+
cacheRecord: { corrupt: true, reason: "bad json", body: {} },
|
|
80
|
+
now: () => NOW, log: (l, m) => logs.push(`${l}:${m}`),
|
|
81
|
+
});
|
|
82
|
+
assert.deepEqual(gaps, {});
|
|
83
|
+
assert.ok(logs.some((l) => l.startsWith("warn:") && /corrupt/.test(l)));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("EXPIRED MANDATE (>7d): stops steering dispatch entirely", () => {
|
|
87
|
+
const logs = [];
|
|
88
|
+
const gaps = readLatestGaps("/tmp", {
|
|
89
|
+
cacheRecord: record({ fetchedAt: new Date(NOW - 8 * 24 * 3600 * 1000).toISOString() }),
|
|
90
|
+
now: () => NOW,
|
|
91
|
+
log: (l, m) => logs.push(`${l}:${m}`),
|
|
92
|
+
readSeries: seriesFrom({ pipeline: [{ value: 1, at: new Date(NOW).toISOString(), source: "method" }] }),
|
|
93
|
+
});
|
|
94
|
+
assert.deepEqual(gaps, {}, "ranking on a week-old target is worse than not ranking");
|
|
95
|
+
assert.ok(logs.some((l) => /expired/.test(l)));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("PROPOSED objectives never influence dispatch order", () => {
|
|
99
|
+
const rec = record();
|
|
100
|
+
rec.body.objectives[1].state = "proposed";
|
|
101
|
+
const gaps = readLatestGaps("/tmp", {
|
|
102
|
+
cacheRecord: rec, now: () => NOW,
|
|
103
|
+
readSeries: seriesFrom({ pipeline: [{ value: 1, at: new Date(NOW).toISOString(), source: "method" }] }),
|
|
104
|
+
});
|
|
105
|
+
assert.deepEqual(gaps, {});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a SELF-ADOPTED objective cannot float its own backlog", () => {
|
|
109
|
+
const rec = record();
|
|
110
|
+
rec.body.objectives[1].adoptedById = SELF;
|
|
111
|
+
const gaps = readLatestGaps("/tmp", {
|
|
112
|
+
cacheRecord: rec, now: () => NOW,
|
|
113
|
+
readSeries: seriesFrom({ pipeline: [{ value: 1, at: new Date(NOW).toISOString(), source: "method" }] }),
|
|
114
|
+
});
|
|
115
|
+
assert.deepEqual(gaps, {}, "a seat that grades itself must not also get dispatch priority for it");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a STALE SENSOR contributes zero urgency, not stale urgency", () => {
|
|
119
|
+
const gaps = readLatestGaps("/tmp", {
|
|
120
|
+
cacheRecord: record(), now: () => NOW,
|
|
121
|
+
readSeries: seriesFrom({
|
|
122
|
+
// Three weeks old against a weekly cadence.
|
|
123
|
+
pipeline: [{ value: 1, at: new Date(NOW - 21 * 24 * 3600 * 1000).toISOString(), source: "method" }],
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
assert.equal(gaps.pipeline.stale, true);
|
|
127
|
+
assert.equal(gaps.pipeline.normalizedGap, 0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("a series reader that throws is contained per objective", () => {
|
|
131
|
+
const logs = [];
|
|
132
|
+
const gaps = readLatestGaps("/tmp", {
|
|
133
|
+
cacheRecord: record(), now: () => NOW,
|
|
134
|
+
log: (l, m) => logs.push(`${l}:${m}`),
|
|
135
|
+
readSeries: () => { throw new Error("disk gone"); },
|
|
136
|
+
});
|
|
137
|
+
assert.deepEqual(gaps, {});
|
|
138
|
+
assert.ok(logs.some((l) => /could not compute the gap/.test(l)));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Weights
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
test("charter rewardWeights override the defaults, per key, ignoring junk", () => {
|
|
146
|
+
assert.deepEqual(resolveBacklogWeights(null), DEFAULT_BACKLOG_WEIGHTS);
|
|
147
|
+
assert.deepEqual(resolveBacklogWeights({ rewardWeights: { gap: 3 } }), { priority: 1, gap: 3, age: 0.25 });
|
|
148
|
+
// Junk must not poison the ranking.
|
|
149
|
+
assert.deepEqual(
|
|
150
|
+
resolveBacklogWeights({ rewardWeights: { gap: "lots", priority: -5, age: NaN } }),
|
|
151
|
+
DEFAULT_BACKLOG_WEIGHTS
|
|
152
|
+
);
|
|
153
|
+
assert.deepEqual(resolveBacklogWeights({ reward_weights: { age: 0 } }).age, 0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Scoring + ranking
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
test("with no gaps at all, ranking reproduces today's flat priority order exactly", () => {
|
|
161
|
+
const items = [
|
|
162
|
+
{ title: "n", priority: "normal" },
|
|
163
|
+
{ title: "c", priority: "critical" },
|
|
164
|
+
{ title: "l", priority: "low" },
|
|
165
|
+
{ title: "h", priority: "high" },
|
|
166
|
+
];
|
|
167
|
+
const ranked = rankBacklog(items, { now: NOW });
|
|
168
|
+
assert.deepEqual(ranked.map((r) => r.title), ["c", "h", "n", "l"]);
|
|
169
|
+
|
|
170
|
+
// …which is precisely the legacy sort.
|
|
171
|
+
const legacy = [...items].sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 3) - (PRIORITY_ORDER[b.priority] ?? 3));
|
|
172
|
+
assert.deepEqual(ranked.map((r) => r.title), legacy.map((r) => r.title));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("a badly-missed objective promotes its work above an equal-priority peer", () => {
|
|
176
|
+
const items = [
|
|
177
|
+
{ title: "unlinked", priority: "normal" },
|
|
178
|
+
{ title: "goal-linked", priority: "normal", advances: ["pipeline"], expected_delta: 1 },
|
|
179
|
+
];
|
|
180
|
+
const gapsByObjective = { pipeline: { normalizedGap: 0.75 } };
|
|
181
|
+
const ranked = rankBacklog(items, { gapsByObjective, now: NOW });
|
|
182
|
+
assert.equal(ranked[0].title, "goal-linked");
|
|
183
|
+
assert.ok(ranked[0]._scoreParts.gapTerm > 0);
|
|
184
|
+
assert.equal(ranked[1]._scoreParts.gapTerm, 0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("an unmeasured objective manufactures NO urgency", () => {
|
|
188
|
+
const items = [
|
|
189
|
+
{ title: "critical-unlinked", priority: "critical" },
|
|
190
|
+
{ title: "linked-but-unmeasured", priority: "low", advances: ["pipeline"], expected_delta: 1 },
|
|
191
|
+
];
|
|
192
|
+
// The objective exists but has never been measured.
|
|
193
|
+
const ranked = rankBacklog(items, { gapsByObjective: { pipeline: { normalizedGap: 0 } }, now: NOW });
|
|
194
|
+
assert.equal(ranked[0].title, "critical-unlinked");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a claimed delta with no measured gap, or a gap with no claimed delta, scores zero", () => {
|
|
198
|
+
const noDelta = scoreQueueItem({ priority: "normal", advances: ["pipeline"] }, { gapsByObjective: { pipeline: { normalizedGap: 0.9 } } });
|
|
199
|
+
assert.equal(noDelta.gapTerm, 0, "a gap with no expected delta is not evidence this item helps");
|
|
200
|
+
const noGap = scoreQueueItem({ priority: "normal", advances: ["pipeline"], expected_delta: 1 }, { gapsByObjective: {} });
|
|
201
|
+
assert.equal(noGap.gapTerm, 0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("the gap term cannot outrank a whole priority level on its own", () => {
|
|
205
|
+
// Worst possible gap and a full claimed delta.
|
|
206
|
+
const linked = scoreQueueItem(
|
|
207
|
+
{ priority: "normal", advances: ["pipeline"], expected_delta: 99 },
|
|
208
|
+
{ gapsByObjective: { pipeline: { normalizedGap: 99 } }, weights: DEFAULT_BACKLOG_WEIGHTS }
|
|
209
|
+
);
|
|
210
|
+
const critical = scoreQueueItem({ priority: "critical" }, { weights: DEFAULT_BACKLOG_WEIGHTS });
|
|
211
|
+
// normal(1) + gap(≤1) == 2 < critical(3). A KPI gap escalates work; it does
|
|
212
|
+
// not let the agent redefine what "critical" means.
|
|
213
|
+
assert.ok(linked.score <= critical.score, `${linked.score} must not exceed critical ${critical.score}`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("the age penalty stops a permanently-lagging KPI starving everything else", () => {
|
|
217
|
+
const gapsByObjective = { pipeline: { normalizedGap: 0.5 } };
|
|
218
|
+
const fresh = scoreQueueItem(
|
|
219
|
+
{ priority: "normal", advances: ["pipeline"], expected_delta: 1, created: new Date(NOW).toISOString() },
|
|
220
|
+
{ gapsByObjective, now: NOW }
|
|
221
|
+
);
|
|
222
|
+
const old = scoreQueueItem(
|
|
223
|
+
{ priority: "normal", advances: ["pipeline"], expected_delta: 1, created: "2026-01-01" },
|
|
224
|
+
{ gapsByObjective, now: NOW }
|
|
225
|
+
);
|
|
226
|
+
assert.ok(old.agePenalty > fresh.agePenalty);
|
|
227
|
+
assert.ok(old.score < fresh.score);
|
|
228
|
+
// …but bounded, so it can never invert a whole priority level either.
|
|
229
|
+
assert.ok(old.agePenalty <= 2 * DEFAULT_BACKLOG_WEIGHTS.age);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("ranking is stable and total — equal scores keep their input order", () => {
|
|
233
|
+
const items = [
|
|
234
|
+
{ title: "a", priority: "normal" },
|
|
235
|
+
{ title: "b", priority: "normal" },
|
|
236
|
+
{ title: "c", priority: "normal" },
|
|
237
|
+
];
|
|
238
|
+
assert.deepEqual(rankBacklog(items, { now: NOW }).map((r) => r.title), ["a", "b", "c"]);
|
|
239
|
+
// And it is a pure function of its inputs.
|
|
240
|
+
assert.deepEqual(rankBacklog(items, { now: NOW }), rankBacklog(items, { now: NOW }));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("rankBacklog never mutates its input", () => {
|
|
244
|
+
const items = [{ title: "a", priority: "low" }, { title: "b", priority: "critical" }];
|
|
245
|
+
const copy = JSON.parse(JSON.stringify(items));
|
|
246
|
+
rankBacklog(items, { now: NOW });
|
|
247
|
+
assert.deepEqual(items, copy);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("junk in, no crash out", () => {
|
|
251
|
+
assert.deepEqual(rankBacklog(null), []);
|
|
252
|
+
assert.deepEqual(rankBacklog(undefined, {}), []);
|
|
253
|
+
const s = scoreQueueItem({}, {});
|
|
254
|
+
assert.ok(Number.isFinite(s.score));
|
|
255
|
+
const weird = scoreQueueItem({ priority: "nonsense", advances: "not-an-array" }, {});
|
|
256
|
+
assert.ok(Number.isFinite(weird.score));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("end to end: a rendered goals.yaml parses and ranks by its measured gap", () => {
|
|
260
|
+
const yaml = [
|
|
261
|
+
"queue: goals",
|
|
262
|
+
"items:",
|
|
263
|
+
' - id: seeded-1',
|
|
264
|
+
' title: "Seeded archetype work"',
|
|
265
|
+
" status: open",
|
|
266
|
+
" priority: normal",
|
|
267
|
+
' next_action: "Do the seeded thing"',
|
|
268
|
+
" created: 2026-08-11",
|
|
269
|
+
' - id: goal-1',
|
|
270
|
+
' title: "Close the pipeline gap"',
|
|
271
|
+
" status: open",
|
|
272
|
+
" priority: normal",
|
|
273
|
+
' next_action: "Book calls"',
|
|
274
|
+
" advances: [pipeline]",
|
|
275
|
+
" expected_delta: 1.5",
|
|
276
|
+
" obligation_key: outcome.pipeline",
|
|
277
|
+
" disposition: SELF",
|
|
278
|
+
" created: 2026-08-11",
|
|
279
|
+
].join("\n");
|
|
280
|
+
const parsed = parseQueueItems(yaml, "goals.yaml");
|
|
281
|
+
assert.equal(parsed.length, 2);
|
|
282
|
+
const ranked = rankBacklog(parsed, { gapsByObjective: { pipeline: { normalizedGap: 0.75 } }, now: NOW });
|
|
283
|
+
assert.equal(ranked[0].id, "goal-1", "the measured gap must win between equal priorities");
|
|
284
|
+
});
|