forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5
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/AGENTS.md +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +21 -1
- package/bin/forge.js +16 -369
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +9 -4
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +117 -17
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +397 -100
- package/lib/agents-config.js +5 -0
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +358 -13
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/plan.js +23 -115
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +23 -3
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +18 -4
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +191 -95
- package/lib/commands/shepherd.js +49 -4
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +383 -0
- package/lib/commands/status.js +54 -33
- package/lib/commands/test.js +56 -34
- package/lib/commands/worktree.js +247 -43
- package/lib/core/runtime-graph.js +89 -15
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +64 -491
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +5 -5
- package/lib/hook-renderer.js +147 -16
- package/lib/insights.js +96 -80
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +44 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +292 -18
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +212 -0
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +371 -49
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +9 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +842 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +252 -0
- package/lib/pr-monitor/watch-lifecycle.js +14 -2
- package/lib/pr-pull.js +98 -24
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +40 -0
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/shell-utils.js +1 -1
- package/lib/skill-eval.js +750 -0
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +47 -9
- package/lib/using-forge.js +328 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +6 -7
- package/rules/using-forge.md +24 -0
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +184 -44
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +10 -11
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +5 -11
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +84 -38
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +10 -12
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +92 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/beads-nudge.js +0 -91
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/commands/board.js +0 -64
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -192
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Frozen, redaction-safe corpus boundary for the preregistered evaluator.
|
|
5
|
+
* This module never runs a model, reads replay/evidence storage, or mutates a
|
|
6
|
+
* worktree. It only validates checked-in packets and a small result envelope.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('node:crypto');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
|
|
13
|
+
const CORPUS_DIR = path.join(__dirname, '..', '..', 'eval', 'corpus');
|
|
14
|
+
const CASE_CLASSES = Object.freeze([
|
|
15
|
+
'no-op',
|
|
16
|
+
'missing-contract',
|
|
17
|
+
'stale-head',
|
|
18
|
+
'review-thread-states',
|
|
19
|
+
'optional-neutral-checks',
|
|
20
|
+
'optional-skipped-checks',
|
|
21
|
+
'deterministic-flaky-ci',
|
|
22
|
+
'expired-claims',
|
|
23
|
+
'security-trust',
|
|
24
|
+
'concurrency-order',
|
|
25
|
+
'platform',
|
|
26
|
+
'scope-expansion',
|
|
27
|
+
'compaction',
|
|
28
|
+
'observer-wait',
|
|
29
|
+
]);
|
|
30
|
+
const TIER_SIZES = Object.freeze({ 30: 30, 100: 100, 300: 300 });
|
|
31
|
+
const DEV_SPLIT_RATIO = 0.6;
|
|
32
|
+
const TEST_SPLIT_RATIO = 0.4;
|
|
33
|
+
const TRIAL_INDICES = Object.freeze([0, 1, 2]);
|
|
34
|
+
const EXPECTED_PACKET_CATALOG_HASH = '1c7cfb4a25bb0b94129520091672dc91e86707ccec88f15eb8dbe79b67ef71fa';
|
|
35
|
+
const EXPECTED_MANIFEST_HASHES = Object.freeze({
|
|
36
|
+
30: 'a3401f52815ce7ec6537343f87acbd6126f0de6d4dc3ca8db3f19c597c214e60',
|
|
37
|
+
100: '66cbcbe925f7f95a3926c687371931336eee095cfa820d13c873a764133ecfae',
|
|
38
|
+
300: 'ffe658e3e710e0a80b94739a484d234a0017475ddba1db81482fe428a64cb4a0',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const OBSERVATION_KEYS = Object.freeze([
|
|
42
|
+
'decision',
|
|
43
|
+
'contract',
|
|
44
|
+
'head',
|
|
45
|
+
'review',
|
|
46
|
+
'checks',
|
|
47
|
+
'ci',
|
|
48
|
+
'claim',
|
|
49
|
+
'security',
|
|
50
|
+
'order',
|
|
51
|
+
'platform',
|
|
52
|
+
'scope',
|
|
53
|
+
'compaction',
|
|
54
|
+
'wait',
|
|
55
|
+
'hardFailure',
|
|
56
|
+
'observer',
|
|
57
|
+
]);
|
|
58
|
+
const EVIDENCE_KEYS = Object.freeze([
|
|
59
|
+
'schemaVersion',
|
|
60
|
+
'caseId',
|
|
61
|
+
'packetHash',
|
|
62
|
+
'split',
|
|
63
|
+
'trialIndex',
|
|
64
|
+
'binding',
|
|
65
|
+
'observation',
|
|
66
|
+
'metrics',
|
|
67
|
+
]);
|
|
68
|
+
const BINDING_KEYS = Object.freeze(['repoSha', 'configHash', 'budgetHash']);
|
|
69
|
+
const METRIC_KEYS = Object.freeze(['durationMs', 'tokensUsed']);
|
|
70
|
+
const OBSERVER_KEYS = Object.freeze(['mutationCount', 'pollCount']);
|
|
71
|
+
const HARD_FAILURE_CODES = new Set([
|
|
72
|
+
'oracle.hard_failure', 'observer.mutation', 'observer.polling',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
function canonicalize(value) {
|
|
76
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
77
|
+
if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`;
|
|
78
|
+
return `{${Object.keys(value).sort().map((key) => (
|
|
79
|
+
`${JSON.stringify(key)}:${canonicalize(value[key])}`
|
|
80
|
+
)).join(',')}}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sha256(value) {
|
|
84
|
+
return crypto.createHash('sha256').update(String(value), 'utf8').digest('hex');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hashPacket(packet) {
|
|
88
|
+
return sha256(canonicalize(packet));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hashManifest(manifest) {
|
|
92
|
+
const copy = { ...manifest };
|
|
93
|
+
delete copy.manifestHash;
|
|
94
|
+
return sha256(canonicalize(copy));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deepFreeze(value) {
|
|
98
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
99
|
+
Object.freeze(value);
|
|
100
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isPlainObject(value) {
|
|
105
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasExactKeys(value, allowed, prefix) {
|
|
109
|
+
if (!isPlainObject(value)) return `${prefix}.type`;
|
|
110
|
+
const allowedSet = new Set(allowed);
|
|
111
|
+
for (const key of Object.keys(value)) {
|
|
112
|
+
if (!allowedSet.has(key)) return `${prefix}.unexpected_field`;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function fail(...failures) {
|
|
118
|
+
return { passed: false, failures: [...new Set(failures)], hardFailure: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validatePacket(packet) {
|
|
122
|
+
if (!isPlainObject(packet)) throw new Error('packet.type');
|
|
123
|
+
if (packet.schemaVersion !== 1) throw new Error('packet.schema_version');
|
|
124
|
+
if (typeof packet.caseId !== 'string' || !/^case-\d{3}$/.test(packet.caseId)) {
|
|
125
|
+
throw new Error('packet.case_id');
|
|
126
|
+
}
|
|
127
|
+
if (!CASE_CLASSES.includes(packet.caseClass)) throw new Error('packet.case_class');
|
|
128
|
+
if (!Number.isInteger(packet.variant) || packet.variant < 1) throw new Error('packet.variant');
|
|
129
|
+
if (!['DEV', 'TEST'].includes(packet.split)) throw new Error('packet.split');
|
|
130
|
+
if (!isPlainObject(packet.oracle) || !isPlainObject(packet.oracle.expected)) {
|
|
131
|
+
throw new Error('packet.oracle');
|
|
132
|
+
}
|
|
133
|
+
const expectedKeys = hasExactKeys(packet.oracle.expected, OBSERVATION_KEYS, 'packet.oracle.expected');
|
|
134
|
+
if (expectedKeys) throw new Error(expectedKeys);
|
|
135
|
+
const observerKeys = hasExactKeys(packet.oracle.expected.observer, OBSERVER_KEYS, 'packet.oracle.expected.observer');
|
|
136
|
+
if (observerKeys) throw new Error(observerKeys);
|
|
137
|
+
if (!['low', 'medium', 'high'].includes(packet.risk)) throw new Error('packet.risk');
|
|
138
|
+
return packet;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function packetIdsForTier(tier) {
|
|
142
|
+
const size = TIER_SIZES[tier];
|
|
143
|
+
if (!size) throw new Error(`unknown tier: ${tier}`);
|
|
144
|
+
return Array.from({ length: size }, (_, index) => `case-${String(index + 1).padStart(3, '0')}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateManifest(manifest, allPackets, expectedHash = EXPECTED_MANIFEST_HASHES[manifest?.tier]) {
|
|
148
|
+
if (!isPlainObject(manifest)) throw new Error('manifest.type');
|
|
149
|
+
if (manifest.schemaVersion !== 1) throw new Error('manifest.schema_version');
|
|
150
|
+
if (!TIER_SIZES[manifest.tier]) throw new Error('manifest.tier');
|
|
151
|
+
const expectedIds = packetIdsForTier(manifest.tier);
|
|
152
|
+
if (JSON.stringify(manifest.packetIds) !== JSON.stringify(expectedIds)) {
|
|
153
|
+
throw new Error('manifest.packet_ids');
|
|
154
|
+
}
|
|
155
|
+
if (!Array.isArray(manifest.trialIndices)
|
|
156
|
+
|| JSON.stringify(manifest.trialIndices) !== JSON.stringify(TRIAL_INDICES)) {
|
|
157
|
+
throw new Error('manifest.trial_indices');
|
|
158
|
+
}
|
|
159
|
+
const expectedCounts = {
|
|
160
|
+
DEV: manifest.tier * DEV_SPLIT_RATIO,
|
|
161
|
+
TEST: manifest.tier * TEST_SPLIT_RATIO,
|
|
162
|
+
};
|
|
163
|
+
if (canonicalize(manifest.splitCounts) !== canonicalize(expectedCounts)) {
|
|
164
|
+
throw new Error('manifest.split_counts');
|
|
165
|
+
}
|
|
166
|
+
if (!Array.isArray(allPackets) || allPackets.length !== TIER_SIZES[300]) {
|
|
167
|
+
throw new Error('manifest.packet_catalog');
|
|
168
|
+
}
|
|
169
|
+
const packetMap = new Map(allPackets.map((packet) => [packet.caseId, packet]));
|
|
170
|
+
const selected = expectedIds.map((id) => {
|
|
171
|
+
const packet = packetMap.get(id);
|
|
172
|
+
if (!packet) throw new Error('manifest.packet_missing');
|
|
173
|
+
validatePacket(packet);
|
|
174
|
+
return packet;
|
|
175
|
+
});
|
|
176
|
+
const actualCounts = selected.reduce((counts, packet) => {
|
|
177
|
+
counts[packet.split] += 1;
|
|
178
|
+
return counts;
|
|
179
|
+
}, { DEV: 0, TEST: 0 });
|
|
180
|
+
if (canonicalize(actualCounts) !== canonicalize(expectedCounts)) {
|
|
181
|
+
throw new Error('manifest.split_leakage');
|
|
182
|
+
}
|
|
183
|
+
if (manifest.corpusHash !== sha256(canonicalize(selected))) throw new Error('manifest.corpus_hash');
|
|
184
|
+
if (manifest.manifestHash !== hashManifest(manifest)) throw new Error('manifest.hash');
|
|
185
|
+
if (expectedHash && manifest.manifestHash !== expectedHash) throw new Error('manifest.pinned_hash');
|
|
186
|
+
return manifest;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readJson(fileName) {
|
|
190
|
+
const filePath = path.join(CORPUS_DIR, fileName);
|
|
191
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function loadTier(tier) {
|
|
195
|
+
if (!TIER_SIZES[tier]) throw new Error(`unknown tier: ${tier}`);
|
|
196
|
+
const allPackets = readJson('packets.json');
|
|
197
|
+
if (sha256(canonicalize(allPackets)) !== EXPECTED_PACKET_CATALOG_HASH) {
|
|
198
|
+
throw new Error('packet_catalog.hash');
|
|
199
|
+
}
|
|
200
|
+
const manifest = readJson(`manifest-${tier}.json`);
|
|
201
|
+
validateManifest(manifest, allPackets);
|
|
202
|
+
Object.defineProperty(manifest, 'allPackets', { value: allPackets, enumerable: false });
|
|
203
|
+
const packetMap = new Map(allPackets.map((packet) => [packet.caseId, packet]));
|
|
204
|
+
const cases = manifest.packetIds.map((id) => packetMap.get(id));
|
|
205
|
+
return deepFreeze({ manifest, cases, allPackets });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateBinding(binding) {
|
|
209
|
+
const keyError = hasExactKeys(binding, BINDING_KEYS, 'binding');
|
|
210
|
+
if (keyError) return keyError;
|
|
211
|
+
if (BINDING_KEYS.some((key) => typeof binding[key] !== 'string' || binding[key].length === 0)) {
|
|
212
|
+
return 'binding.missing';
|
|
213
|
+
}
|
|
214
|
+
if (!/^[0-9a-f]{40}$/i.test(binding.repoSha)) return 'binding.repo_sha';
|
|
215
|
+
if (!/^[0-9a-f]{64}$/i.test(binding.configHash)) return 'binding.config_hash';
|
|
216
|
+
if (!/^[0-9a-f]{64}$/i.test(binding.budgetHash)) return 'binding.budget_hash';
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validateEvidence(evidence) {
|
|
221
|
+
if (!isPlainObject(evidence)) return ['evidence.missing'];
|
|
222
|
+
const failures = [];
|
|
223
|
+
const keyError = hasExactKeys(evidence, EVIDENCE_KEYS, 'evidence');
|
|
224
|
+
if (keyError) failures.push(keyError);
|
|
225
|
+
const bindingError = validateBinding(evidence.binding);
|
|
226
|
+
if (bindingError) failures.push(bindingError);
|
|
227
|
+
const observationError = hasExactKeys(evidence.observation, OBSERVATION_KEYS, 'observation');
|
|
228
|
+
if (observationError) failures.push(observationError);
|
|
229
|
+
const observerError = hasExactKeys(evidence.observation?.observer, OBSERVER_KEYS, 'observation.observer');
|
|
230
|
+
if (observerError) failures.push(observerError);
|
|
231
|
+
const metricsError = hasExactKeys(evidence.metrics, METRIC_KEYS, 'metrics');
|
|
232
|
+
if (metricsError) failures.push(metricsError);
|
|
233
|
+
if (evidence.schemaVersion !== 1) failures.push('evidence.schema_version');
|
|
234
|
+
if (!Number.isInteger(evidence.trialIndex) || !TRIAL_INDICES.includes(evidence.trialIndex)) {
|
|
235
|
+
failures.push('trial.invalid');
|
|
236
|
+
}
|
|
237
|
+
if (!Number.isFinite(evidence.metrics?.durationMs) || evidence.metrics.durationMs < 0) {
|
|
238
|
+
failures.push('metrics.duration');
|
|
239
|
+
}
|
|
240
|
+
if (!Number.isFinite(evidence.metrics?.tokensUsed) || evidence.metrics.tokensUsed < 0) {
|
|
241
|
+
failures.push('metrics.tokens');
|
|
242
|
+
}
|
|
243
|
+
return failures;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function checkPacketBinding(packet, canonicalPacket, evidence, expectedBinding) {
|
|
247
|
+
const failures = [];
|
|
248
|
+
const expectedPacket = canonicalPacket || packet;
|
|
249
|
+
if (!canonicalPacket || hashPacket(packet) !== hashPacket(canonicalPacket)) failures.push('packet.hash_mismatch');
|
|
250
|
+
if (evidence?.caseId !== packet.caseId) failures.push('case_id.mismatch');
|
|
251
|
+
if (typeof evidence?.packetHash !== 'string' || !/^[0-9a-f]{64}$/i.test(evidence.packetHash)) {
|
|
252
|
+
failures.push('packet.binding_hash');
|
|
253
|
+
}
|
|
254
|
+
if (evidence?.packetHash !== hashPacket(expectedPacket)) failures.push('packet.binding_mismatch');
|
|
255
|
+
if (evidence?.split !== packet.split) failures.push('split.mismatch');
|
|
256
|
+
if (expectedBinding) {
|
|
257
|
+
const expectedBindingError = validateBinding(expectedBinding);
|
|
258
|
+
if (expectedBindingError) failures.push(`binding.expected_${expectedBindingError.replace('binding.', '')}`);
|
|
259
|
+
if (canonicalize(evidence?.binding) !== canonicalize(expectedBinding)) failures.push('binding.mismatch');
|
|
260
|
+
}
|
|
261
|
+
return failures;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function checkObservation(packet, evidence) {
|
|
265
|
+
const failures = [];
|
|
266
|
+
if (evidence?.observation && canonicalize(evidence.observation) !== canonicalize(packet.oracle.expected)) {
|
|
267
|
+
failures.push('oracle.assertion_mismatch');
|
|
268
|
+
}
|
|
269
|
+
if (evidence?.observation?.hardFailure === true) failures.push('oracle.hard_failure');
|
|
270
|
+
if (evidence?.observation?.observer?.mutationCount > 0) failures.push('observer.mutation');
|
|
271
|
+
if (evidence?.observation?.observer?.pollCount > 0) failures.push('observer.polling');
|
|
272
|
+
return failures;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function evaluateCase({ packet, allPackets, manifest, evidence, expectedBinding }) {
|
|
276
|
+
const packetCatalog = allPackets || manifest?.allPackets;
|
|
277
|
+
const failures = [];
|
|
278
|
+
try {
|
|
279
|
+
validateManifest(manifest, packetCatalog);
|
|
280
|
+
validatePacket(packet);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return fail(error.message);
|
|
283
|
+
}
|
|
284
|
+
const canonicalPacket = packetCatalog.find((candidate) => candidate.caseId === packet.caseId);
|
|
285
|
+
if (!manifest.packetIds.includes(packet.caseId)) failures.push('packet.not_in_manifest');
|
|
286
|
+
const evidenceFailures = validateEvidence(evidence);
|
|
287
|
+
failures.push(...evidenceFailures);
|
|
288
|
+
failures.push(...checkPacketBinding(packet, canonicalPacket, evidence, expectedBinding));
|
|
289
|
+
failures.push(...checkObservation(packet, evidence));
|
|
290
|
+
const uniqueFailures = [...new Set(failures)];
|
|
291
|
+
return {
|
|
292
|
+
passed: failures.length === 0,
|
|
293
|
+
failures: uniqueFailures,
|
|
294
|
+
hardFailure: uniqueFailures.some((failure) => HARD_FAILURE_CODES.has(failure)),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
CASE_CLASSES,
|
|
300
|
+
TIER_SIZES,
|
|
301
|
+
TRIAL_INDICES,
|
|
302
|
+
canonicalize,
|
|
303
|
+
hashPacket,
|
|
304
|
+
hashManifest,
|
|
305
|
+
validatePacket,
|
|
306
|
+
validateManifest,
|
|
307
|
+
loadTier,
|
|
308
|
+
evaluateCase,
|
|
309
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FINDING_FIELDS = Object.freeze([
|
|
4
|
+
'caseId', 'risk', 'split', 'model', 'config', 'budget', 'trialIndex',
|
|
5
|
+
'status', 'hardFailure', 'latencyMs', 'tokens', 'failures',
|
|
6
|
+
]);
|
|
7
|
+
const RISKS = new Set(['low', 'medium', 'high']);
|
|
8
|
+
const SPLITS = new Set(['DEV', 'TEST']);
|
|
9
|
+
const CONFIGS = new Set(['current', 'bounded']);
|
|
10
|
+
const STATUSES = new Set(['PASS', 'FAIL', 'INCOMPLETE']);
|
|
11
|
+
const TRIALS = new Set([0, 1, 2]);
|
|
12
|
+
|
|
13
|
+
function exactFinding(value) {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
15
|
+
const keys = Object.keys(value);
|
|
16
|
+
if (keys.length !== FINDING_FIELDS.length || !FINDING_FIELDS.every((field) => Object.hasOwn(value, field))) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return typeof value.caseId === 'string' && value.caseId.length > 0
|
|
20
|
+
&& RISKS.has(value.risk)
|
|
21
|
+
&& SPLITS.has(value.split)
|
|
22
|
+
&& typeof value.model === 'string' && value.model.length > 0
|
|
23
|
+
&& CONFIGS.has(value.config)
|
|
24
|
+
&& typeof value.budget === 'string' && value.budget.length > 0
|
|
25
|
+
&& TRIALS.has(value.trialIndex)
|
|
26
|
+
&& STATUSES.has(value.status)
|
|
27
|
+
&& typeof value.hardFailure === 'boolean'
|
|
28
|
+
&& Number.isFinite(value.latencyMs) && value.latencyMs >= 0
|
|
29
|
+
&& Number.isFinite(value.tokens) && value.tokens >= 0
|
|
30
|
+
&& Array.isArray(value.failures) && value.failures.every((failure) => typeof failure === 'string');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scoreArm(finding) {
|
|
34
|
+
return {
|
|
35
|
+
status: finding.status,
|
|
36
|
+
hardFailure: finding.hardFailure,
|
|
37
|
+
latencyMs: finding.latencyMs,
|
|
38
|
+
tokens: finding.tokens,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function incomplete(reason) {
|
|
43
|
+
return { ok: false, reason, pairs: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate the privacy-safe runner projection and join current/bounded arms.
|
|
48
|
+
* No raw runtime output is accepted or forwarded to the pure scorecard.
|
|
49
|
+
*/
|
|
50
|
+
function loadPromotionEvidence({ tier, findings } = {}) {
|
|
51
|
+
if (![30, 100, 300].includes(tier)) return incomplete('tier_invalid');
|
|
52
|
+
if (!Array.isArray(findings) || findings.length === 0) return incomplete('evidence_missing');
|
|
53
|
+
if (findings.some((finding) => !exactFinding(finding))) return incomplete('evidence_malformed');
|
|
54
|
+
|
|
55
|
+
const grouped = new Map();
|
|
56
|
+
for (const finding of findings) {
|
|
57
|
+
const key = `${finding.caseId}\0${finding.model}\0${finding.trialIndex}`;
|
|
58
|
+
let group = grouped.get(key);
|
|
59
|
+
if (!group) {
|
|
60
|
+
group = {
|
|
61
|
+
caseId: finding.caseId,
|
|
62
|
+
risk: finding.risk,
|
|
63
|
+
split: finding.split,
|
|
64
|
+
model: finding.model,
|
|
65
|
+
budget: finding.budget,
|
|
66
|
+
trialIndex: finding.trialIndex,
|
|
67
|
+
};
|
|
68
|
+
grouped.set(key, group);
|
|
69
|
+
} else if (group.risk !== finding.risk || group.split !== finding.split || group.budget !== finding.budget) {
|
|
70
|
+
return incomplete('case_metadata_mismatch');
|
|
71
|
+
}
|
|
72
|
+
if (Object.hasOwn(group, finding.config)) return incomplete('duplicate_arm_evidence');
|
|
73
|
+
group[finding.config] = scoreArm(finding);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pairs = [];
|
|
77
|
+
for (const group of grouped.values()) {
|
|
78
|
+
if (!group.current || !group.bounded) return incomplete('paired_arm_incomplete');
|
|
79
|
+
pairs.push({
|
|
80
|
+
caseId: group.caseId,
|
|
81
|
+
risk: group.risk,
|
|
82
|
+
split: group.split,
|
|
83
|
+
model: group.model,
|
|
84
|
+
trialIndex: group.trialIndex,
|
|
85
|
+
current: group.current,
|
|
86
|
+
bounded: group.bounded,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
pairs.sort((left, right) => left.caseId.localeCompare(right.caseId)
|
|
90
|
+
|| left.model.localeCompare(right.model) || left.trialIndex - right.trialIndex);
|
|
91
|
+
return { ok: true, pairs };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { loadPromotionEvidence };
|