@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.
Files changed (148) hide show
  1. package/framework-features.json +30 -0
  2. package/lib/backlog.mjs +136 -0
  3. package/lib/cadences.mjs +63 -2
  4. package/lib/cadences.test.mjs +105 -0
  5. package/lib/capability/inventory.mjs +542 -0
  6. package/lib/capability/inventory.test.mjs +232 -0
  7. package/lib/capability/probe.mjs +255 -0
  8. package/lib/channels/contract.mjs +37 -1
  9. package/lib/channels/contract.test.mjs +25 -1
  10. package/lib/claude-bin.mjs +37 -3
  11. package/lib/claude-bin.test.mjs +42 -8
  12. package/lib/execution/disposition.mjs +501 -0
  13. package/lib/execution/disposition.test.mjs +482 -0
  14. package/lib/execution/drive.mjs +352 -0
  15. package/lib/execution/drive.test.mjs +270 -0
  16. package/lib/execution/effects.mjs +340 -0
  17. package/lib/execution/effects.test.mjs +193 -0
  18. package/lib/execution/index.mjs +152 -0
  19. package/lib/execution/intake.mjs +581 -0
  20. package/lib/execution/intake.test.mjs +343 -0
  21. package/lib/execution/journal.mjs +374 -0
  22. package/lib/execution/journal.test.mjs +261 -0
  23. package/lib/execution/match.mjs +331 -0
  24. package/lib/execution/match.test.mjs +235 -0
  25. package/lib/execution/pipeline.mjs +341 -0
  26. package/lib/execution/pipeline.test.mjs +389 -0
  27. package/lib/execution/route.mjs +332 -0
  28. package/lib/execution/route.test.mjs +186 -0
  29. package/lib/execution/surface-policy.mjs +446 -0
  30. package/lib/execution/surface-policy.test.mjs +162 -0
  31. package/lib/goals/admission.mjs +209 -0
  32. package/lib/goals/admission.test.mjs +139 -0
  33. package/lib/goals/classify.mjs +206 -0
  34. package/lib/goals/classify.test.mjs +109 -0
  35. package/lib/goals/collaborate.mjs +415 -0
  36. package/lib/goals/collaborate.test.mjs +324 -0
  37. package/lib/goals/gaps.mjs +111 -0
  38. package/lib/goals/gaps.test.mjs +284 -0
  39. package/lib/goals/loop.mjs +537 -0
  40. package/lib/goals/loop.test.mjs +719 -0
  41. package/lib/identity/persona.mjs +247 -0
  42. package/lib/identity/persona.test.mjs +117 -0
  43. package/lib/kpi.mjs +469 -0
  44. package/lib/kpi.test.mjs +244 -0
  45. package/lib/mandate/audit.mjs +168 -0
  46. package/lib/mandate/audit.test.mjs +195 -0
  47. package/lib/mandate/cache.mjs +162 -0
  48. package/lib/mandate/derive.mjs +317 -0
  49. package/lib/mandate/derive.test.mjs +224 -0
  50. package/lib/mandate/model.mjs +352 -0
  51. package/lib/mandate/model.test.mjs +145 -0
  52. package/lib/mandate/refresh.mjs +187 -0
  53. package/lib/mandate/refresh.test.mjs +293 -0
  54. package/lib/mcp/server.test.mjs +4 -4
  55. package/lib/org/approvals.mjs +14 -2
  56. package/lib/org/client.mjs +58 -22
  57. package/lib/org/client.test.mjs +3 -1
  58. package/lib/org/inbound/directedness.mjs +720 -0
  59. package/lib/org/inbound/directedness.test.mjs +543 -0
  60. package/lib/org/inbound/facts.mjs +501 -0
  61. package/lib/org/inbound/facts.test.mjs +375 -0
  62. package/lib/org/inbound/hydrate.mjs +535 -0
  63. package/lib/org/inbound/hydrate.test.mjs +326 -0
  64. package/lib/org/inbound/index.mjs +233 -0
  65. package/lib/org/inbound/index.test.mjs +324 -0
  66. package/lib/org/inbound/io.mjs +141 -0
  67. package/lib/org/inbound/project.mjs +201 -0
  68. package/lib/org/inbound/project.test.mjs +287 -0
  69. package/lib/org/inbound/surfaces.mjs +257 -0
  70. package/lib/org/knowledge.mjs +10 -1
  71. package/lib/org/knowledge.test.mjs +8 -1
  72. package/lib/org/leases.mjs +5 -0
  73. package/lib/org/mesh.mjs +17 -2
  74. package/lib/org/messaging.mjs +40 -4
  75. package/lib/org/messaging.test.mjs +40 -0
  76. package/lib/org/param-contract.mjs +694 -0
  77. package/lib/org/param-contract.test.mjs +451 -0
  78. package/lib/org/protocol.checksum +1 -1
  79. package/lib/org/protocol.mjs +8 -0
  80. package/lib/org/protocol.test.mjs +5 -1
  81. package/lib/org/push.mjs +1025 -0
  82. package/lib/org/push.test.mjs +690 -0
  83. package/lib/org/tool-surface.mjs +138 -38
  84. package/lib/org/tool-surface.test.mjs +13 -8
  85. package/lib/org/typing.mjs +341 -0
  86. package/lib/org/typing.test.mjs +291 -0
  87. package/lib/plan/compile.mjs +510 -0
  88. package/lib/plan/compile.test.mjs +286 -0
  89. package/lib/plan/emit.mjs +256 -0
  90. package/lib/plan/emit.test.mjs +246 -0
  91. package/lib/plan/explain.mjs +226 -0
  92. package/lib/plan/explain.test.mjs +188 -0
  93. package/lib/plan/schema.mjs +140 -0
  94. package/lib/resource-governor.mjs +47 -1
  95. package/lib/resource-governor.test.mjs +21 -1
  96. package/lib/setup/enroll-from-cohort.mjs +105 -17
  97. package/lib/setup/enroll-from-cohort.test.mjs +68 -1
  98. package/lib/setup/sections/identity.mjs +15 -4
  99. package/lib/setup/sections/identity.test.mjs +94 -0
  100. package/lib/setup/sections/inventory.mjs +178 -0
  101. package/lib/setup/sections/inventory.test.mjs +198 -0
  102. package/lib/setup/sections/mandate.mjs +392 -0
  103. package/lib/setup/sections/mandate.test.mjs +373 -0
  104. package/lib/setup/sections/subagents.mjs +427 -0
  105. package/lib/setup/sections/subagents.test.mjs +429 -0
  106. package/lib/setup/sections/verify.mjs +121 -0
  107. package/lib/setup/sections/verify.test.mjs +175 -0
  108. package/lib/setup/sot.mjs +2 -0
  109. package/lib/subagents/cli.mjs +463 -0
  110. package/lib/subagents/cli.test.mjs +389 -0
  111. package/lib/subagents/client.mjs +373 -0
  112. package/lib/subagents/client.test.mjs +309 -0
  113. package/lib/subagents/gap.mjs +268 -0
  114. package/lib/subagents/gap.test.mjs +234 -0
  115. package/lib/subagents/lock.mjs +296 -0
  116. package/lib/subagents/lock.test.mjs +248 -0
  117. package/lib/subagents/manifest.mjs +224 -0
  118. package/lib/subagents/manifest.test.mjs +175 -0
  119. package/lib/subagents/refs.mjs +274 -0
  120. package/lib/subagents/refs.test.mjs +204 -0
  121. package/lib/subagents/resolve.mjs +455 -0
  122. package/lib/subagents/resolve.test.mjs +422 -0
  123. package/lib/subagents/schema.mjs +467 -0
  124. package/lib/subagents/schema.test.mjs +306 -0
  125. package/package.json +8 -3
  126. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  127. package/policies/ai-disclosure.yaml +42 -2
  128. package/scaffold/CLAUDE.md +16 -2
  129. package/schedules/triggers/goal-steward.md +79 -0
  130. package/scripts/ci/conformance-org-api.mjs +792 -0
  131. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  132. package/scripts/daemon/agent-daemon.mjs +36 -4
  133. package/scripts/daemon/cadence-handlers.mjs +145 -1
  134. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  135. package/scripts/daemon/inbox-deferral.mjs +45 -2
  136. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  137. package/scripts/daemon/inbox-wake.mjs +282 -0
  138. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  139. package/scripts/daemon/prompt-builder.mjs +41 -1
  140. package/scripts/daemon/typing-registry.mjs +55 -2
  141. package/scripts/daemon/typing-registry.test.mjs +25 -0
  142. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  143. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  144. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  145. package/scripts/setup/generate-plan.mjs +108 -0
  146. package/scripts/setup/init-capability-manifest.mjs +70 -0
  147. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  148. package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * lib/mandate/derive.test.mjs — the pillars model, derived at init from the
3
+ * charter and the role.
4
+ *
5
+ * This is the step that makes the inert PILLAR sections in an agent's
6
+ * `config/agent.json` load-bearing: charter clause → PILLAR → OBJECTIVE → GOAL,
7
+ * with a sensor bound to a capability the manifest says is actually REACHABLE.
8
+ *
9
+ * The negative cases matter more than the happy path here: an objective whose
10
+ * sensor cannot be reached must be proposed as a HUMAN measurement obligation
11
+ * with a recorded degradation, never bound to a capability that does not exist.
12
+ */
13
+
14
+ "use strict";
15
+
16
+ import { test } from "node:test";
17
+ import assert from "node:assert/strict";
18
+
19
+ import { deriveMandate, toMandateBody, pickSensor, slug, firstClause } from "./derive.mjs";
20
+ import { validateMandateBody, pillars, ancestry, provenanceOf } from "./model.mjs";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Fixtures
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const CHARTER = [
27
+ { id: "cs_1", kind: "PILLAR", order: 1, title: "Revenue growth", body: "Own the revenue line end to end." },
28
+ { id: "cs_2", kind: "PILLAR", order: 2, title: "Customer retention", body: "Keep the customers we win." },
29
+ { id: "cs_9", kind: "PRINCIPLE", order: 9, title: "Be direct", body: "Say the hard thing early." },
30
+ ];
31
+
32
+ /** A manifest where only the CRM read is actually reachable. */
33
+ const ENTRIES = [
34
+ { id: "crm_list_deals", plane: "org", reachable: true },
35
+ { id: "crm_list_contacts", plane: "org", reachable: false },
36
+ ];
37
+ /** The shape `lib/capability/inventory.buildInventory` actually returns. */
38
+ const MANIFEST = { version: 1, entries: ENTRIES };
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Pure helpers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ test("slug and firstClause are deterministic and bounded", () => {
45
+ assert.equal(slug("Revenue Growth!"), "revenue-growth");
46
+ assert.equal(slug(" Multiple spaces "), "multiple-spaces");
47
+ assert.equal(firstClause("Own the revenue line. And then some more."), "Own the revenue line.");
48
+ assert.ok(firstClause("x".repeat(500)).length <= 120);
49
+ });
50
+
51
+ test("pickSensor binds only to a REACHABLE capability", () => {
52
+ const reachable = new Set(["crm_list_deals"]);
53
+ const bound = pickSensor("pipeline coverage across open deals", reachable);
54
+ if (bound.capability) {
55
+ assert.equal(bound.source, "method");
56
+ assert.ok(reachable.has(bound.capability), "a bound sensor must name a reachable capability");
57
+ }
58
+
59
+ // Same text, nothing reachable: the family matched but no capability can be
60
+ // cited — that is a measurement DEBT, and it must be visible.
61
+ const unbound = pickSensor("pipeline coverage across open deals", new Set());
62
+ assert.equal(unbound.capability, null);
63
+ assert.equal(unbound.source, "human");
64
+ });
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // The derivation
68
+ // ---------------------------------------------------------------------------
69
+
70
+ test("charter PILLAR sections become PILLAR nodes carrying their charterSectionId", () => {
71
+ const r = deriveMandate({ charterSections: CHARTER, kpiCategories: [{ id: "pipeline", name: "Pipeline" }] });
72
+ const ps = pillars(r.objectives);
73
+ assert.equal(ps.length, 2, "only PILLAR/CAPABILITY sections become pillars — a PRINCIPLE is not an outcome");
74
+ assert.deepEqual(ps.map((p) => p.charterSectionId), ["cs_1", "cs_2"]);
75
+ assert.deepEqual(ps.map((p) => p.text), ["Revenue growth", "Customer retention"]);
76
+ for (const p of ps) {
77
+ assert.equal(p.parentKey, null);
78
+ assert.equal(p.state, "proposed", "nothing is adopted by derivation — a human adopts");
79
+ assert.equal(p.source.origin, "charter");
80
+ }
81
+ });
82
+
83
+ test("EVERYTHING derived is proposed — derivation can never adopt", () => {
84
+ const r = deriveMandate({
85
+ charterSections: CHARTER,
86
+ kpiCategories: [{ id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
87
+ priorities: [{ id: "p1", title: "Land three lighthouse accounts" }],
88
+ manifest: MANIFEST,
89
+ });
90
+ assert.ok(r.objectives.length >= 4);
91
+ for (const o of r.objectives) {
92
+ assert.equal(o.state, "proposed", `"${o.key}" must be proposed, not ${o.state}`);
93
+ assert.ok(!o.adoptedById, "derivation must never stamp an adopter");
94
+ }
95
+ // And therefore nothing here can drive work: adoptedObjectives is empty.
96
+ const validation = validateMandateBody(r.objectives, { ownerMemberId: "mem_self" });
97
+ assert.equal(validation.errors.length, 0, `derived tree must be structurally valid: ${validation.errors.join("; ")}`);
98
+ });
99
+
100
+ test("the derived tree is structurally valid: every objective hangs off a pillar", () => {
101
+ const r = deriveMandate({
102
+ charterSections: CHARTER,
103
+ kpiCategories: [
104
+ { id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] },
105
+ { id: "churn", name: "Customer retention rate", examples: ["logo_retention_pct"] },
106
+ ],
107
+ manifest: MANIFEST,
108
+ });
109
+ const validation = validateMandateBody(r.objectives);
110
+ assert.equal(validation.ok, true, validation.errors.join("; "));
111
+
112
+ for (const o of r.objectives.filter((x) => x.kind !== "PILLAR")) {
113
+ assert.ok(o.parentKey, `"${o.key}" must have a parent`);
114
+ const chain = ancestry(r.objectives, o.key);
115
+ assert.ok(
116
+ chain.some((n) => n.kind === "PILLAR"),
117
+ `"${o.key}" must trace to a PILLAR — provenance is the whole point`
118
+ );
119
+ }
120
+ });
121
+
122
+ test("provenance walks objective → pillar → charter clause, offline", () => {
123
+ const r = deriveMandate({
124
+ charterSections: CHARTER,
125
+ kpiCategories: [{ id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
126
+ manifest: MANIFEST,
127
+ });
128
+ const obj = r.objectives.find((o) => o.kind === "OBJECTIVE");
129
+ const prov = provenanceOf(r.objectives, obj.key);
130
+ assert.ok(prov.pillar, "an objective must resolve to a pillar");
131
+ assert.match(prov.clause, /^cs_/, "and the pillar must carry the charter clause id");
132
+ assert.ok(prov.chain.includes(obj.key));
133
+ });
134
+
135
+ test("an objective with no reachable sensor is a recorded DEGRADATION, not a silent human task", () => {
136
+ const r = deriveMandate({
137
+ charterSections: CHARTER,
138
+ // A KPI category nothing in the manifest can measure.
139
+ kpiCategories: [{ id: "brand-sentiment", name: "Brand sentiment", examples: ["nps_score"] }],
140
+ manifest: MANIFEST,
141
+ });
142
+ const obj = r.objectives.find((o) => o.key === "brand-sentiment");
143
+ assert.ok(obj);
144
+ assert.notEqual(obj.sensor && obj.sensor.source, "method");
145
+ assert.ok(
146
+ r.degradations.some((d) => d.kind === "no_reachable_sensor" && d.key === "brand-sentiment"),
147
+ `expected a no_reachable_sensor degradation; got ${JSON.stringify(r.degradations)}`
148
+ );
149
+ });
150
+
151
+ test("an empty charter is reported, not silently accepted", () => {
152
+ const r = deriveMandate({ charterSections: [], kpiCategories: [] });
153
+ assert.ok(r.degradations.some((d) => d.kind === "no_charter_pillars"));
154
+ assert.ok(r.degradations.some((d) => d.kind === "no_kpi_categories"));
155
+ assert.equal(r.objectives.length, 0);
156
+ });
157
+
158
+ test("derivation is deterministic — same inputs, byte-identical tree", () => {
159
+ const input = {
160
+ charterSections: CHARTER,
161
+ kpiCategories: [{ id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
162
+ priorities: [{ id: "p1", title: "Land three lighthouse accounts" }],
163
+ manifest: MANIFEST,
164
+ };
165
+ assert.equal(JSON.stringify(deriveMandate(input)), JSON.stringify(deriveMandate(input)));
166
+ });
167
+
168
+ test("duplicate titles never collide into one key", () => {
169
+ const r = deriveMandate({
170
+ charterSections: [
171
+ { id: "cs_1", kind: "PILLAR", order: 1, title: "Growth", body: "Growth" },
172
+ { id: "cs_2", kind: "PILLAR", order: 2, title: "Growth", body: "Growth again" },
173
+ ],
174
+ });
175
+ const keys = r.objectives.map((o) => o.key);
176
+ assert.equal(new Set(keys).size, keys.length, `keys must be unique; got ${keys.join(",")}`);
177
+ });
178
+
179
+ test("overrides let a human set the real target without re-deriving", () => {
180
+ const r = deriveMandate({
181
+ charterSections: CHARTER,
182
+ kpiCategories: [{ id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
183
+ manifest: MANIFEST,
184
+ overrides: { pipeline: { target: 3.5, tolerance: 0.2, cadence: "monthly", direction: "up" } },
185
+ });
186
+ const obj = r.objectives.find((o) => o.key === "pipeline");
187
+ assert.equal(obj.target, 3.5);
188
+ assert.equal(obj.tolerance, 0.2);
189
+ assert.equal(obj.cadence, "monthly");
190
+ });
191
+
192
+ test("toMandateBody produces the shape the cache and compiler consume", () => {
193
+ const r = deriveMandate({ charterSections: CHARTER, kpiCategories: [{ id: "pipeline", name: "Pipeline" }] });
194
+ const body = toMandateBody({ memberId: "mem_self", objectives: r.objectives });
195
+ assert.equal(body.memberId, "mem_self");
196
+ assert.ok(Array.isArray(body.objectives));
197
+ assert.equal(typeof body.budgetCentsPerPeriod, "number");
198
+ assert.ok(Array.isArray(body.reactsTo));
199
+ // And it must round-trip through the validator the loop uses.
200
+ assert.equal(validateMandateBody(body).ok, true);
201
+ });
202
+
203
+ test("a bare ENTRY ARRAY manifest is tolerated, not a crash", () => {
204
+ // `[].entries` is Array.prototype.entries — a function. Walking it naively
205
+ // threw "function is not iterable" and took the whole derivation with it.
206
+ const fromObject = deriveMandate({
207
+ charterSections: CHARTER,
208
+ kpiCategories: [{ id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
209
+ manifest: MANIFEST,
210
+ });
211
+ const fromArray = deriveMandate({
212
+ charterSections: CHARTER,
213
+ kpiCategories: [{ id: "pipeline", name: "Pipeline coverage", examples: ["pipeline_coverage_x"] }],
214
+ manifest: ENTRIES,
215
+ });
216
+ assert.deepEqual(fromArray.objectives, fromObject.objectives);
217
+ });
218
+
219
+ test("toMandateBody tolerates junk without throwing", () => {
220
+ const body = toMandateBody({});
221
+ assert.equal(body.memberId, null);
222
+ assert.deepEqual(body.objectives, []);
223
+ assert.equal(validateMandateBody(body).ok, true);
224
+ });
@@ -0,0 +1,352 @@
1
+ /**
2
+ * lib/mandate/model.mjs — the operating-pillar spine, as data: validation,
3
+ * tree navigation, and the anti-Goodhart law.
4
+ *
5
+ * The spine is ONE self-referencing tree (the shape `lib/mandate/derive.mjs`
6
+ * emits and `lib/mandate/cache.mjs` persists as `body.objectives`):
7
+ *
8
+ * PILLAR a standing area of accountability — a charter clause / control
9
+ * tower. Carries `charterSectionId`, so everything under it is
10
+ * provenance-linked to the org's own words.
11
+ * OBJECTIVE what we are moving inside that pillar this period.
12
+ * GOAL the measurable, dated commitment — the KPI itself.
13
+ *
14
+ * This module deliberately does NOT derive, fetch or cache anything (that is
15
+ * derive.mjs / cache.mjs). It answers the three questions the goal loop needs:
16
+ *
17
+ * 1. is this tree structurally sound? `validateMandateBody`
18
+ * 2. which nodes may legally drive work? `adoptedObjectives`
19
+ * 3. why is this node here, and who said so? `ancestry` / `provenanceOf`
20
+ *
21
+ * **The anti-Goodhart law lives here.** An agent may PROPOSE an objective and
22
+ * may REPORT a sample, but a node whose `adoptedById` is its own owner is
23
+ * excluded from `adoptedObjectives` and reported as an error: a seat cannot
24
+ * define, measure and be graded on the same number. hq enforces the same rule
25
+ * server-side in `mandate.adopt`; this is the client-side mirror so an offline
26
+ * agent cannot quietly grade itself either.
27
+ *
28
+ * PURE: no fs, no network, no clock. ESM.
29
+ *
30
+ * @module lib/mandate/model
31
+ */
32
+
33
+ "use strict";
34
+
35
+ /** The three tiers of the spine, outermost first. */
36
+ export const KINDS = Object.freeze(["PILLAR", "OBJECTIVE", "GOAL"]);
37
+
38
+ /** Lifecycle. Only `active` may drive work; everything else is inert. */
39
+ export const STATES = Object.freeze(["proposed", "active", "at_risk", "met", "missed", "retired"]);
40
+
41
+ /** Which way "good" points for a metric. */
42
+ export const DIRECTIONS = Object.freeze(["up", "down", "hold", "band"]);
43
+
44
+ /** Collaborator roles. Each routes to an EXISTING org primitive (see goals/collaborate.mjs). */
45
+ export const ROLES = Object.freeze(["DOER", "CONTRIBUTOR", "REVIEWER", "APPROVER", "INFORMED"]);
46
+
47
+ /** Where a KPI number came from. `llm` is advisory and can never create work. */
48
+ export const SENSOR_SOURCES = Object.freeze(["method", "human", "llm"]);
49
+
50
+ /** Kinds that carry a number and can therefore be measured. */
51
+ export const MEASURABLE_KINDS = Object.freeze(["OBJECTIVE", "GOAL"]);
52
+
53
+ /** Which parents each kind may legally hang from (`null` = may be a root). */
54
+ const LEGAL_PARENTS = Object.freeze({
55
+ PILLAR: [null],
56
+ OBJECTIVE: ["PILLAR", null],
57
+ GOAL: ["OBJECTIVE", "PILLAR"],
58
+ });
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Accessors — tolerate every shape the mandate arrives in
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Pull the objective list out of whatever the caller had to hand: a cache
66
+ * record (`{body:{objectives}}`), a bare body (`{objectives}`), or an array.
67
+ * Never throws; an unrecognised shape yields `[]`.
68
+ * @param {object|object[]} source
69
+ * @returns {object[]}
70
+ */
71
+ export function objectivesOf(source) {
72
+ if (Array.isArray(source)) return source.filter(Boolean);
73
+ if (!source || typeof source !== "object") return [];
74
+ if (Array.isArray(source.objectives)) return source.objectives.filter(Boolean);
75
+ if (source.body && Array.isArray(source.body.objectives)) return source.body.objectives.filter(Boolean);
76
+ return [];
77
+ }
78
+
79
+ /** The seat the mandate belongs to, from a record or a body. */
80
+ export function memberIdOf(source) {
81
+ if (!source || typeof source !== "object") return null;
82
+ return source.memberId || (source.body && source.body.memberId) || null;
83
+ }
84
+
85
+ /** Index objectives by key. @returns {Map<string,object>} */
86
+ export function indexByKey(objectives) {
87
+ const m = new Map();
88
+ for (const o of objectivesOf(objectives)) if (o && o.key) m.set(o.key, o);
89
+ return m;
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Validation — fail-closed, mirrors lib/archetype.validateProfile
94
+ // ---------------------------------------------------------------------------
95
+
96
+ /**
97
+ * Validate the mandate tree. Returns EVERY problem found and never throws: a
98
+ * partly-broken mandate must degrade to "the valid subset drives work", not to
99
+ * a crashed daemon. The loop refuses to admit work against any key in
100
+ * `badKeys`.
101
+ *
102
+ * @param {object|object[]} source cache record, body, or objective array
103
+ * @param {object} [opts] @param {string} [opts.ownerMemberId] the seat itself
104
+ * @returns {{ok:boolean, errors:string[], warnings:string[], badKeys:string[]}}
105
+ */
106
+ export function validateMandateBody(source, opts = {}) {
107
+ const list = objectivesOf(source);
108
+ const owner = opts.ownerMemberId || memberIdOf(source) || null;
109
+ const errors = [];
110
+ const warnings = [];
111
+ const badKeys = new Set();
112
+
113
+ const byKey = new Map();
114
+ for (const o of list) {
115
+ if (!o.key) {
116
+ errors.push("objective with no key");
117
+ continue;
118
+ }
119
+ if (byKey.has(o.key)) {
120
+ errors.push(`duplicate key "${o.key}"`);
121
+ badKeys.add(o.key);
122
+ continue;
123
+ }
124
+ byKey.set(o.key, o);
125
+ }
126
+
127
+ for (const o of list) {
128
+ if (!o.key) continue;
129
+ const at = `"${o.key}"`;
130
+ const kind = String(o.kind || "").toUpperCase();
131
+
132
+ if (!KINDS.includes(kind)) {
133
+ errors.push(`${at}: unknown kind "${o.kind}"`);
134
+ badKeys.add(o.key);
135
+ }
136
+ if (!STATES.includes(String(o.state))) {
137
+ errors.push(`${at}: unknown state "${o.state}"`);
138
+ badKeys.add(o.key);
139
+ }
140
+ if (o.direction != null && !DIRECTIONS.includes(String(o.direction))) {
141
+ errors.push(`${at}: unknown direction "${o.direction}"`);
142
+ badKeys.add(o.key);
143
+ }
144
+
145
+ // Parentage.
146
+ const legal = LEGAL_PARENTS[kind] || [];
147
+ if (o.parentKey) {
148
+ const parent = byKey.get(o.parentKey);
149
+ if (!parent) {
150
+ errors.push(`${at}: parent "${o.parentKey}" not found`);
151
+ badKeys.add(o.key);
152
+ } else if (!legal.includes(String(parent.kind || "").toUpperCase())) {
153
+ errors.push(`${at}: a ${kind} may not hang from a ${parent.kind}`);
154
+ badKeys.add(o.key);
155
+ }
156
+ } else if (!legal.includes(null)) {
157
+ errors.push(`${at}: a ${kind} requires a parent (${legal.filter(Boolean).join("|")})`);
158
+ badKeys.add(o.key);
159
+ }
160
+
161
+ // Measurability of an ACTIVE node.
162
+ if (MEASURABLE_KINDS.includes(kind) && o.state === "active") {
163
+ if (!o.metric) {
164
+ errors.push(`${at}: active ${kind} has no metric`);
165
+ badKeys.add(o.key);
166
+ }
167
+ // `Number(null)` is 0, so a null target would otherwise read as a legal
168
+ // target of zero — the exact class of silent coercion this codebase keeps
169
+ // getting bitten by. Require the value to be present AND finite.
170
+ if (o.target == null || o.target === "" || !Number.isFinite(Number(o.target))) {
171
+ errors.push(`${at}: active ${kind} has no numeric target`);
172
+ badKeys.add(o.key);
173
+ }
174
+ if (!o.cadence) warnings.push(`${at}: active ${kind} has no measure cadence — it will never come due`);
175
+ if (!o.sensor || !o.sensor.capability) {
176
+ warnings.push(`${at}: active ${kind} has no method sensor — measurement is a human obligation`);
177
+ } else if (!SENSOR_SOURCES.includes(String(o.sensor.source))) {
178
+ errors.push(`${at}: unknown sensor source "${o.sensor.source}"`);
179
+ badKeys.add(o.key);
180
+ }
181
+ }
182
+
183
+ // Adoption. THE anti-Goodhart law.
184
+ if (o.state === "active") {
185
+ if (o.adoptedById && owner && o.adoptedById === owner) {
186
+ errors.push(
187
+ `${at}: self-adopted — adoptedById equals the owning seat (${owner}). A seat cannot define, measure and be graded on the same number.`
188
+ );
189
+ badKeys.add(o.key);
190
+ } else if (!o.adoptedById) {
191
+ warnings.push(`${at}: active but the snapshot carries no adoptedById — adoption is unverifiable locally`);
192
+ }
193
+ }
194
+
195
+ for (const c of collaboratorsOf(o)) {
196
+ if (!ROLES.includes(c.role)) warnings.push(`${at}: unknown collaborator role "${c.role}"`);
197
+ }
198
+ }
199
+
200
+ // Cycles.
201
+ for (const o of list) {
202
+ if (!o.key) continue;
203
+ const walked = new Set();
204
+ let cur = o;
205
+ while (cur && cur.parentKey) {
206
+ if (walked.has(cur.key)) {
207
+ errors.push(`cycle in the objective tree at "${o.key}"`);
208
+ badKeys.add(o.key);
209
+ break;
210
+ }
211
+ walked.add(cur.key);
212
+ cur = byKey.get(cur.parentKey);
213
+ }
214
+ }
215
+
216
+ return { ok: errors.length === 0, errors, warnings, badKeys: [...badKeys] };
217
+ }
218
+
219
+ /** Normalise an objective's collaborators to `[{memberId, role}]`. Pure. */
220
+ export function collaboratorsOf(objective) {
221
+ const raw = (objective && objective.collaborators) || [];
222
+ if (!Array.isArray(raw)) return [];
223
+ const out = [];
224
+ for (const c of raw) {
225
+ if (!c) continue;
226
+ const memberId = typeof c === "string" ? c : c.memberId || c.member_id || c.id;
227
+ if (!memberId) continue;
228
+ const role = String((typeof c === "object" && (c.role || c.kind)) || "CONTRIBUTOR").toUpperCase();
229
+ out.push({ memberId: String(memberId), role });
230
+ }
231
+ return out;
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Tree navigation
236
+ // ---------------------------------------------------------------------------
237
+
238
+ /**
239
+ * Nest a flat list into roots → children. Orphans (an unresolvable `parentKey`)
240
+ * surface as roots so nothing is silently dropped.
241
+ * @param {object|object[]} source @returns {object[]}
242
+ */
243
+ export function buildTree(source) {
244
+ const list = objectivesOf(source).map((o) => ({ ...o, children: [] }));
245
+ const byKey = new Map(list.map((o) => [o.key, o]));
246
+ const roots = [];
247
+ for (const o of list) {
248
+ const parent = o.parentKey ? byKey.get(o.parentKey) : null;
249
+ if (parent) parent.children.push(o);
250
+ else roots.push(o);
251
+ }
252
+ return roots;
253
+ }
254
+
255
+ /**
256
+ * The provenance chain for a key: `[node, parent, …, pillar]`. This is the
257
+ * offline "why did the agent do that?" walk — it needs no network.
258
+ * @param {object|object[]} source @param {string} key @returns {object[]}
259
+ */
260
+ export function ancestry(source, key) {
261
+ const byKey = indexByKey(source);
262
+ const out = [];
263
+ const guard = new Set();
264
+ let cur = byKey.get(key);
265
+ while (cur && !guard.has(cur.key)) {
266
+ guard.add(cur.key);
267
+ out.push(cur);
268
+ cur = cur.parentKey ? byKey.get(cur.parentKey) : null;
269
+ }
270
+ return out;
271
+ }
272
+
273
+ /** The PILLAR a key ultimately hangs from, or null. */
274
+ export function pillarFor(source, key) {
275
+ const chain = ancestry(source, key);
276
+ for (let i = chain.length - 1; i >= 0; i--) {
277
+ if (String(chain[i].kind).toUpperCase() === "PILLAR") return chain[i];
278
+ }
279
+ return null;
280
+ }
281
+
282
+ /**
283
+ * The provenance block stamped onto every machine-created task and audit line.
284
+ * @param {object|object[]} source @param {string} key
285
+ * @returns {{chain:string[], pillar:string|null, clause:string|null, adoptedBy:string|null, origin:string|null}}
286
+ */
287
+ export function provenanceOf(source, key) {
288
+ const chain = ancestry(source, key);
289
+ const self = chain[0] || null;
290
+ const pillar = pillarFor(source, key);
291
+ return {
292
+ chain: chain.map((n) => n.key),
293
+ pillar: pillar ? pillar.key : null,
294
+ clause: (pillar && pillar.charterSectionId) || (self && self.charterSectionId) || null,
295
+ adoptedBy: (self && self.adoptedById) || null,
296
+ origin: (self && self.source && self.source.origin) || null,
297
+ };
298
+ }
299
+
300
+ /** All PILLAR nodes, in input order. */
301
+ export function pillars(source) {
302
+ return objectivesOf(source).filter((o) => String(o.kind).toUpperCase() === "PILLAR");
303
+ }
304
+
305
+ /**
306
+ * The objectives that may legally drive work: measurable, `active`, structurally
307
+ * valid, and NOT self-adopted. Everything else — including every `proposed`
308
+ * node — compiles no obligations and admits no backlog.
309
+ *
310
+ * @param {object|object[]} source
311
+ * @param {object} [opts] @param {string} [opts.ownerMemberId] @param {string[]} [opts.badKeys]
312
+ * @returns {object[]}
313
+ */
314
+ export function adoptedObjectives(source, opts = {}) {
315
+ const owner = opts.ownerMemberId || memberIdOf(source) || null;
316
+ const bad = new Set(opts.badKeys || []);
317
+ return objectivesOf(source).filter((o) => {
318
+ if (!o || bad.has(o.key)) return false;
319
+ if (!MEASURABLE_KINDS.includes(String(o.kind).toUpperCase())) return false;
320
+ if (o.state !== "active") return false;
321
+ if (isSelfAdopted(o, owner)) return false;
322
+ return true;
323
+ });
324
+ }
325
+
326
+ /** True when a node was adopted by its own owning seat (the Goodhart hole). */
327
+ export function isSelfAdopted(objective, ownerMemberId) {
328
+ if (!objective || !objective.adoptedById) return false;
329
+ const owner = ownerMemberId || objective.ownerMemberId || objective.memberId || null;
330
+ return !!owner && objective.adoptedById === owner;
331
+ }
332
+
333
+ export default {
334
+ KINDS,
335
+ STATES,
336
+ DIRECTIONS,
337
+ ROLES,
338
+ SENSOR_SOURCES,
339
+ MEASURABLE_KINDS,
340
+ objectivesOf,
341
+ memberIdOf,
342
+ indexByKey,
343
+ validateMandateBody,
344
+ collaboratorsOf,
345
+ buildTree,
346
+ ancestry,
347
+ pillarFor,
348
+ provenanceOf,
349
+ pillars,
350
+ adoptedObjectives,
351
+ isSelfAdopted,
352
+ };