@aragon-agent/cli 0.6.44 → 0.6.45
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/CHANGELOG.md +44 -0
- package/README.md +69 -2
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +224 -11
- package/dist/agent/governance-view.d.ts +40 -0
- package/dist/agent/governance-view.js +40 -0
- package/dist/agent/governance-wiring.d.ts +162 -0
- package/dist/agent/governance-wiring.js +470 -0
- package/dist/agent/plan-prompt.d.ts +7 -0
- package/dist/agent/plan-prompt.js +2 -1
- package/dist/agent/reducer.d.ts +33 -1
- package/dist/agent/reducer.js +45 -9
- package/dist/agent/system-prompt.d.ts +19 -0
- package/dist/agent/system-prompt.js +9 -1
- package/dist/cli.js +45 -1
- package/dist/commands/builtins.js +10 -4
- package/dist/commands/governance-command.d.ts +35 -0
- package/dist/commands/governance-command.js +292 -0
- package/dist/commands/governance-runs-table.d.ts +40 -0
- package/dist/commands/governance-runs-table.js +130 -0
- package/dist/commands/registry.d.ts +12 -0
- package/dist/config/app-paths.d.ts +10 -0
- package/dist/config/app-paths.js +12 -0
- package/dist/config/cli-commands.d.ts +18 -0
- package/dist/config/cli-commands.js +47 -1
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +10 -1
- package/dist/config/governance-config.d.ts +125 -0
- package/dist/config/governance-config.js +164 -0
- package/dist/config/governance-launch.d.ts +71 -0
- package/dist/config/governance-launch.js +58 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/load.js +26 -1
- package/dist/config/model-profile-store.d.ts +4 -2
- package/dist/config/schema.d.ts +22 -0
- package/dist/config/schema.js +6 -0
- package/dist/config/store.js +14 -1
- package/dist/config/ui-state.d.ts +11 -0
- package/dist/config/ui-state.js +11 -0
- package/dist/diagnostics/doctor.js +20 -0
- package/dist/governance/card-normalize.d.ts +24 -0
- package/dist/governance/card-normalize.js +76 -0
- package/dist/governance/copy.d.ts +109 -0
- package/dist/governance/copy.js +223 -0
- package/dist/governance/engine/aftermath.d.ts +49 -0
- package/dist/governance/engine/aftermath.js +134 -0
- package/dist/governance/engine/artifact-persist.d.ts +35 -0
- package/dist/governance/engine/artifact-persist.js +86 -0
- package/dist/governance/engine/budget.d.ts +52 -0
- package/dist/governance/engine/budget.js +112 -0
- package/dist/governance/engine/cards.d.ts +18 -0
- package/dist/governance/engine/cards.js +186 -0
- package/dist/governance/engine/digest.d.ts +32 -0
- package/dist/governance/engine/digest.js +63 -0
- package/dist/governance/engine/engine-core.d.ts +219 -0
- package/dist/governance/engine/engine-core.js +562 -0
- package/dist/governance/engine/engine.d.ts +68 -0
- package/dist/governance/engine/engine.js +314 -0
- package/dist/governance/engine/estimate.d.ts +38 -0
- package/dist/governance/engine/estimate.js +46 -0
- package/dist/governance/engine/inspector.d.ts +16 -0
- package/dist/governance/engine/inspector.js +396 -0
- package/dist/governance/engine/integration.d.ts +35 -0
- package/dist/governance/engine/integration.js +220 -0
- package/dist/governance/engine/lifecycle.d.ts +29 -0
- package/dist/governance/engine/lifecycle.js +656 -0
- package/dist/governance/engine/pause.d.ts +56 -0
- package/dist/governance/engine/pause.js +107 -0
- package/dist/governance/engine/phase-members.d.ts +15 -0
- package/dist/governance/engine/phase-members.js +34 -0
- package/dist/governance/engine/phase-planner.d.ts +53 -0
- package/dist/governance/engine/phase-planner.js +282 -0
- package/dist/governance/engine/rebuild.d.ts +27 -0
- package/dist/governance/engine/rebuild.js +57 -0
- package/dist/governance/engine/report.d.ts +66 -0
- package/dist/governance/engine/report.js +220 -0
- package/dist/governance/engine/run-digest.d.ts +20 -0
- package/dist/governance/engine/run-digest.js +42 -0
- package/dist/governance/engine/spec-builder.d.ts +117 -0
- package/dist/governance/engine/spec-builder.js +321 -0
- package/dist/governance/engine/stage-runner.d.ts +22 -0
- package/dist/governance/engine/stage-runner.js +664 -0
- package/dist/governance/engine/start-input.d.ts +19 -0
- package/dist/governance/engine/start-input.js +111 -0
- package/dist/governance/engine/state-machine.d.ts +101 -0
- package/dist/governance/engine/state-machine.js +586 -0
- package/dist/governance/engine/submission-queries.d.ts +42 -0
- package/dist/governance/engine/submission-queries.js +97 -0
- package/dist/governance/engine/submission-render.d.ts +12 -0
- package/dist/governance/engine/submission-render.js +92 -0
- package/dist/governance/engine/submissions.d.ts +27 -0
- package/dist/governance/engine/submissions.js +455 -0
- package/dist/governance/engine/visibility.d.ts +104 -0
- package/dist/governance/engine/visibility.js +223 -0
- package/dist/governance/governed-dispatch.d.ts +19 -0
- package/dist/governance/governed-dispatch.js +17 -0
- package/dist/governance/limits.d.ts +107 -0
- package/dist/governance/limits.js +131 -0
- package/dist/governance/posture-transition.d.ts +38 -0
- package/dist/governance/posture-transition.js +44 -0
- package/dist/governance/posture.d.ts +35 -0
- package/dist/governance/posture.js +59 -0
- package/dist/governance/profiles.d.ts +48 -0
- package/dist/governance/profiles.js +140 -0
- package/dist/governance/prompts/orchestrator-block.d.ts +40 -0
- package/dist/governance/prompts/orchestrator-block.js +128 -0
- package/dist/governance/prompts/plan-addendum.d.ts +22 -0
- package/dist/governance/prompts/plan-addendum.js +60 -0
- package/dist/governance/prompts/refusals.d.ts +48 -0
- package/dist/governance/prompts/refusals.js +82 -0
- package/dist/governance/prompts/role-blocks.d.ts +40 -0
- package/dist/governance/prompts/role-blocks.js +255 -0
- package/dist/governance/report-reader.d.ts +31 -0
- package/dist/governance/report-reader.js +57 -0
- package/dist/governance/roles.d.ts +83 -0
- package/dist/governance/roles.js +142 -0
- package/dist/governance/rules/claims.d.ts +83 -0
- package/dist/governance/rules/claims.js +250 -0
- package/dist/governance/rules/elimination.d.ts +66 -0
- package/dist/governance/rules/elimination.js +105 -0
- package/dist/governance/rules/events-table.d.ts +46 -0
- package/dist/governance/rules/events-table.js +94 -0
- package/dist/governance/rules/failure-analysis.d.ts +28 -0
- package/dist/governance/rules/failure-analysis.js +93 -0
- package/dist/governance/rules/ledger-replay.d.ts +53 -0
- package/dist/governance/rules/ledger-replay.js +53 -0
- package/dist/governance/rules/ledger-state.d.ts +85 -0
- package/dist/governance/rules/ledger-state.js +175 -0
- package/dist/governance/rules/ledger.d.ts +228 -0
- package/dist/governance/rules/ledger.js +560 -0
- package/dist/governance/rules/rca.d.ts +68 -0
- package/dist/governance/rules/rca.js +220 -0
- package/dist/governance/rules/settlement.d.ts +129 -0
- package/dist/governance/rules/settlement.js +363 -0
- package/dist/governance/rules/suspicion.d.ts +66 -0
- package/dist/governance/rules/suspicion.js +169 -0
- package/dist/governance/store/artifacts.d.ts +81 -0
- package/dist/governance/store/artifacts.js +173 -0
- package/dist/governance/store/atomic.d.ts +79 -0
- package/dist/governance/store/atomic.js +119 -0
- package/dist/governance/store/events-log.d.ts +35 -0
- package/dist/governance/store/events-log.js +86 -0
- package/dist/governance/store/run-lock.d.ts +51 -0
- package/dist/governance/store/run-lock.js +122 -0
- package/dist/governance/store/run-store.d.ts +229 -0
- package/dist/governance/store/run-store.js +644 -0
- package/dist/governance/store/scoreboard-export.d.ts +53 -0
- package/dist/governance/store/scoreboard-export.js +65 -0
- package/dist/governance/tools/child-tools.d.ts +31 -0
- package/dist/governance/tools/child-tools.js +65 -0
- package/dist/governance/tools/guard.d.ts +71 -0
- package/dist/governance/tools/guard.js +194 -0
- package/dist/governance/tools/inspector-tools.d.ts +63 -0
- package/dist/governance/tools/inspector-tools.js +424 -0
- package/dist/governance/tools/lead-tools.d.ts +42 -0
- package/dist/governance/tools/lead-tools.js +226 -0
- package/dist/governance/tools/normalize-util.d.ts +101 -0
- package/dist/governance/tools/normalize-util.js +230 -0
- package/dist/governance/tools/scan-incomplete.d.ts +79 -0
- package/dist/governance/tools/scan-incomplete.js +486 -0
- package/dist/governance/tools/shared-tools.d.ts +44 -0
- package/dist/governance/tools/shared-tools.js +125 -0
- package/dist/governance/tools/submit-normalizers.d.ts +28 -0
- package/dist/governance/tools/submit-normalizers.js +474 -0
- package/dist/governance/tools/submit-tools.d.ts +26 -0
- package/dist/governance/tools/submit-tools.js +171 -0
- package/dist/governance/types.d.ts +661 -0
- package/dist/governance/types.js +27 -0
- package/dist/session/validate-session.js +32 -1
- package/dist/team/runtime.d.ts +8 -0
- package/dist/team/runtime.js +9 -0
- package/dist/team/subagent.d.ts +23 -0
- package/dist/team/subagent.js +14 -3
- package/dist/team/types.d.ts +6 -0
- package/dist/tools/index.d.ts +30 -7
- package/dist/tools/index.js +14 -0
- package/dist/tools/plan-tools.d.ts +14 -2
- package/dist/tools/plan-tools.js +5 -2
- package/dist/ui/App.js +145 -25
- package/dist/ui/BottomStatusRow.js +1 -1
- package/dist/ui/Composer.d.ts +7 -1
- package/dist/ui/Composer.js +9 -6
- package/dist/ui/GovernancePanel.d.ts +50 -0
- package/dist/ui/GovernancePanel.js +77 -0
- package/dist/ui/GovernanceRun.d.ts +18 -0
- package/dist/ui/GovernanceRun.js +11 -0
- package/dist/ui/StatusBar.d.ts +15 -2
- package/dist/ui/StatusBar.js +10 -4
- package/dist/ui/Transcript.js +3 -0
- package/dist/ui/entries/GovernanceCard.d.ts +30 -0
- package/dist/ui/entries/GovernanceCard.js +32 -0
- package/dist/ui/governance-copy.d.ts +69 -0
- package/dist/ui/governance-copy.js +104 -0
- package/dist/ui/governance-status.d.ts +145 -0
- package/dist/ui/governance-status.js +326 -0
- package/dist/ui/interaction-copy.d.ts +4 -0
- package/dist/ui/interaction-copy.js +5 -0
- package/dist/ui/interaction-hints.d.ts +17 -0
- package/dist/ui/interaction-hints.js +14 -2
- package/dist/ui/layout/governance-board.d.ts +121 -0
- package/dist/ui/layout/governance-board.js +382 -0
- package/dist/ui/layout/governance-card.d.ts +53 -0
- package/dist/ui/layout/governance-card.js +134 -0
- package/dist/ui/layout/governance-chip.d.ts +38 -0
- package/dist/ui/layout/governance-chip.js +64 -0
- package/dist/ui/layout/governance-panel.d.ts +85 -0
- package/dist/ui/layout/governance-panel.js +358 -0
- package/dist/ui/layout/governance-roster.d.ts +65 -0
- package/dist/ui/layout/governance-roster.js +275 -0
- package/dist/ui/layout/status-detail-layout.js +3 -3
- package/dist/ui/layout/status-layout.d.ts +5 -2
- package/dist/ui/layout/status-layout.js +10 -6
- package/dist/ui/layout/virtual-window.js +11 -0
- package/dist/ui/overlays/GovernanceBoardOverlay.d.ts +41 -0
- package/dist/ui/overlays/GovernanceBoardOverlay.js +76 -0
- package/dist/ui/overlays/GovernanceSettingsPage.d.ts +48 -0
- package/dist/ui/overlays/GovernanceSettingsPage.js +93 -0
- package/dist/ui/overlays/HelpOverlay.js +4 -3
- package/dist/ui/overlays/QuestionOverlay.d.ts +9 -1
- package/dist/ui/overlays/QuestionOverlay.js +4 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +21 -1
- package/dist/ui/overlays/governance-settings.d.ts +79 -0
- package/dist/ui/overlays/governance-settings.js +140 -0
- package/dist/ui/overlays/model-profile-state.d.ts +6 -1
- package/dist/ui/overlays/model-profile-state.js +10 -0
- package/dist/ui/palettes.d.ts +17 -0
- package/dist/ui/palettes.js +9 -0
- package/dist/ui/scrollback/live-preview.js +2 -0
- package/dist/ui/scrollback/settled-boundary.js +3 -0
- package/dist/ui/shortcuts.js +2 -2
- package/dist/ui/theme.d.ts +7 -0
- package/dist/ui/theme.js +1 -0
- package/dist/ui/transcript-text.js +9 -0
- package/dist/ui/use-wheel-routing.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identities, roster sizing and work-package assignment (governance-mode 5.2).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY (`governance/` is glyph-scanned). Pure: no I/O, no clock, no randomness, so a
|
|
5
|
+
* resumed run recomputes exactly the same roster.
|
|
6
|
+
*
|
|
7
|
+
* IDENTITIES ARE LEDGER ENTITIES, NOT PROCESSES (5.2). Every dispatch builds a fresh
|
|
8
|
+
* `Agent`; continuity comes from the ledger and the artifact store. The id is also the
|
|
9
|
+
* subagent `label`, so it must satisfy the label rule `[a-z0-9-]{1,12}` AND must never be
|
|
10
|
+
* the reserved aggregate label `team` (a child labelled `team` would be addressed by
|
|
11
|
+
* `team_send {to:"team"}` as a broadcast). Ids here are `<prefix><slot>` and
|
|
12
|
+
* `<id>-r<generation>`, which cannot spell `team` by construction.
|
|
13
|
+
*
|
|
14
|
+
* "THE WRITER NEVER REVIEWS THEMSELF" IS STRUCTURAL: DEV, REV and TEST ids come from three
|
|
15
|
+
* disjoint prefixes, so no assignment function can hand a work package's author the review
|
|
16
|
+
* of it.
|
|
17
|
+
*/
|
|
18
|
+
/** The subagent label rule every identity satisfies. */
|
|
19
|
+
export const IDENTITY_ID_PATTERN = /^[a-z0-9-]{1,12}$/;
|
|
20
|
+
/** The single inspector's id. Never scored, never respawned. */
|
|
21
|
+
export const INSPECTOR_ID = 'insp';
|
|
22
|
+
const ROLE_PREFIX = { DEV: 'dev', REV: 'rev', TEST: 'tst' };
|
|
23
|
+
const PREFIX_ROLE = { dev: 'DEV', rev: 'REV', tst: 'TEST' };
|
|
24
|
+
/** REV / TEST roster is `clamp(ceil(K / 2), 1, 3)` (5.2). */
|
|
25
|
+
const REVIEW_ROSTER_MIN = 1;
|
|
26
|
+
const REVIEW_ROSTER_MAX = 3;
|
|
27
|
+
/** Highest slot / generation an id can carry and still fit 12 characters. */
|
|
28
|
+
const MAX_SLOT = 999;
|
|
29
|
+
const MAX_GENERATION = 99;
|
|
30
|
+
/** The roster id of slot `slot` (1-based) of a scored role: `dev1`, `rev2`, `tst1`. */
|
|
31
|
+
export function makeIdentityId(role, slot) {
|
|
32
|
+
if (!Number.isInteger(slot) || slot < 1 || slot > MAX_SLOT) {
|
|
33
|
+
throw new Error(`makeIdentityId: slot must be an integer in 1..${MAX_SLOT}, got ${slot}`);
|
|
34
|
+
}
|
|
35
|
+
return `${ROLE_PREFIX[role]}${slot}`;
|
|
36
|
+
}
|
|
37
|
+
/** `dev2` -> `dev2-r1`; `dev2-r1` -> `dev2-r2`. The base slot never changes. */
|
|
38
|
+
export function respawnIdentityId(predecessorId) {
|
|
39
|
+
const parsed = parseIdentityId(predecessorId);
|
|
40
|
+
if (!parsed || parsed.role === 'INSP') {
|
|
41
|
+
throw new Error(`respawnIdentityId: "${predecessorId}" is not a respawnable identity`);
|
|
42
|
+
}
|
|
43
|
+
const generation = parsed.generation + 1;
|
|
44
|
+
if (generation > MAX_GENERATION) {
|
|
45
|
+
throw new Error(`respawnIdentityId: generation ${generation} exceeds ${MAX_GENERATION}`);
|
|
46
|
+
}
|
|
47
|
+
return `${ROLE_PREFIX[parsed.role]}${parsed.slot}-r${generation}`;
|
|
48
|
+
}
|
|
49
|
+
/** Inverse of `makeIdentityId` / `respawnIdentityId`; `null` for anything else. */
|
|
50
|
+
export function parseIdentityId(id) {
|
|
51
|
+
if (id === INSPECTOR_ID)
|
|
52
|
+
return { role: 'INSP', slot: 0, generation: 0 };
|
|
53
|
+
const m = /^(dev|rev|tst)([1-9][0-9]{0,2})(?:-r([1-9][0-9]?))?$/.exec(id);
|
|
54
|
+
if (!m)
|
|
55
|
+
return null;
|
|
56
|
+
const role = PREFIX_ROLE[m[1]];
|
|
57
|
+
if (!role)
|
|
58
|
+
return null;
|
|
59
|
+
return { role, slot: Number(m[2]), generation: m[3] ? Number(m[3]) : 0 };
|
|
60
|
+
}
|
|
61
|
+
/** The ids `dev1..devN` for `count` slots of `role`. */
|
|
62
|
+
export function rosterIds(role, count) {
|
|
63
|
+
return Array.from({ length: Math.max(0, count) }, (_, i) => makeIdentityId(role, i + 1));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Roster size for `wpCount` work packages (5.2): `DEV = K`, `REV = TEST =
|
|
67
|
+
* clamp(ceil(K/2), 1, 3)`. `stageWidthCap` (the user's `team.maxSubagents`, D-24) bounds the
|
|
68
|
+
* REV / TEST counts, because those are exactly the identities that run side by side in one
|
|
69
|
+
* review / test stage. DEV is NOT bounded: the engine splits a wide dev stage into chunks
|
|
70
|
+
* (the `NEXT` it prints is already chunked), so the roster may exceed one dispatch.
|
|
71
|
+
*/
|
|
72
|
+
export function rosterSizes(wpCount, stageWidthCap) {
|
|
73
|
+
const k = Math.max(1, Math.floor(wpCount));
|
|
74
|
+
const cap = Math.max(1, Math.floor(stageWidthCap));
|
|
75
|
+
const reviewers = Math.min(cap, Math.min(REVIEW_ROSTER_MAX, Math.max(REVIEW_ROSTER_MIN, Math.ceil(k / 2))));
|
|
76
|
+
return { dev: k, rev: reviewers, test: reviewers };
|
|
77
|
+
}
|
|
78
|
+
/** Follow `takeover` links to the identity that now owns `id`'s work; cycle-safe. */
|
|
79
|
+
function resolveSuccessor(id, takeover) {
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
let current = id;
|
|
82
|
+
while (takeover[current] !== undefined && !seen.has(current)) {
|
|
83
|
+
seen.add(current);
|
|
84
|
+
current = takeover[current];
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Assign work packages to identities.
|
|
90
|
+
*
|
|
91
|
+
* 1. A WP with a sticky preference goes to that identity - or to its successor if it was
|
|
92
|
+
* replaced - as long as the result is an active identity.
|
|
93
|
+
* 2. Every other WP goes to the identity with the fewest WPs so far, ties broken by roster
|
|
94
|
+
* order. With no stickiness this is exactly "WP i goes to identity i mod M" (5.2); with
|
|
95
|
+
* stickiness it keeps the load level instead of piling every new WP on the identity that
|
|
96
|
+
* happens to come next in a fixed rotation.
|
|
97
|
+
*
|
|
98
|
+
* Identities that receive nothing are omitted, so the caller dispatches no idle child.
|
|
99
|
+
* Deterministic: the same input always yields the same output.
|
|
100
|
+
*/
|
|
101
|
+
export function assignRoundRobin(options) {
|
|
102
|
+
const { wpIds, identities, sticky = {}, takeover = {} } = options;
|
|
103
|
+
if (identities.length === 0) {
|
|
104
|
+
throw new Error('assignRoundRobin: at least one active identity is required');
|
|
105
|
+
}
|
|
106
|
+
const active = new Set(identities);
|
|
107
|
+
const buckets = new Map(identities.map((id) => [id, []]));
|
|
108
|
+
const pinned = new Map();
|
|
109
|
+
for (const wpId of wpIds) {
|
|
110
|
+
const preferred = sticky[wpId];
|
|
111
|
+
if (preferred === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
const owner = resolveSuccessor(preferred, takeover);
|
|
114
|
+
if (active.has(owner))
|
|
115
|
+
pinned.set(wpId, owner);
|
|
116
|
+
}
|
|
117
|
+
for (const wpId of wpIds) {
|
|
118
|
+
const owner = pinned.get(wpId);
|
|
119
|
+
if (owner !== undefined)
|
|
120
|
+
buckets.get(owner).push(wpId);
|
|
121
|
+
}
|
|
122
|
+
for (const wpId of wpIds) {
|
|
123
|
+
if (pinned.has(wpId))
|
|
124
|
+
continue;
|
|
125
|
+
buckets.get(leastLoaded(identities, buckets)).push(wpId);
|
|
126
|
+
}
|
|
127
|
+
const order = new Map(wpIds.map((id, i) => [id, i]));
|
|
128
|
+
return identities
|
|
129
|
+
.map((identity) => ({
|
|
130
|
+
identity,
|
|
131
|
+
wpIds: buckets.get(identity).sort((a, b) => order.get(a) - order.get(b)),
|
|
132
|
+
}))
|
|
133
|
+
.filter((a) => a.wpIds.length > 0);
|
|
134
|
+
}
|
|
135
|
+
function leastLoaded(identities, buckets) {
|
|
136
|
+
let best = identities[0];
|
|
137
|
+
for (const id of identities) {
|
|
138
|
+
if (buckets.get(id).length < buckets.get(best).length)
|
|
139
|
+
best = id;
|
|
140
|
+
}
|
|
141
|
+
return best;
|
|
142
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claims and finding fingerprints (governance-mode 5.5 / 5.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY (`governance/` is glyph-scanned).
|
|
5
|
+
*
|
|
6
|
+
* A CLAIM is an accusation that books NOTHING when it is raised: a tester who says "the
|
|
7
|
+
* reviewer missed this" earns +10 only if the inspector upholds it in the settle stage, and
|
|
8
|
+
* pays -10 if it is rejected. Deferring the booking is what keeps claims from being a
|
|
9
|
+
* score farm ("accuse everyone, collect T2").
|
|
10
|
+
*
|
|
11
|
+
* FINGERPRINTS decide "the same problem rejected twice" (D8) WITHOUT a language model:
|
|
12
|
+
* `normalized file + sorted keyword set of the issue text`. Two findings count as the same
|
|
13
|
+
* when they name the same file and share most of their keywords. Deterministic, so the
|
|
14
|
+
* result is reproducible from the event log and pinned by fixed examples in the tests.
|
|
15
|
+
*/
|
|
16
|
+
import type { Claim, LedgerState, ReviewSubmission, TestSubmission } from '../types.js';
|
|
17
|
+
import type { LedgerEvent } from './ledger.js';
|
|
18
|
+
/** Lower-case, forward slashes, no leading `./`: so `.\\Src\\A.ts` and `src/a.ts` agree. */
|
|
19
|
+
export declare function normalizeFindingPath(file: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The sorted, de-duplicated keyword set of an issue text: lower-cased, punctuation
|
|
22
|
+
* stripped, stop-words and bare numbers (line numbers drift between rounds) dropped.
|
|
23
|
+
* Falls back to the unfiltered tokens, then to `_`, so an issue never fingerprints to an
|
|
24
|
+
* empty set that would match every other finding on the same file.
|
|
25
|
+
*/
|
|
26
|
+
export declare function issueKeywords(issue: string): string[];
|
|
27
|
+
export interface FingerprintInput {
|
|
28
|
+
file: string;
|
|
29
|
+
issue: string;
|
|
30
|
+
}
|
|
31
|
+
/** `file#kw1+kw2+...`. Deterministic; the unit tests pin fixed examples. */
|
|
32
|
+
export declare function fingerprintFinding(finding: FingerprintInput): string;
|
|
33
|
+
/**
|
|
34
|
+
* D8 test: does any new finding repeat a previously rejected one? Same file AND a keyword
|
|
35
|
+
* overlap of at least 60 percent of the smaller set (and at least two shared keywords
|
|
36
|
+
* unless the smaller set has one).
|
|
37
|
+
*/
|
|
38
|
+
export declare function findingsIntersect(a: readonly string[], b: readonly string[]): boolean;
|
|
39
|
+
export interface ClaimContext {
|
|
40
|
+
wpId: string;
|
|
41
|
+
phase: number;
|
|
42
|
+
/** The DEV holding the WP. */
|
|
43
|
+
developer: string | null;
|
|
44
|
+
/** The REV assigned to the WP. */
|
|
45
|
+
reviewer: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Claims implied by a test submission: one per kind, never one per issue (a tester cannot
|
|
49
|
+
* multiply T2 by splitting one defect into five lines). Explicit `claims[]` come first
|
|
50
|
+
* and win over an implied claim of the same kind. Ids are deterministic
|
|
51
|
+
* (`cl-<wp>-t<attempt>-<n>`) so replay and tests need no id generator.
|
|
52
|
+
*/
|
|
53
|
+
export declare function claimsFromTest(ctx: ClaimContext, submission: TestSubmission): Claim[];
|
|
54
|
+
/** A reviewer's `securityFinding` becomes a security claim against the developer. */
|
|
55
|
+
export declare function claimsFromReview(ctx: ClaimContext, submission: ReviewSubmission): Claim[];
|
|
56
|
+
export interface RuleClaimInput {
|
|
57
|
+
/** Ledger state: only used to look up the roles of the parties. */
|
|
58
|
+
state: LedgerState;
|
|
59
|
+
claim: Claim;
|
|
60
|
+
decision: 'upheld' | 'rejected';
|
|
61
|
+
ruling: string;
|
|
62
|
+
at: number;
|
|
63
|
+
/** The inspector's identity, e.g. `insp`. */
|
|
64
|
+
inspectorId: string;
|
|
65
|
+
}
|
|
66
|
+
export interface RuleClaimResult {
|
|
67
|
+
claim: Claim;
|
|
68
|
+
events: LedgerEvent[];
|
|
69
|
+
notes: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Expand an inspector ruling into ledger events (5.6 L2):
|
|
73
|
+
*
|
|
74
|
+
* - upheld missed_by_review: R4 against the reviewer, T2 for the tester.
|
|
75
|
+
* - upheld incomplete_impl: R7 against the reviewer, T7 for the tester.
|
|
76
|
+
* - upheld security: D6 against the developer. upheld same_issue: D8 against the developer.
|
|
77
|
+
* - upheld false_positive: R5 / T5 against the party whose finding was wrong.
|
|
78
|
+
* - ANY rejected claim: T5 (tester) or R5 (reviewer) against the CLAIMANT.
|
|
79
|
+
*
|
|
80
|
+
* Idempotent: a claim that is no longer `open` yields no events, so a repeated
|
|
81
|
+
* `ledger_rule` call cannot pay twice.
|
|
82
|
+
*/
|
|
83
|
+
export declare function ruleClaim(input: RuleClaimInput): RuleClaimResult;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claims and finding fingerprints (governance-mode 5.5 / 5.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY (`governance/` is glyph-scanned).
|
|
5
|
+
*
|
|
6
|
+
* A CLAIM is an accusation that books NOTHING when it is raised: a tester who says "the
|
|
7
|
+
* reviewer missed this" earns +10 only if the inspector upholds it in the settle stage, and
|
|
8
|
+
* pays -10 if it is rejected. Deferring the booking is what keeps claims from being a
|
|
9
|
+
* score farm ("accuse everyone, collect T2").
|
|
10
|
+
*
|
|
11
|
+
* FINGERPRINTS decide "the same problem rejected twice" (D8) WITHOUT a language model:
|
|
12
|
+
* `normalized file + sorted keyword set of the issue text`. Two findings count as the same
|
|
13
|
+
* when they name the same file and share most of their keywords. Deterministic, so the
|
|
14
|
+
* result is reproducible from the event log and pinned by fixed examples in the tests.
|
|
15
|
+
*/
|
|
16
|
+
import { eventDef } from './events-table.js';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Fingerprints
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const STOP_WORDS = new Set([
|
|
21
|
+
'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'am',
|
|
22
|
+
'of', 'in', 'on', 'at', 'to', 'for', 'from', 'by', 'with', 'without', 'within',
|
|
23
|
+
'and', 'or', 'but', 'nor', 'not', 'no', 'this', 'that', 'these', 'those',
|
|
24
|
+
'it', 'its', 'as', 'if', 'then', 'than', 'so', 'do', 'does', 'did', 'done',
|
|
25
|
+
'has', 'have', 'had', 'should', 'must', 'can', 'could', 'would', 'will', 'shall',
|
|
26
|
+
'may', 'might', 'there', 'here', 'which', 'when', 'where', 'while', 'into',
|
|
27
|
+
'onto', 'over', 'under', 'also', 'only', 'very', 'too', 'any', 'all', 'some',
|
|
28
|
+
'each', 'every', 'more', 'most', 'less', 'such', 'per', 'via', 'about',
|
|
29
|
+
'because', 'however', 'currently', 'still', 'just', 'line', 'lines',
|
|
30
|
+
]);
|
|
31
|
+
/** Separator between the file part and the keyword part of a fingerprint. */
|
|
32
|
+
const FP_SEPARATOR = '#';
|
|
33
|
+
/** Lower-case, forward slashes, no leading `./`: so `.\\Src\\A.ts` and `src/a.ts` agree. */
|
|
34
|
+
export function normalizeFindingPath(file) {
|
|
35
|
+
return file.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/{2,}/g, '/').toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
/** Plural `s` only: enough to unify "handles"/"handle"; anything cleverer breeds collisions. */
|
|
38
|
+
function stem(word) {
|
|
39
|
+
if (word.length > 3 && word.endsWith('s') && !/(ss|us|is)$/.test(word))
|
|
40
|
+
return word.slice(0, -1);
|
|
41
|
+
return word;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The sorted, de-duplicated keyword set of an issue text: lower-cased, punctuation
|
|
45
|
+
* stripped, stop-words and bare numbers (line numbers drift between rounds) dropped.
|
|
46
|
+
* Falls back to the unfiltered tokens, then to `_`, so an issue never fingerprints to an
|
|
47
|
+
* empty set that would match every other finding on the same file.
|
|
48
|
+
*/
|
|
49
|
+
export function issueKeywords(issue) {
|
|
50
|
+
const tokens = issue
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
53
|
+
.split(' ')
|
|
54
|
+
.filter((t) => t.length > 0);
|
|
55
|
+
const meaningful = tokens.filter((t) => !STOP_WORDS.has(t) && !/^\d+$/.test(t)).map(stem);
|
|
56
|
+
const chosen = meaningful.length > 0 ? meaningful : tokens;
|
|
57
|
+
const unique = [...new Set(chosen)].sort();
|
|
58
|
+
return unique.length > 0 ? unique : ['_'];
|
|
59
|
+
}
|
|
60
|
+
/** `file#kw1+kw2+...`. Deterministic; the unit tests pin fixed examples. */
|
|
61
|
+
export function fingerprintFinding(finding) {
|
|
62
|
+
const file = normalizeFindingPath(finding.file);
|
|
63
|
+
return `${file}${FP_SEPARATOR}${issueKeywords(finding.issue).join('+')}`;
|
|
64
|
+
}
|
|
65
|
+
function parseFingerprint(fingerprint) {
|
|
66
|
+
const at = fingerprint.lastIndexOf(FP_SEPARATOR);
|
|
67
|
+
if (at < 0)
|
|
68
|
+
return { file: fingerprint, keywords: [] };
|
|
69
|
+
return {
|
|
70
|
+
file: fingerprint.slice(0, at),
|
|
71
|
+
keywords: fingerprint.slice(at + 1).split('+').filter((k) => k.length > 0),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function sameProblem(a, b) {
|
|
75
|
+
if (a === b)
|
|
76
|
+
return true;
|
|
77
|
+
const left = parseFingerprint(a);
|
|
78
|
+
const right = parseFingerprint(b);
|
|
79
|
+
if (left.file !== right.file)
|
|
80
|
+
return false;
|
|
81
|
+
const rightSet = new Set(right.keywords);
|
|
82
|
+
const shared = left.keywords.filter((k) => rightSet.has(k)).length;
|
|
83
|
+
const smaller = Math.min(left.keywords.length, right.keywords.length);
|
|
84
|
+
if (smaller === 0)
|
|
85
|
+
return false;
|
|
86
|
+
return shared >= Math.min(2, smaller) && shared / smaller >= 0.6;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* D8 test: does any new finding repeat a previously rejected one? Same file AND a keyword
|
|
90
|
+
* overlap of at least 60 percent of the smaller set (and at least two shared keywords
|
|
91
|
+
* unless the smaller set has one).
|
|
92
|
+
*/
|
|
93
|
+
export function findingsIntersect(a, b) {
|
|
94
|
+
return a.some((x) => b.some((y) => sameProblem(x, y)));
|
|
95
|
+
}
|
|
96
|
+
function finishClaim(ctx, id, draft) {
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
kind: draft.kind,
|
|
100
|
+
raisedBy: draft.raisedBy,
|
|
101
|
+
against: draft.against,
|
|
102
|
+
wpId: ctx.wpId,
|
|
103
|
+
justification: draft.justification,
|
|
104
|
+
status: 'open',
|
|
105
|
+
phase: ctx.phase,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Who a claim of `kind` accuses when the claimant did not say. */
|
|
109
|
+
function defaultAgainst(kind, ctx) {
|
|
110
|
+
switch (kind) {
|
|
111
|
+
case 'security':
|
|
112
|
+
case 'same_issue':
|
|
113
|
+
return ctx.developer;
|
|
114
|
+
case 'missed_by_review':
|
|
115
|
+
case 'incomplete_impl':
|
|
116
|
+
case 'false_positive':
|
|
117
|
+
return ctx.reviewer;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** First `max` justifications joined, so one claim can carry several flagged issues. */
|
|
121
|
+
function joinJustifications(parts, max) {
|
|
122
|
+
const kept = parts.filter((p) => p.trim().length > 0).slice(0, max);
|
|
123
|
+
return kept.join(' | ');
|
|
124
|
+
}
|
|
125
|
+
function flaggedIssueClaim(ctx, submission) {
|
|
126
|
+
const flagged = submission.issues.filter((i) => i.missedByReview === true);
|
|
127
|
+
if (flagged.length === 0 || ctx.reviewer === null)
|
|
128
|
+
return null;
|
|
129
|
+
const kind = submission.verdict === 'INCOMPLETE_IMPL' ? 'incomplete_impl' : 'missed_by_review';
|
|
130
|
+
const justification = joinJustifications(flagged.map((i) => i.reviewMissJustification ?? i.description), 3);
|
|
131
|
+
return { kind, raisedBy: submission.identity, against: ctx.reviewer, justification };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Claims implied by a test submission: one per kind, never one per issue (a tester cannot
|
|
135
|
+
* multiply T2 by splitting one defect into five lines). Explicit `claims[]` come first
|
|
136
|
+
* and win over an implied claim of the same kind. Ids are deterministic
|
|
137
|
+
* (`cl-<wp>-t<attempt>-<n>`) so replay and tests need no id generator.
|
|
138
|
+
*/
|
|
139
|
+
export function claimsFromTest(ctx, submission) {
|
|
140
|
+
const drafts = [];
|
|
141
|
+
for (const c of submission.claims) {
|
|
142
|
+
const against = c.against ?? defaultAgainst(c.kind, ctx);
|
|
143
|
+
if (against === null || drafts.some((d) => d.kind === c.kind))
|
|
144
|
+
continue;
|
|
145
|
+
drafts.push({
|
|
146
|
+
kind: c.kind,
|
|
147
|
+
raisedBy: submission.identity,
|
|
148
|
+
against,
|
|
149
|
+
justification: c.justification,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const implied = flaggedIssueClaim(ctx, submission);
|
|
153
|
+
if (implied !== null && !drafts.some((d) => d.kind === implied.kind))
|
|
154
|
+
drafts.push(implied);
|
|
155
|
+
return drafts.map((d, i) => finishClaim(ctx, `cl-${ctx.wpId}-t${submission.attempt}-${i + 1}`, d));
|
|
156
|
+
}
|
|
157
|
+
/** A reviewer's `securityFinding` becomes a security claim against the developer. */
|
|
158
|
+
export function claimsFromReview(ctx, submission) {
|
|
159
|
+
const finding = submission.securityFinding;
|
|
160
|
+
if (finding === undefined || ctx.developer === null)
|
|
161
|
+
return [];
|
|
162
|
+
return [
|
|
163
|
+
finishClaim(ctx, `cl-${ctx.wpId}-r${submission.attempt}-1`, {
|
|
164
|
+
kind: 'security',
|
|
165
|
+
raisedBy: submission.identity,
|
|
166
|
+
against: ctx.developer,
|
|
167
|
+
justification: joinJustifications([finding.description, finding.evidence], 2),
|
|
168
|
+
}),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
function roleOf(state, agentId) {
|
|
172
|
+
return state.agents[agentId]?.role;
|
|
173
|
+
}
|
|
174
|
+
/** The "you were wrong" event for an agent of `role` (T5 tester, R5 reviewer). */
|
|
175
|
+
function falsePositiveCode(role) {
|
|
176
|
+
if (role === 'TEST')
|
|
177
|
+
return 'T5';
|
|
178
|
+
if (role === 'REV')
|
|
179
|
+
return 'R5';
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const UPHELD_CODES = {
|
|
183
|
+
missed_by_review: { against: 'R4', raisedBy: 'T2' },
|
|
184
|
+
incomplete_impl: { against: 'R7', raisedBy: 'T7' },
|
|
185
|
+
security: { against: 'D6', raisedBy: null },
|
|
186
|
+
same_issue: { against: 'D8', raisedBy: null },
|
|
187
|
+
false_positive: { against: null, raisedBy: null },
|
|
188
|
+
};
|
|
189
|
+
function claimEvent(input, code, agentId) {
|
|
190
|
+
const { claim } = input;
|
|
191
|
+
return {
|
|
192
|
+
code,
|
|
193
|
+
agentId,
|
|
194
|
+
at: input.at,
|
|
195
|
+
phase: claim.phase,
|
|
196
|
+
by: `inspector:${input.inspectorId}`,
|
|
197
|
+
reason: `Claim ${claim.id} ${input.decision}: ${eventDef(code).title}`,
|
|
198
|
+
evidence: input.ruling,
|
|
199
|
+
wpId: claim.wpId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function upheldEvents(input, notes) {
|
|
203
|
+
const { claim, state } = input;
|
|
204
|
+
const events = [];
|
|
205
|
+
const codes = UPHELD_CODES[claim.kind];
|
|
206
|
+
if (claim.kind === 'false_positive') {
|
|
207
|
+
const code = falsePositiveCode(roleOf(state, claim.against));
|
|
208
|
+
if (code === null)
|
|
209
|
+
notes.push(`No false-positive event exists for ${claim.against}.`);
|
|
210
|
+
else
|
|
211
|
+
events.push(claimEvent(input, code, claim.against));
|
|
212
|
+
return events;
|
|
213
|
+
}
|
|
214
|
+
if (codes.against !== null)
|
|
215
|
+
events.push(claimEvent(input, codes.against, claim.against));
|
|
216
|
+
if (codes.raisedBy !== null)
|
|
217
|
+
events.push(claimEvent(input, codes.raisedBy, claim.raisedBy));
|
|
218
|
+
return events;
|
|
219
|
+
}
|
|
220
|
+
function rejectedEvents(input, notes) {
|
|
221
|
+
const { claim, state } = input;
|
|
222
|
+
const code = falsePositiveCode(roleOf(state, claim.raisedBy));
|
|
223
|
+
if (code === null) {
|
|
224
|
+
notes.push(`Rejected claim ${claim.id}: no penalty event exists for ${claim.raisedBy}.`);
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
return [claimEvent(input, code, claim.raisedBy)];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Expand an inspector ruling into ledger events (5.6 L2):
|
|
231
|
+
*
|
|
232
|
+
* - upheld missed_by_review: R4 against the reviewer, T2 for the tester.
|
|
233
|
+
* - upheld incomplete_impl: R7 against the reviewer, T7 for the tester.
|
|
234
|
+
* - upheld security: D6 against the developer. upheld same_issue: D8 against the developer.
|
|
235
|
+
* - upheld false_positive: R5 / T5 against the party whose finding was wrong.
|
|
236
|
+
* - ANY rejected claim: T5 (tester) or R5 (reviewer) against the CLAIMANT.
|
|
237
|
+
*
|
|
238
|
+
* Idempotent: a claim that is no longer `open` yields no events, so a repeated
|
|
239
|
+
* `ledger_rule` call cannot pay twice.
|
|
240
|
+
*/
|
|
241
|
+
export function ruleClaim(input) {
|
|
242
|
+
const notes = [];
|
|
243
|
+
if (input.claim.status !== 'open') {
|
|
244
|
+
notes.push(`Claim ${input.claim.id} was already ${input.claim.status}; nothing booked.`);
|
|
245
|
+
return { claim: input.claim, events: [], notes };
|
|
246
|
+
}
|
|
247
|
+
const events = input.decision === 'upheld' ? upheldEvents(input, notes) : rejectedEvents(input, notes);
|
|
248
|
+
const claim = { ...input.claim, status: input.decision, rulingText: input.ruling };
|
|
249
|
+
return { claim, events, notes };
|
|
250
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elimination rules and team health (governance-mode 5.8).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY (`governance/` is glyph-scanned).
|
|
5
|
+
*
|
|
6
|
+
* TWO WAYS OUT, BOTH READ `confirmed` ONLY (I-G3):
|
|
7
|
+
*
|
|
8
|
+
* - KILL: `confirmed <= 0`, immediate, no appeal. `apply()` performs it inside the
|
|
9
|
+
* booking that depleted the balance; `killDepleted` is the sweep the engine runs after
|
|
10
|
+
* a batch as a safety net (and for a state loaded from an older checkpoint).
|
|
11
|
+
* - TAIL: a role with at least three active members drops its lowest member when that
|
|
12
|
+
* member is BOTH the unique lowest and below 60 for two phase ends in a row. The first
|
|
13
|
+
* time is a warning. A tie for the lowest place eliminates nobody (a coin toss between
|
|
14
|
+
* equals would be arbitrary) and is reported as `tie_skip`.
|
|
15
|
+
*
|
|
16
|
+
* Provisional escrow never saves or condemns an agent: a clawback can slash an agent's
|
|
17
|
+
* total without touching survival.
|
|
18
|
+
*/
|
|
19
|
+
import type { LedgerState, ScoredRole } from '../types.js';
|
|
20
|
+
export { AVOID_ADVICE, FAILURE_ANALYSIS_TOP_PENALTIES, buildFailureAnalysis, negativeEventCodes, } from './failure-analysis.js';
|
|
21
|
+
export interface KillSweepResult {
|
|
22
|
+
state: LedgerState;
|
|
23
|
+
eliminated: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Eliminate every active agent whose `confirmed` is <= 0. Idempotent. */
|
|
26
|
+
export declare function killDepleted(state: LedgerState, at: number): KillSweepResult;
|
|
27
|
+
export interface TailWarning {
|
|
28
|
+
agentId: string;
|
|
29
|
+
role: ScoredRole;
|
|
30
|
+
strikes: number;
|
|
31
|
+
}
|
|
32
|
+
export interface TailTie {
|
|
33
|
+
role: ScoredRole;
|
|
34
|
+
agentIds: string[];
|
|
35
|
+
confirmed: number;
|
|
36
|
+
}
|
|
37
|
+
export interface TailResult {
|
|
38
|
+
state: LedgerState;
|
|
39
|
+
/** First (or later, not yet fatal) strike: the agent is told it is in the tail. */
|
|
40
|
+
warnings: TailWarning[];
|
|
41
|
+
eliminated: string[];
|
|
42
|
+
/** Ties for the lowest place: nobody is eliminated (`tie_skip`). */
|
|
43
|
+
tieSkips: TailTie[];
|
|
44
|
+
notes: string[];
|
|
45
|
+
}
|
|
46
|
+
export interface TailRequest {
|
|
47
|
+
phase: number;
|
|
48
|
+
at: number;
|
|
49
|
+
/** Stored-event seq to stamp on any elimination. */
|
|
50
|
+
seq?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Phase-end tail check for every role. Order is fixed (DEV, REV, TEST) so the result and
|
|
54
|
+
* any seq stamping are reproducible on replay.
|
|
55
|
+
*/
|
|
56
|
+
export declare function evaluateTail(state: LedgerState, request: TailRequest): TailResult;
|
|
57
|
+
export type TeamHealthLabel = 'UNKNOWN' | 'HEALTHY' | 'CHECKPOINT_ADVISED';
|
|
58
|
+
export interface TeamHealthStatus {
|
|
59
|
+
/** Mean `confirmed` over active agents; null before any agent exists. */
|
|
60
|
+
health: number | null;
|
|
61
|
+
label: TeamHealthLabel;
|
|
62
|
+
/** Text for the status / settle brief when a coordination checkpoint is advised. */
|
|
63
|
+
advisory: string | null;
|
|
64
|
+
}
|
|
65
|
+
/** `< 70` advises a coordination checkpoint (and a focused interface check at settle). */
|
|
66
|
+
export declare function teamHealthStatus(state: LedgerState): TeamHealthStatus;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elimination rules and team health (governance-mode 5.8).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY (`governance/` is glyph-scanned).
|
|
5
|
+
*
|
|
6
|
+
* TWO WAYS OUT, BOTH READ `confirmed` ONLY (I-G3):
|
|
7
|
+
*
|
|
8
|
+
* - KILL: `confirmed <= 0`, immediate, no appeal. `apply()` performs it inside the
|
|
9
|
+
* booking that depleted the balance; `killDepleted` is the sweep the engine runs after
|
|
10
|
+
* a batch as a safety net (and for a state loaded from an older checkpoint).
|
|
11
|
+
* - TAIL: a role with at least three active members drops its lowest member when that
|
|
12
|
+
* member is BOTH the unique lowest and below 60 for two phase ends in a row. The first
|
|
13
|
+
* time is a warning. A tie for the lowest place eliminates nobody (a coin toss between
|
|
14
|
+
* equals would be arbitrary) and is reported as `tie_skip`.
|
|
15
|
+
*
|
|
16
|
+
* Provisional escrow never saves or condemns an agent: a clawback can slash an agent's
|
|
17
|
+
* total without touching survival.
|
|
18
|
+
*/
|
|
19
|
+
import { GOV_LIMITS } from '../limits.js';
|
|
20
|
+
import { eliminate } from './ledger.js';
|
|
21
|
+
import { activeAgents, teamHealth } from './ledger-state.js';
|
|
22
|
+
export { AVOID_ADVICE, FAILURE_ANALYSIS_TOP_PENALTIES, buildFailureAnalysis, negativeEventCodes, } from './failure-analysis.js';
|
|
23
|
+
const SCORED_ROLES = ['DEV', 'REV', 'TEST'];
|
|
24
|
+
/** Eliminate every active agent whose `confirmed` is <= 0. Idempotent. */
|
|
25
|
+
export function killDepleted(state, at) {
|
|
26
|
+
let current = state;
|
|
27
|
+
const eliminated = [];
|
|
28
|
+
for (const agent of activeAgents(state)) {
|
|
29
|
+
if (agent.confirmed > 0)
|
|
30
|
+
continue;
|
|
31
|
+
const outcome = eliminate(current, { agentId: agent.id, reason: 'depleted', at });
|
|
32
|
+
current = outcome.state;
|
|
33
|
+
if (outcome.eliminated)
|
|
34
|
+
eliminated.push(agent.id);
|
|
35
|
+
}
|
|
36
|
+
return { state: current, eliminated };
|
|
37
|
+
}
|
|
38
|
+
function withStrikes(state, agent, strikes) {
|
|
39
|
+
if (agent.tailStrikes === strikes)
|
|
40
|
+
return state;
|
|
41
|
+
return { ...state, agents: { ...state.agents, [agent.id]: { ...agent, tailStrikes: strikes } } };
|
|
42
|
+
}
|
|
43
|
+
/** Clear strikes: the streak must be CONSECUTIVE phase ends. */
|
|
44
|
+
function resetStrikes(state, agents) {
|
|
45
|
+
return agents.reduce((s, a) => withStrikes(s, s.agents[a.id], 0), state);
|
|
46
|
+
}
|
|
47
|
+
function evaluateRole(state, role, request, out) {
|
|
48
|
+
const members = activeAgents(state, role);
|
|
49
|
+
if (members.length < GOV_LIMITS.tailEliminationMinRoleSize) {
|
|
50
|
+
return resetStrikes(state, members);
|
|
51
|
+
}
|
|
52
|
+
const lowest = Math.min(...members.map((m) => m.confirmed));
|
|
53
|
+
if (lowest >= GOV_LIMITS.tailEliminationThreshold)
|
|
54
|
+
return resetStrikes(state, members);
|
|
55
|
+
const tied = members.filter((m) => m.confirmed === lowest);
|
|
56
|
+
if (tied.length > 1) {
|
|
57
|
+
out.tieSkips.push({ role, agentIds: tied.map((m) => m.id), confirmed: lowest });
|
|
58
|
+
out.notes.push(`tie_skip: ${role} ${tied.map((m) => m.id).join(', ')} tie at ${lowest}.`);
|
|
59
|
+
return resetStrikes(state, members);
|
|
60
|
+
}
|
|
61
|
+
const target = tied[0];
|
|
62
|
+
let current = resetStrikes(state, members.filter((m) => m.id !== target.id));
|
|
63
|
+
const strikes = target.tailStrikes + 1;
|
|
64
|
+
current = withStrikes(current, current.agents[target.id], strikes);
|
|
65
|
+
if (strikes < 2) {
|
|
66
|
+
out.warnings.push({ agentId: target.id, role, strikes });
|
|
67
|
+
out.notes.push(`Tail warning: ${target.id} is lowest in ${role} at ${lowest}.`);
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
const outcome = eliminate(current, {
|
|
71
|
+
agentId: target.id,
|
|
72
|
+
reason: 'tail',
|
|
73
|
+
at: request.at,
|
|
74
|
+
...(request.seq === undefined ? {} : { seq: request.seq }),
|
|
75
|
+
});
|
|
76
|
+
if (outcome.eliminated)
|
|
77
|
+
out.eliminated.push(target.id);
|
|
78
|
+
return outcome.state;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Phase-end tail check for every role. Order is fixed (DEV, REV, TEST) so the result and
|
|
82
|
+
* any seq stamping are reproducible on replay.
|
|
83
|
+
*/
|
|
84
|
+
export function evaluateTail(state, request) {
|
|
85
|
+
const out = { state, warnings: [], eliminated: [], tieSkips: [], notes: [] };
|
|
86
|
+
let current = state;
|
|
87
|
+
for (const role of SCORED_ROLES)
|
|
88
|
+
current = evaluateRole(current, role, request, out);
|
|
89
|
+
out.state = current;
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** `< 70` advises a coordination checkpoint (and a focused interface check at settle). */
|
|
93
|
+
export function teamHealthStatus(state) {
|
|
94
|
+
const health = teamHealth(state);
|
|
95
|
+
if (health === null)
|
|
96
|
+
return { health, label: 'UNKNOWN', advisory: null };
|
|
97
|
+
if (health >= GOV_LIMITS.healthWarnThreshold)
|
|
98
|
+
return { health, label: 'HEALTHY', advisory: null };
|
|
99
|
+
return {
|
|
100
|
+
health,
|
|
101
|
+
label: 'CHECKPOINT_ADVISED',
|
|
102
|
+
advisory: `Team health ${health.toFixed(1)} is below ${GOV_LIMITS.healthWarnThreshold}: ` +
|
|
103
|
+
'a coordination checkpoint is advised; the next settle checks interface conflicts.',
|
|
104
|
+
};
|
|
105
|
+
}
|