@claude-flow/cli 3.56.10 → 3.57.0
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/.claude/helpers/.helpers-version +1 -1
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/daemon.js +8 -2
- package/dist/src/commands/doctor.js +95 -18
- package/dist/src/commands/policy.js +6 -1
- package/dist/src/index.js +5 -5
- package/dist/src/init/settings-generator.js +4 -58
- package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
- package/dist/src/mcp-tools/memory-tools.js +30 -5
- package/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/dist/src/mcp-tools/neural-tools.js +20 -50
- package/dist/src/mcp-tools/policy-tools.js +5 -3
- package/dist/src/memory/intelligence.js +80 -33
- package/dist/src/memory/stats-file-lock.d.ts +7 -0
- package/dist/src/memory/stats-file-lock.js +53 -0
- package/dist/src/services/distill-oracle.d.ts +1 -1
- package/dist/src/services/distill-oracle.js +2 -2
- package/dist/src/services/policy-anchor-log.d.ts +13 -0
- package/dist/src/services/policy-anchor-log.js +242 -0
- package/dist/src/services/policy-archive-scan.d.ts +7 -0
- package/dist/src/services/policy-archive-scan.js +50 -0
- package/dist/src/services/policy-ledger-anchor.d.ts +14 -8
- package/dist/src/services/policy-ledger-anchor.js +356 -40
- package/dist/src/services/policy-ledger-audit.d.ts +22 -0
- package/dist/src/services/policy-ledger-audit.js +260 -0
- package/dist/src/services/policy-ledger-record.d.ts +3 -0
- package/dist/src/services/policy-ledger-record.js +22 -0
- package/dist/src/services/policy-runtime.d.ts +1 -0
- package/dist/src/services/policy-runtime.js +261 -40
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
- package/package.json +2 -2
- package/plugins/ruflo-console/dist/tui.mjs +153 -153
- package/plugins/ruflo-metaharness/hooks/screen.ts +7 -1
- package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
- package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +0 -179
- package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +0 -1
- package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +0 -32
- package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +0 -23
- package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +0 -11
- package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +0 -16
- package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +0 -91
- package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +0 -17
- package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +0 -12
- package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +0 -8
- package/plugins/ruflo-metaharness/.claude-flow/neural/patterns.json +0 -2809
- package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +0 -6
- package/plugins/ruflo-metaharness/.claude-flow/policy/state.json +0 -11
- package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-shm +0 -0
- package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-wal +0 -0
- package/plugins/ruflo-metaharness/.swarm/backups/memory-2026-10-14T16-09-38-164Z.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/backups/store-agentdb-memory.db-2026-10-14T16-09-38-166Z.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/ewc-fisher.json +0 -397
- package/plugins/ruflo-metaharness/.swarm/memory.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/memory.db-shm +0 -0
- package/plugins/ruflo-metaharness/.swarm/memory.db-wal +0 -0
- package/plugins/ruflo-metaharness/agentdb.rvf +0 -0
- package/plugins/ruflo-metaharness/agentdb.rvf.lock +0 -0
- package/plugins/ruflo-metaharness/ruvector.db +0 -0
|
@@ -908,9 +908,6 @@ export const neuralTools = [
|
|
|
908
908
|
if (!v.valid)
|
|
909
909
|
return { success: false, error: v.error };
|
|
910
910
|
}
|
|
911
|
-
// Resolve provider so embeddingProvider in the result matches the old
|
|
912
|
-
// eager-init reporting.
|
|
913
|
-
await ensureEmbeddings();
|
|
914
911
|
const store = loadNeuralStore();
|
|
915
912
|
const method = input.method || 'quantize';
|
|
916
913
|
const targetReduction = input.targetSize || 0.5;
|
|
@@ -919,38 +916,25 @@ export const neuralTools = [
|
|
|
919
916
|
return { success: false, error: 'No patterns to compress. Train patterns first with neural_train.' };
|
|
920
917
|
}
|
|
921
918
|
const beforeCount = patterns.length;
|
|
922
|
-
const beforeSize = patterns.reduce((s, p) => s + (p.embedding?.length || 0) * 4, 0); // Float32 = 4 bytes
|
|
923
919
|
if (method === 'quantize') {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}
|
|
940
|
-
saveNeuralStore(store);
|
|
941
|
-
return {
|
|
942
|
-
success: true, _real: true, method,
|
|
943
|
-
embeddingProvider: embeddingServiceName,
|
|
944
|
-
patternsCompressed: totalCompressed,
|
|
945
|
-
compressionRatio: '3.92x (Int8)',
|
|
946
|
-
beforeBytes: beforeSize,
|
|
947
|
-
afterBytes: Math.round(beforeSize / 3.92),
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
catch {
|
|
951
|
-
return { success: false, error: 'Quantization requires memory-initializer. Run `memory init` first.' };
|
|
952
|
-
}
|
|
920
|
+
// Neural-store readers consume `embedding` as a full-precision number[]
|
|
921
|
+
// (search, prediction, deduplication, and optimization). The former
|
|
922
|
+
// implementation discarded the Int8 vector, persisted only metadata,
|
|
923
|
+
// and reported a calculated 3.92x reduction. Refuse until the store has
|
|
924
|
+
// a persisted representation with a reader-side decode contract.
|
|
925
|
+
const storedBytes = readFileSync(getNeuralPath()).byteLength;
|
|
926
|
+
return {
|
|
927
|
+
success: false,
|
|
928
|
+
error: 'Quantized embeddings are not persisted: neural-store readers require full-precision embedding arrays.',
|
|
929
|
+
method,
|
|
930
|
+
patternsCompressed: 0,
|
|
931
|
+
beforeBytes: storedBytes,
|
|
932
|
+
afterBytes: storedBytes,
|
|
933
|
+
compressionRatio: '1.00x (unchanged JSON storage)',
|
|
934
|
+
};
|
|
953
935
|
}
|
|
936
|
+
// Resolve provider only for operations that actually use embeddings.
|
|
937
|
+
await ensureEmbeddings();
|
|
954
938
|
if (method === 'prune') {
|
|
955
939
|
// Prune patterns with low usage count below threshold (targetReduction as min usage)
|
|
956
940
|
const threshold = targetReduction;
|
|
@@ -1136,24 +1120,10 @@ export const neuralTools = [
|
|
|
1136
1120
|
if (duplicatesRemoved > 0)
|
|
1137
1121
|
actions.push(`Removed ${duplicatesRemoved} near-duplicate patterns`);
|
|
1138
1122
|
}
|
|
1139
|
-
// memory / balanced:
|
|
1123
|
+
// memory / balanced: report the unsupported persisted representation
|
|
1124
|
+
// instead of attaching metadata for a discarded Int8 vector.
|
|
1140
1125
|
if (target === 'memory' || target === 'balanced') {
|
|
1141
|
-
|
|
1142
|
-
const { quantizeInt8, getQuantizationStats } = await import('../memory/memory-initializer.js');
|
|
1143
|
-
for (const p of Object.values(store.patterns)) {
|
|
1144
|
-
if (p.embedding && p.embedding.length > 0 && !p._quantized) {
|
|
1145
|
-
const stats = getQuantizationStats(p.embedding);
|
|
1146
|
-
const q = quantizeInt8(p.embedding);
|
|
1147
|
-
p._quantized = { scale: q.scale, zeroPoint: q.zeroPoint, compressionRatio: stats.compressionRatio };
|
|
1148
|
-
patternsQuantized++;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
if (patternsQuantized > 0)
|
|
1152
|
-
actions.push(`Quantized ${patternsQuantized} pattern embeddings (Int8, ~3.92x)`);
|
|
1153
|
-
}
|
|
1154
|
-
catch {
|
|
1155
|
-
actions.push('Quantization skipped (memory-initializer not available)');
|
|
1156
|
-
}
|
|
1126
|
+
actions.push('Quantization skipped: neural-store readers require persisted full-precision embedding arrays.');
|
|
1157
1127
|
}
|
|
1158
1128
|
// accuracy / balanced: prune low-usage, zero-embedding patterns
|
|
1159
1129
|
if (target === 'accuracy' || target === 'balanced') {
|
|
@@ -24,9 +24,11 @@ export const policyTools = [
|
|
|
24
24
|
category: 'security',
|
|
25
25
|
inputSchema: {
|
|
26
26
|
type: 'object',
|
|
27
|
-
properties: {
|
|
27
|
+
properties: {
|
|
28
|
+
full: { type: 'boolean', description: 'Also replay the archived receipts to verify the complete historical ledger, not just the retained hot tail (#3164)' },
|
|
29
|
+
},
|
|
28
30
|
},
|
|
29
|
-
handler: async (
|
|
31
|
+
handler: async (input, context) => {
|
|
30
32
|
const root = typeof context?.projectRoot === 'string' ? context.projectRoot : process.cwd();
|
|
31
33
|
const state = loadPolicyState(root);
|
|
32
34
|
return {
|
|
@@ -39,7 +41,7 @@ export const policyTools = [
|
|
|
39
41
|
approvals: state.approvals.length,
|
|
40
42
|
receipts: state.receipts.length,
|
|
41
43
|
},
|
|
42
|
-
ledger: await verifyPolicyLedger(root),
|
|
44
|
+
ledger: await verifyPolicyLedger(root, { full: input.full === true }),
|
|
43
45
|
};
|
|
44
46
|
},
|
|
45
47
|
},
|
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { resolveTrainingBackend } from '../ruvector/lora-adapter.js';
|
|
18
18
|
import { getProjectCwd } from '../mcp-tools/types.js';
|
|
19
|
+
import { withStatsFileLock } from './stats-file-lock.js';
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// Persistence Configuration
|
|
21
22
|
// ============================================================================
|
|
@@ -664,11 +665,9 @@ function bumpStat(key, n = 1) {
|
|
|
664
665
|
}
|
|
665
666
|
function readStatsFile() {
|
|
666
667
|
try {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
671
|
-
return data && typeof data === 'object' ? data : null;
|
|
668
|
+
// Read-only callers keep their unavailable-on-error contract, while invalid
|
|
669
|
+
// disk values cannot poison local state and a later repaired-file flush.
|
|
670
|
+
return readStatsForWrite(getStatsPath());
|
|
672
671
|
}
|
|
673
672
|
catch {
|
|
674
673
|
return null;
|
|
@@ -701,33 +700,79 @@ function maxTimestamp(a, b) {
|
|
|
701
700
|
return a;
|
|
702
701
|
return Math.max(a, b);
|
|
703
702
|
}
|
|
704
|
-
/**
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
703
|
+
/** Missing files are first use; unreadable or invalid files must not be replaced. */
|
|
704
|
+
function readStatsForWrite(path) {
|
|
705
|
+
let text;
|
|
706
|
+
try {
|
|
707
|
+
text = readFileSync(path, 'utf-8');
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
if (error.code === 'ENOENT')
|
|
711
|
+
return null;
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
let data;
|
|
715
|
+
try {
|
|
716
|
+
data = JSON.parse(text);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
throw Object.assign(new Error('Invalid learning stats JSON'), { code: 'EINVALIDSTATS' });
|
|
720
|
+
}
|
|
721
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
722
|
+
throw Object.assign(new Error('Invalid learning stats object'), { code: 'EINVALIDSTATS' });
|
|
723
|
+
}
|
|
724
|
+
for (const key of ['trajectoriesRecorded', 'patternsLearned', 'signalsProcessed']) {
|
|
725
|
+
const value = data[key];
|
|
726
|
+
// Older files may omit a counter, but present counters must be exact counts.
|
|
727
|
+
if (value === undefined)
|
|
728
|
+
continue;
|
|
729
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
730
|
+
throw Object.assign(new Error(`Invalid learning stats field: ${key}`), { code: 'EINVALIDSTATS' });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const timestamp = data.lastAdaptation;
|
|
734
|
+
if (timestamp != null && (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp < 0)) {
|
|
735
|
+
throw Object.assign(new Error('Invalid learning stats timestamp'), { code: 'EINVALIDSTATS' });
|
|
736
|
+
}
|
|
737
|
+
return data;
|
|
738
|
+
}
|
|
739
|
+
/** Merge pending deltas under the cooperating-writer lock, then adopt committed totals. */
|
|
709
740
|
function savePersistedStats() {
|
|
741
|
+
let statsPath;
|
|
710
742
|
try {
|
|
711
|
-
ensureDataDir();
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
743
|
+
statsPath = join(ensureDataDir(), 'stats.json');
|
|
744
|
+
withStatsFileLock(statsPath, (path) => {
|
|
745
|
+
const disk = readStatsForWrite(path);
|
|
746
|
+
const merged = disk
|
|
747
|
+
? {
|
|
748
|
+
trajectoriesRecorded: (disk.trajectoriesRecorded ?? 0) + pendingDelta.trajectoriesRecorded,
|
|
749
|
+
patternsLearned: (disk.patternsLearned ?? 0) + pendingDelta.patternsLearned,
|
|
750
|
+
signalsProcessed: (disk.signalsProcessed ?? 0) + pendingDelta.signalsProcessed,
|
|
751
|
+
lastAdaptation: maxTimestamp(disk.lastAdaptation ?? null, globalStats.lastAdaptation),
|
|
752
|
+
}
|
|
753
|
+
: { ...globalStats };
|
|
754
|
+
for (const key of ['trajectoriesRecorded', 'patternsLearned', 'signalsProcessed']) {
|
|
755
|
+
if (!Number.isSafeInteger(merged[key]) || merged[key] < 0) {
|
|
756
|
+
throw Object.assign(new Error('Learning stats counter overflow'), { code: 'EINVALIDSTATS' });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (merged.lastAdaptation != null &&
|
|
760
|
+
(!Number.isFinite(merged.lastAdaptation) || merged.lastAdaptation < 0)) {
|
|
761
|
+
throw Object.assign(new Error('Invalid merged learning stats timestamp'), { code: 'EINVALIDSTATS' });
|
|
720
762
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
763
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
764
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2), 'utf-8');
|
|
765
|
+
renameSync(tmp, path);
|
|
766
|
+
// Commit local state before release: a failed release must not replay it.
|
|
767
|
+
globalStats = merged;
|
|
768
|
+
pendingDelta = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0 };
|
|
769
|
+
persistedStatsLoaded = true;
|
|
770
|
+
});
|
|
728
771
|
}
|
|
729
|
-
catch {
|
|
730
|
-
|
|
772
|
+
catch (error) {
|
|
773
|
+
const { code = 'UNKNOWN', path = statsPath || 'neural stats directory' } = error;
|
|
774
|
+
// Do not print file contents, which may be malformed or user-controlled.
|
|
775
|
+
console.warn(`[intelligence] Could not persist learning stats at ${path} (${code}); pending increments retained`);
|
|
731
776
|
}
|
|
732
777
|
}
|
|
733
778
|
/**
|
|
@@ -1015,13 +1060,15 @@ export async function recordTrajectory(steps, verdict) {
|
|
|
1015
1060
|
const avgQuality = verdict === 'success' ? 1.0 : verdict === 'partial' ? 0.5 : 0.0;
|
|
1016
1061
|
ruvllmCoord.recordTrajectory({
|
|
1017
1062
|
steps: enrichedSteps.map(s => ({
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1063
|
+
type: ['query', 'route', 'generate', 'memory', 'feedback'].includes(s.type)
|
|
1064
|
+
? s.type : 'generate',
|
|
1065
|
+
input: s.content,
|
|
1066
|
+
output: '',
|
|
1067
|
+
durationMs: 0,
|
|
1068
|
+
confidence: avgQuality
|
|
1022
1069
|
})),
|
|
1023
1070
|
totalReward: avgQuality,
|
|
1024
|
-
|
|
1071
|
+
outcome: verdict
|
|
1025
1072
|
});
|
|
1026
1073
|
}
|
|
1027
1074
|
catch {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize cooperating local writers without stealing a paused writer's lock.
|
|
3
|
+
* Abnormal termination can leave an orphan: recovery requires confirming that
|
|
4
|
+
* no writer still owns the directory before removing it. Never age-reap it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function withStatsFileLock<T>(file: string, write: (canonicalFile: string) => T): T;
|
|
7
|
+
//# sourceMappingURL=stats-file-lock.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, realpathSync, rmdirSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Serialize cooperating local writers without stealing a paused writer's lock.
|
|
5
|
+
* Abnormal termination can leave an orphan: recovery requires confirming that
|
|
6
|
+
* no writer still owns the directory before removing it. Never age-reap it.
|
|
7
|
+
*/
|
|
8
|
+
export function withStatsFileLock(file, write) {
|
|
9
|
+
const canonicalFile = join(realpathSync(dirname(file)), basename(file));
|
|
10
|
+
const lock = `${canonicalFile}.lock`;
|
|
11
|
+
const deadline = performance.now() + 1000;
|
|
12
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
13
|
+
for (;;) {
|
|
14
|
+
try {
|
|
15
|
+
mkdirSync(lock, { mode: 0o700 });
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code !== 'EEXIST')
|
|
20
|
+
throw error;
|
|
21
|
+
const remaining = deadline - performance.now();
|
|
22
|
+
if (remaining <= 0) {
|
|
23
|
+
throw Object.assign(new Error('Learning stats lock is busy'), { code: 'ELOCKED', path: lock });
|
|
24
|
+
}
|
|
25
|
+
Atomics.wait(sleeper, 0, 0, Math.min(10, remaining));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
// Only remove our empty directory. Never recursively remove foreign data.
|
|
30
|
+
rmdirSync(lock);
|
|
31
|
+
};
|
|
32
|
+
const onExit = () => {
|
|
33
|
+
try {
|
|
34
|
+
cleanup();
|
|
35
|
+
}
|
|
36
|
+
catch { /* abnormal exits may require deliberate recovery */ }
|
|
37
|
+
};
|
|
38
|
+
process.once('exit', onExit);
|
|
39
|
+
try {
|
|
40
|
+
return write(canonicalFile);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
process.removeListener('exit', onExit);
|
|
44
|
+
try {
|
|
45
|
+
cleanup();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const code = error.code || 'UNKNOWN';
|
|
49
|
+
console.warn(`[intelligence] Could not release learning stats lock at ${lock} (${code}); verify ownership before recovery`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=stats-file-lock.js.map
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { type VerifyTaskKind } from '../ruvector/output-verifier.js';
|
|
38
38
|
import { FableHarness, type ReflectItem, type ReflectResult } from './fable-harness.js';
|
|
39
|
-
export declare const MH_DARWIN_PIN = "0.
|
|
39
|
+
export declare const MH_DARWIN_PIN = "0.12.0";
|
|
40
40
|
export type ResolvedProvenance = 'oracle:test-exec' | 'judge:fable' | 'proxy:structural';
|
|
41
41
|
/** SWE-bench-shaped / bench-suite-mapped test spec that Tier 1 can execute. */
|
|
42
42
|
export interface TestSpec {
|
|
@@ -43,9 +43,9 @@ import { FableHarness, } from './fable-harness.js';
|
|
|
43
43
|
// behavior mid-run (the #142 pin-drift failure mode). Pin it, and let
|
|
44
44
|
// scripts/check-metaharness-pins.mjs watch this constant for drift. Kept in
|
|
45
45
|
// lock-step with the optionalDependencies pin in package.json and the plugin
|
|
46
|
-
// darwin cache (versioned by the plugin's own `~0.
|
|
46
|
+
// darwin cache (versioned by the plugin's own `~0.12.0` pin in _darwin.mjs;
|
|
47
47
|
// both are checked by scripts/check-metaharness-pins.mjs — #3366).
|
|
48
|
-
export const MH_DARWIN_PIN = '0.
|
|
48
|
+
export const MH_DARWIN_PIN = '0.12.0';
|
|
49
49
|
// ── Public API ───────────────────────────────────────────────────────────
|
|
50
50
|
/**
|
|
51
51
|
* Label each trajectory with `resolved` + honest provenance, trying the tiers
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const ATOMIC_JSON_WRITER: unique symbol;
|
|
2
|
+
export interface AtomicJsonSource {
|
|
3
|
+
[ATOMIC_JSON_WRITER](fd: number): void;
|
|
4
|
+
}
|
|
5
|
+
/** Supports property order/whitespace from ordinary JSON writers. Unknown
|
|
6
|
+
* root values remain bounded and syntax-checked; duplicate known keys fail. */
|
|
7
|
+
export declare function anchorEntries(file: string): Generator<unknown>;
|
|
8
|
+
/** Serialization callback runs inside the runtime's existing durable atomic
|
|
9
|
+
* writer. Failure removes the temporary file; no partial log is published. */
|
|
10
|
+
export declare function appendedAnchorLog(file: string | undefined, next: unknown, validate: (value: unknown, sequence: number, previous: string | null) => value is {
|
|
11
|
+
hash: string;
|
|
12
|
+
}): AtomicJsonSource;
|
|
13
|
+
//# sourceMappingURL=policy-anchor-log.d.ts.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/** Streaming reader/writer for the existing version-1 anchor JSON object.
|
|
2
|
+
* Every entry is still parsed and authenticated by the caller. This bounds
|
|
3
|
+
* memory without weakening historical validation or changing the log format.
|
|
4
|
+
*/
|
|
5
|
+
import { closeSync, constants, fstatSync, openSync, readSync, writeSync } from 'node:fs';
|
|
6
|
+
import { TextDecoder } from 'node:util';
|
|
7
|
+
export const ATOMIC_JSON_WRITER = Symbol('policy-atomic-json-writer');
|
|
8
|
+
const CHUNK_BYTES = 64 * 1024;
|
|
9
|
+
const VALUE_LIMIT = 16 * 1024 * 1024;
|
|
10
|
+
const STRING_STRUCTURE = /["\\]/g;
|
|
11
|
+
const VALUE_STRUCTURE = /["\\{}\[\]]/g;
|
|
12
|
+
class JsonReader {
|
|
13
|
+
fd;
|
|
14
|
+
buffer = Buffer.allocUnsafe(CHUNK_BYTES);
|
|
15
|
+
decoder = new TextDecoder('utf-8', { fatal: true });
|
|
16
|
+
text = '';
|
|
17
|
+
cursor = 0;
|
|
18
|
+
position = 0;
|
|
19
|
+
ended = false;
|
|
20
|
+
constructor(file) {
|
|
21
|
+
this.fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
22
|
+
try {
|
|
23
|
+
if (!fstatSync(this.fd).isFile())
|
|
24
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
closeSync(this.fd);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
close() { closeSync(this.fd); }
|
|
32
|
+
peek() {
|
|
33
|
+
while (this.cursor >= this.text.length && !this.ended) {
|
|
34
|
+
const count = readSync(this.fd, this.buffer, 0, this.buffer.length, this.position);
|
|
35
|
+
this.position += count;
|
|
36
|
+
this.text = this.decoder.decode(this.buffer.subarray(0, count), { stream: count !== 0 });
|
|
37
|
+
this.cursor = 0;
|
|
38
|
+
if (!count)
|
|
39
|
+
this.ended = true;
|
|
40
|
+
}
|
|
41
|
+
return this.text[this.cursor] ?? '';
|
|
42
|
+
}
|
|
43
|
+
next() { const character = this.peek(); if (character)
|
|
44
|
+
this.cursor++; return character; }
|
|
45
|
+
whitespace() { while (/[ \t\r\n]/.test(this.peek()) && this.peek())
|
|
46
|
+
this.next(); }
|
|
47
|
+
expect(character) { this.whitespace(); if (this.next() !== character)
|
|
48
|
+
throw new Error('policy-anchor-log-corrupt'); }
|
|
49
|
+
value() {
|
|
50
|
+
this.whitespace();
|
|
51
|
+
const first = this.peek();
|
|
52
|
+
if (!first)
|
|
53
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
54
|
+
const compound = first === '{' || first === '[';
|
|
55
|
+
const string = first === '"';
|
|
56
|
+
const parts = [];
|
|
57
|
+
let bytes = 0;
|
|
58
|
+
let depth = 0;
|
|
59
|
+
let quoted = false;
|
|
60
|
+
let escaped = false;
|
|
61
|
+
for (;;) {
|
|
62
|
+
if (!this.peek()) {
|
|
63
|
+
if (quoted || depth !== 0 || !parts.length)
|
|
64
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const start = this.cursor;
|
|
68
|
+
let end = start;
|
|
69
|
+
let complete = false;
|
|
70
|
+
// Scan the available segment directly. Avoid per-character method,
|
|
71
|
+
// regular-expression and string-concatenation overhead on mature logs.
|
|
72
|
+
for (; end < this.text.length; end++) {
|
|
73
|
+
if (!escaped && (quoted || compound)) {
|
|
74
|
+
const markers = quoted ? STRING_STRUCTURE : VALUE_STRUCTURE;
|
|
75
|
+
markers.lastIndex = end;
|
|
76
|
+
const next = markers.exec(this.text);
|
|
77
|
+
if (!next) {
|
|
78
|
+
end = this.text.length;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
end = next.index;
|
|
82
|
+
}
|
|
83
|
+
const code = this.text.charCodeAt(end);
|
|
84
|
+
if (!quoted && depth === 0 && (parts.length > 0 || end > start) && !compound && !string
|
|
85
|
+
&& (code === 32 || code === 9 || code === 10 || code === 13 || code === 44 || code === 125 || code === 93)) {
|
|
86
|
+
complete = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (quoted) {
|
|
90
|
+
if (escaped)
|
|
91
|
+
escaped = false;
|
|
92
|
+
else if (code === 92)
|
|
93
|
+
escaped = true;
|
|
94
|
+
else if (code === 34) {
|
|
95
|
+
quoted = false;
|
|
96
|
+
if (string && depth === 0) {
|
|
97
|
+
end++;
|
|
98
|
+
complete = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (code === 34)
|
|
104
|
+
quoted = true;
|
|
105
|
+
else if (code === 123 || code === 91)
|
|
106
|
+
depth++;
|
|
107
|
+
else if (code === 125 || code === 93) {
|
|
108
|
+
depth--;
|
|
109
|
+
if (depth < 0)
|
|
110
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
111
|
+
if (compound && depth === 0) {
|
|
112
|
+
end++;
|
|
113
|
+
complete = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const segment = this.text.slice(start, end);
|
|
119
|
+
bytes += Buffer.byteLength(segment);
|
|
120
|
+
if (bytes > VALUE_LIMIT)
|
|
121
|
+
throw new Error('policy-anchor-log-record-too-large');
|
|
122
|
+
if (segment)
|
|
123
|
+
parts.push(segment);
|
|
124
|
+
this.cursor = end;
|
|
125
|
+
if (complete)
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
// JSON.parse validates the complete bounded token's grammar, including
|
|
129
|
+
// bracket kinds, escapes, control characters and scalar spellings.
|
|
130
|
+
return JSON.parse(parts.join(''));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Supports property order/whitespace from ordinary JSON writers. Unknown
|
|
134
|
+
* root values remain bounded and syntax-checked; duplicate known keys fail. */
|
|
135
|
+
export function* anchorEntries(file) {
|
|
136
|
+
const reader = new JsonReader(file);
|
|
137
|
+
let version;
|
|
138
|
+
let seenVersion = false;
|
|
139
|
+
let seenEntries = false;
|
|
140
|
+
try {
|
|
141
|
+
reader.expect('{');
|
|
142
|
+
reader.whitespace();
|
|
143
|
+
if (reader.peek() === '}')
|
|
144
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
145
|
+
for (;;) {
|
|
146
|
+
const key = reader.value();
|
|
147
|
+
if (typeof key !== 'string')
|
|
148
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
149
|
+
reader.expect(':');
|
|
150
|
+
if (key === 'entries') {
|
|
151
|
+
if (seenEntries)
|
|
152
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
153
|
+
seenEntries = true;
|
|
154
|
+
reader.expect('[');
|
|
155
|
+
reader.whitespace();
|
|
156
|
+
if (reader.peek() !== ']') {
|
|
157
|
+
for (;;) {
|
|
158
|
+
yield reader.value();
|
|
159
|
+
reader.whitespace();
|
|
160
|
+
if (reader.peek() === ']')
|
|
161
|
+
break;
|
|
162
|
+
reader.expect(',');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
reader.expect(']');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const value = reader.value();
|
|
169
|
+
if (key === 'version') {
|
|
170
|
+
if (seenVersion)
|
|
171
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
172
|
+
seenVersion = true;
|
|
173
|
+
version = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
reader.whitespace();
|
|
177
|
+
if (reader.peek() === '}')
|
|
178
|
+
break;
|
|
179
|
+
reader.expect(',');
|
|
180
|
+
}
|
|
181
|
+
reader.expect('}');
|
|
182
|
+
reader.whitespace();
|
|
183
|
+
if (reader.peek() || !seenEntries || !seenVersion || version !== 1)
|
|
184
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
reader.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function writeText(fd, text) {
|
|
191
|
+
const bytes = Buffer.from(text);
|
|
192
|
+
let count = 0;
|
|
193
|
+
while (count < bytes.length) {
|
|
194
|
+
const next = writeSync(fd, bytes, count, bytes.length - count);
|
|
195
|
+
if (!next)
|
|
196
|
+
throw new Error('policy-anchor-log-short-write');
|
|
197
|
+
count += next;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Serialization callback runs inside the runtime's existing durable atomic
|
|
201
|
+
* writer. Failure removes the temporary file; no partial log is published. */
|
|
202
|
+
export function appendedAnchorLog(file, next, validate) {
|
|
203
|
+
return { [ATOMIC_JSON_WRITER](fd) {
|
|
204
|
+
let pending = '';
|
|
205
|
+
let pendingBytes = 0;
|
|
206
|
+
const flush = () => { if (pendingBytes)
|
|
207
|
+
writeText(fd, pending); pending = ''; pendingBytes = 0; };
|
|
208
|
+
const emit = (text) => {
|
|
209
|
+
const bytes = Buffer.byteLength(text);
|
|
210
|
+
if (pendingBytes + bytes > CHUNK_BYTES)
|
|
211
|
+
flush();
|
|
212
|
+
if (bytes >= CHUNK_BYTES)
|
|
213
|
+
writeText(fd, text);
|
|
214
|
+
else {
|
|
215
|
+
pending += text;
|
|
216
|
+
pendingBytes += bytes;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
emit('{"version":1,"entries":[\n');
|
|
220
|
+
let count = 0;
|
|
221
|
+
let previous = null;
|
|
222
|
+
if (file) {
|
|
223
|
+
for (const entry of anchorEntries(file)) {
|
|
224
|
+
if (!validate(entry, count, previous))
|
|
225
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
226
|
+
if (count)
|
|
227
|
+
emit(',\n');
|
|
228
|
+
emit(JSON.stringify(entry));
|
|
229
|
+
count++;
|
|
230
|
+
previous = entry.hash;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!validate(next, count, previous))
|
|
234
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
235
|
+
if (count)
|
|
236
|
+
emit(',\n');
|
|
237
|
+
emit(JSON.stringify(next));
|
|
238
|
+
emit('\n]}\n');
|
|
239
|
+
flush();
|
|
240
|
+
} };
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=policy-anchor-log.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Fixed-chunk reverse archive scan used by the synchronous anchor gate.
|
|
2
|
+
* Memory follows the largest single line, never the entire archive/window.
|
|
3
|
+
*/
|
|
4
|
+
import { readSync } from 'node:fs';
|
|
5
|
+
const CHUNK_BYTES = 64 * 1024;
|
|
6
|
+
export function* reverseArchiveRecords(fd, size, limit) {
|
|
7
|
+
let position = size;
|
|
8
|
+
let parts = [];
|
|
9
|
+
let partBytes = 0;
|
|
10
|
+
let terminated = false;
|
|
11
|
+
while (position > 0) {
|
|
12
|
+
const count = Math.min(CHUNK_BYTES, position);
|
|
13
|
+
position -= count;
|
|
14
|
+
const chunk = Buffer.allocUnsafe(count);
|
|
15
|
+
let read = 0;
|
|
16
|
+
while (read < count) {
|
|
17
|
+
const next = readSync(fd, chunk, read, count - read, position + read);
|
|
18
|
+
if (!next)
|
|
19
|
+
throw new Error('policy-ledger-archive-changed');
|
|
20
|
+
read += next;
|
|
21
|
+
}
|
|
22
|
+
let end = chunk.length;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const newline = chunk.lastIndexOf(10, end - 1);
|
|
25
|
+
if (newline < 0)
|
|
26
|
+
break;
|
|
27
|
+
const piece = chunk.subarray(newline + 1, end);
|
|
28
|
+
if (piece.length + partBytes > limit)
|
|
29
|
+
throw new Error(`policy-ledger-archive-record-too-large:${limit}`);
|
|
30
|
+
const bytes = parts.length ? Buffer.concat([piece, ...parts], piece.length + partBytes) : Buffer.from(piece);
|
|
31
|
+
yield { bytes, offset: position + newline + 1, terminated };
|
|
32
|
+
terminated = true;
|
|
33
|
+
parts = [];
|
|
34
|
+
partBytes = 0;
|
|
35
|
+
end = newline;
|
|
36
|
+
if (end === 0)
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (end > 0) {
|
|
40
|
+
if (partBytes + end > limit)
|
|
41
|
+
throw new Error(`policy-ledger-archive-record-too-large:${limit}`);
|
|
42
|
+
const piece = Buffer.from(chunk.subarray(0, end));
|
|
43
|
+
parts.unshift(piece);
|
|
44
|
+
partBytes += piece.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (partBytes > 0)
|
|
48
|
+
yield { bytes: Buffer.concat(parts, partBytes), offset: 0, terminated };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=policy-archive-scan.js.map
|