mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-20 08:38:05 +02:00
feat: group verified capsules into offline retrospective reports
Independent local Codex review PASS at 472cfa94fb, no P0/P1. Independent retrospective, CLI, envelope and capsule tests: 73 passed. CI run 34664487219 attempt 2 passed all 42 jobs. Read-only offline capsule grouping only; no provenance, scoring, execution or promotion claim. Rollback: revert this squash commit.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
* node scripts/eval-harness.js capsule verify <dir>
|
||||
* node scripts/eval-harness.js capsule project <dir>
|
||||
* node scripts/eval-harness.js capsule export <dir> <out-dir>
|
||||
* node scripts/eval-harness.js capsule group <dir> [<dir> ...]
|
||||
* node scripts/eval-harness.js gate run <gate.config.json> [--work-dir <dir>] [--capsule <dir>]
|
||||
* node scripts/eval-harness.js receipt build <capsule-dir> [--artifact <file>] [--gate <gate-receipt.json>] [--out <file>]
|
||||
* node scripts/eval-harness.js receipt verify <receipt.json> <capsule-dir> [--artifact <file>] [--gate <gate-receipt.json>]
|
||||
@@ -26,7 +27,7 @@ function usage(message) {
|
||||
if (message) {
|
||||
process.stderr.write(`eval-harness: ${message}\n`);
|
||||
}
|
||||
const header = fs.readFileSync(__filename, 'utf8').split('\n').slice(3, 15).map((line) => line.replace(/^ \*\s?/, '')).join('\n');
|
||||
const header = fs.readFileSync(__filename, 'utf8').split('\n').slice(3, 16).map((line) => line.replace(/^ \*\s?/, '')).join('\n');
|
||||
process.stderr.write(`${header}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
@@ -64,6 +65,13 @@ function runExample(action) {
|
||||
function runCapsule(action, rest) {
|
||||
const dir = rest[0];
|
||||
if (!dir) usage('capsule commands need a capsule directory');
|
||||
if (action === 'group') {
|
||||
if (rest.length > harness.retrospective.MAX_INPUTS || rest.some(arg => !arg.trim() || arg.startsWith('--'))) {
|
||||
usage(`capsule group needs 1 to ${harness.retrospective.MAX_INPUTS} directory paths and accepts no flags`);
|
||||
}
|
||||
print(harness.retrospective.groupCapsules(rest));
|
||||
return;
|
||||
}
|
||||
if (action === 'verify') {
|
||||
const result = harness.capsule.verify(dir);
|
||||
print(result);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*
|
||||
* envelope capsule-envelope/v1 contract, redaction, secret canaries
|
||||
* capsule append-only hash-linked journal with five lineages
|
||||
* retrospective offline report-only grouping of selected capsule snapshots
|
||||
* gate static inspection and disabled execution gate, syntactic warnings
|
||||
* replay declared tool effects, fixtures, fail-closed replay, retired effect preload
|
||||
* receipt offline-verifiable capsule receipts
|
||||
@@ -16,6 +17,7 @@ module.exports = {
|
||||
canonical: require('./canonical'),
|
||||
envelope: require('./envelope'),
|
||||
capsule: require('./capsule'),
|
||||
retrospective: require('./retrospective'),
|
||||
gate: require('./gate'),
|
||||
replay: require('./replay'),
|
||||
receipt: require('./receipt'),
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
/** Read-only retrospective preparation over explicit, quiescent local capsules.
|
||||
* Counts are declarations in the records, never scores or promotion evidence.
|
||||
* Inherits capsule.project's local-reader limits; not hostile-filesystem isolation.
|
||||
*/
|
||||
const capsule = require('./capsule');
|
||||
const { LINEAGES, EFFECT_CLASSES } = require('./envelope');
|
||||
const { hashValue } = require('./canonical');
|
||||
|
||||
const MAX_INPUTS = 100;
|
||||
const SOURCE_FIELDS = ['entry_count', 'root_hash', 'journal_sha256', 'projection_hash'];
|
||||
|
||||
class RetrospectiveError extends Error {
|
||||
constructor(code, message, inputIndex) {
|
||||
super(message);
|
||||
this.name = 'RetrospectiveError';
|
||||
this.code = code;
|
||||
if (inputIndex !== undefined) this.input_index = inputIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function readProjection(dir, index) {
|
||||
try {
|
||||
return capsule.project(dir);
|
||||
} catch {
|
||||
// Reader errors can contain private paths or journal content. No partial
|
||||
// report or unchecked diagnostic content crosses the report boundary.
|
||||
throw new RetrospectiveError('retrospective.invalid_capsule', `capsule at input index ${index} failed verification`, index);
|
||||
}
|
||||
}
|
||||
|
||||
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
||||
const identity = projection => JSON.stringify([projection.run_id, projection.capsule_id]);
|
||||
|
||||
function summarizeGroup(harnessVersion, projections) {
|
||||
const total = keys => Object.fromEntries(keys.map(key => [key, 0]));
|
||||
const byLineage = total(LINEAGES);
|
||||
const byEffect = total(EFFECT_CLASSES);
|
||||
for (const projection of projections) {
|
||||
for (const key of LINEAGES) byLineage[key] += projection.by_lineage[key];
|
||||
for (const key of EFFECT_CLASSES) byEffect[key] += projection.by_effect_class[key];
|
||||
}
|
||||
return {
|
||||
harness_version: harnessVersion,
|
||||
capsule_count: projections.length,
|
||||
entry_count: projections.reduce((sum, item) => sum + item.entry_count, 0),
|
||||
by_lineage: byLineage,
|
||||
by_effect_class: byEffect,
|
||||
sources: [...projections].sort((a, b) => compare(identity(a), identity(b)))
|
||||
.map(item => ({
|
||||
identity_hash: hashValue([item.run_id, item.capsule_id]),
|
||||
...Object.fromEntries(SOURCE_FIELDS.map(key => [key, item[key]])),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Group up to 100 selected snapshots of one task family by harness_version.
|
||||
* Duplicate identities count once only if the verified projections match.
|
||||
* Different checkpoints of the same identity are ambiguous and refused.
|
||||
* Does not read saved projections, copy payloads, write files or invoke tools.
|
||||
*/
|
||||
function groupCapsules(dirs) {
|
||||
if (!Array.isArray(dirs) || dirs.length < 1 || dirs.length > MAX_INPUTS
|
||||
|| !Array.from(dirs).every(dir => typeof dir === 'string' && dir.trim() && !dir.includes('\0'))) {
|
||||
throw new RetrospectiveError('retrospective.invalid_inputs', `supply 1 to ${MAX_INPUTS} capsule directory paths`);
|
||||
}
|
||||
const projections = dirs.map(readProjection);
|
||||
const family = projections[0].task_family;
|
||||
const unique = new Map();
|
||||
for (const [index, projection] of projections.entries()) {
|
||||
if (projection.task_family !== family) {
|
||||
throw new RetrospectiveError('retrospective.mixed_task_families', 'all capsules must have the same task family', index);
|
||||
}
|
||||
const key = identity(projection);
|
||||
const previous = unique.get(key);
|
||||
if (previous && previous.projection_hash !== projection.projection_hash) {
|
||||
throw new RetrospectiveError('retrospective.conflicting_identity', 'conflicting snapshots share a capsule identity', index);
|
||||
}
|
||||
unique.set(key, projection);
|
||||
}
|
||||
const byHarness = new Map();
|
||||
for (const projection of unique.values()) {
|
||||
const version = projection.harness_version;
|
||||
byHarness.set(version, [...(byHarness.get(version) || []), projection]);
|
||||
}
|
||||
const report = {
|
||||
schema: 'capsule-retrospective/v1',
|
||||
report_only: true,
|
||||
task_family: family,
|
||||
input_count: dirs.length,
|
||||
capsule_count: unique.size,
|
||||
duplicate_count: dirs.length - unique.size,
|
||||
groups: [...byHarness].sort(([a], [b]) => compare(a, b))
|
||||
.map(([version, items]) => summarizeGroup(version, items)),
|
||||
};
|
||||
return { ...report, report_hash: hashValue(report) };
|
||||
}
|
||||
|
||||
module.exports = { groupCapsules, MAX_INPUTS, RetrospectiveError };
|
||||
Reference in New Issue
Block a user