sneakoscope 2.0.12 → 2.0.14
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/README.md +5 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/core/agents/agent-orchestrator.js +11 -4
- package/dist/core/agents/agent-output-validator.js +1 -1
- package/dist/core/codex-control/codex-fake-sdk-adapter.js +3 -3
- package/dist/core/codex-control/codex-sdk-adapter.js +10 -0
- package/dist/core/codex-control/codex-task-runner.js +4 -2
- package/dist/core/codex-control/gpt-final-review-schema.js +61 -14
- package/dist/core/commands/naruto-command.js +1 -0
- package/dist/core/commands/research-command.js +112 -19
- package/dist/core/fsx.js +1 -1
- package/dist/core/naruto/naruto-real-worker-child.js +11 -3
- package/dist/core/naruto/naruto-real-worker-runtime.js +4 -0
- package/dist/core/pipeline/final-gpt-patch-stage.js +20 -3
- package/dist/core/research/claim-evidence-matrix.js +160 -0
- package/dist/core/research/experiment-plan.js +53 -0
- package/dist/core/research/falsification.js +18 -0
- package/dist/core/research/implementation-blueprint-densifier.js +124 -0
- package/dist/core/research/implementation-blueprint-markdown.js +31 -0
- package/dist/core/research/implementation-blueprint.js +66 -0
- package/dist/core/research/replication-pack.js +50 -0
- package/dist/core/research/research-claim-builder.js +114 -0
- package/dist/core/research/research-cycle-runner.js +129 -0
- package/dist/core/research/research-final-reviewer.js +212 -0
- package/dist/core/research/research-handoff.js +51 -0
- package/dist/core/research/research-prompt-contract.js +24 -0
- package/dist/core/research/research-quality-contract.js +61 -0
- package/dist/core/research/research-report-quality.js +67 -0
- package/dist/core/research/research-source-ledger-merge.js +186 -0
- package/dist/core/research/research-source-shards.js +176 -0
- package/dist/core/research/research-stage-runner.js +515 -0
- package/dist/core/research/research-work-graph.js +166 -0
- package/dist/core/research/source-quality-report.js +94 -0
- package/dist/core/research.js +356 -44
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +5 -3
- package/dist/core/zellij/zellij-slot-pane-renderer.js +259 -16
- package/dist/scripts/codex-sdk-research-pipeline-check.js +44 -5
- package/dist/scripts/packlist-performance-check.js +1 -1
- package/dist/scripts/release-dag-full-coverage-check.js +14 -1
- package/dist/scripts/release-parallel-speed-budget-check.js +7 -2
- package/dist/scripts/research-blueprint-densifier-check.js +21 -0
- package/dist/scripts/research-claim-builder-check.js +19 -0
- package/dist/scripts/research-complete-package-fixture-check.js +23 -0
- package/dist/scripts/research-final-reviewer-blackbox.js +22 -0
- package/dist/scripts/research-parallel-source-shards-check.js +22 -0
- package/dist/scripts/research-quality-gate-check.js +111 -0
- package/dist/scripts/research-real-cycle-no-legacy-final-md-check.js +14 -0
- package/dist/scripts/research-short-report-rejection-check.js +46 -0
- package/dist/scripts/research-source-ledger-merge-check.js +26 -0
- package/dist/scripts/research-stage-cycle-runtime-blackbox.js +24 -0
- package/dist/scripts/zellij-slot-column-anchor-check.js +26 -5
- package/dist/scripts/zellij-slot-pane-renderer-check.js +73 -5
- package/package.json +28 -1
- package/schemas/codex/agent-result.schema.json +1 -1
- package/schemas/research/claim-evidence-matrix.schema.json +37 -0
- package/schemas/research/experiment-plan.schema.json +17 -0
- package/schemas/research/implementation-blueprint.schema.json +30 -0
- package/schemas/research/replication-pack.schema.json +17 -0
- package/schemas/research/research-final-review.schema.json +16 -0
- package/schemas/research/research-quality-contract.schema.json +37 -0
- package/schemas/research/research-source-shard.schema.json +46 -0
- package/schemas/research/source-quality-report.schema.json +18 -0
- package/dist/build-manifest.json +0 -1168
- package/dist/scripts/release-readiness-report.js +0 -1146
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/permission-request.command.input.schema.json +0 -61
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/permission-request.command.output.schema.json +0 -103
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-compact.command.input.schema.json +0 -52
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-compact.command.output.schema.json +0 -24
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-tool-use.command.input.schema.json +0 -67
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-tool-use.command.output.schema.json +0 -84
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-compact.command.input.schema.json +0 -52
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-compact.command.output.schema.json +0 -24
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-tool-use.command.input.schema.json +0 -65
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-tool-use.command.output.schema.json +0 -105
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/session-start.command.input.schema.json +0 -59
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/session-start.command.output.schema.json +0 -63
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/snapshot-metadata.json +0 -31
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/stop.command.input.schema.json +0 -63
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/stop.command.output.schema.json +0 -45
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/user-prompt-submit.command.input.schema.json +0 -59
- package/dist/vendor/openai-codex/rust-v0.131.0/hooks/user-prompt-submit.command.output.schema.json +0 -81
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { normalizeClaimEvidenceMatrix } from './claim-evidence-matrix.js';
|
|
2
|
+
export async function buildClaimEvidenceMatrixFromSourceShards(input) {
|
|
3
|
+
const sources = [
|
|
4
|
+
...(Array.isArray(input.sourceLedger?.sources) ? input.sourceLedger.sources : []),
|
|
5
|
+
...(Array.isArray(input.sourceLedger?.counterevidence_sources) ? input.sourceLedger.counterevidence_sources : [])
|
|
6
|
+
];
|
|
7
|
+
const noveltyEntries = Array.isArray(input.noveltyLedger?.entries) ? input.noveltyLedger.entries : [];
|
|
8
|
+
const candidates = new Map();
|
|
9
|
+
for (const entry of noveltyEntries) {
|
|
10
|
+
const id = String(entry?.id || '').trim();
|
|
11
|
+
if (!id)
|
|
12
|
+
continue;
|
|
13
|
+
candidates.set(id, {
|
|
14
|
+
id,
|
|
15
|
+
claim: String(entry?.claim || entry?.title || id),
|
|
16
|
+
claim_type: entry?.type === 'implementation_guidance' ? 'implementation_guidance' : 'hypothesis',
|
|
17
|
+
importance: candidates.size < 2 ? 'critical' : 'high',
|
|
18
|
+
source_ids: normalizeStringList(entry?.source_ids || entry?.evidence),
|
|
19
|
+
counterevidence_ids: normalizeStringList(entry?.counterevidence_ids || entry?.falsifiers),
|
|
20
|
+
test_or_probe: String(entry?.next_experiment || entry?.test_or_probe || '').trim()
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
for (const source of sources) {
|
|
24
|
+
const claimIds = normalizeStringList(source?.claim_ids);
|
|
25
|
+
if (claimIds.length) {
|
|
26
|
+
for (const claimId of claimIds) {
|
|
27
|
+
const existing = candidates.get(claimId) || {
|
|
28
|
+
id: claimId,
|
|
29
|
+
claim: claimTextFromSource(source, claimId),
|
|
30
|
+
claim_type: 'inference',
|
|
31
|
+
importance: candidates.size < 2 ? 'critical' : candidates.size < 8 ? 'high' : 'medium',
|
|
32
|
+
source_ids: [],
|
|
33
|
+
counterevidence_ids: [],
|
|
34
|
+
test_or_probe: ''
|
|
35
|
+
};
|
|
36
|
+
if (source?.stance === 'undermines')
|
|
37
|
+
existing.counterevidence_ids = [...new Set([...(existing.counterevidence_ids || []), source.id])];
|
|
38
|
+
else
|
|
39
|
+
existing.source_ids = [...new Set([...(existing.source_ids || []), source.id])];
|
|
40
|
+
if (!existing.test_or_probe)
|
|
41
|
+
existing.test_or_probe = `Probe ${claimId} against supporting and undermining source layers.`;
|
|
42
|
+
candidates.set(claimId, existing);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (String(source?.notes || '').trim()) {
|
|
46
|
+
const id = `hypothesis-${candidates.size + 1}`;
|
|
47
|
+
candidates.set(id, {
|
|
48
|
+
id,
|
|
49
|
+
claim: String(source.notes).slice(0, 240),
|
|
50
|
+
claim_type: 'hypothesis',
|
|
51
|
+
importance: 'medium',
|
|
52
|
+
source_ids: source?.stance === 'undermines' ? [] : [source.id].filter(Boolean),
|
|
53
|
+
counterevidence_ids: source?.stance === 'undermines' ? [source.id].filter(Boolean) : [],
|
|
54
|
+
test_or_probe: `Turn ${source.id || id} notes into a decisive source-backed probe.`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const falsificationCounterIds = new Set((Array.isArray(input.falsificationLedger?.cases) ? input.falsificationLedger.cases : [])
|
|
59
|
+
.flatMap((row) => [...normalizeStringList(row?.counterevidence_source_ids), ...normalizeStringList(row?.source_ids)]));
|
|
60
|
+
const claims = [...candidates.values()].slice(0, Math.max(8, candidates.size)).map((candidate, index) => {
|
|
61
|
+
const sourceIds = normalizeStringList(candidate.source_ids).filter((id) => sourceById(sources, id));
|
|
62
|
+
const counterIds = normalizeStringList(candidate.counterevidence_ids).filter((id) => sourceById(sources, id) || falsificationCounterIds.has(id));
|
|
63
|
+
const layers = sourceLayersForSourceIds(sources, [...sourceIds, ...counterIds]);
|
|
64
|
+
return {
|
|
65
|
+
id: candidate.id,
|
|
66
|
+
claim: candidate.claim,
|
|
67
|
+
claim_type: candidate.claim_type,
|
|
68
|
+
importance: candidate.importance || (index < 2 ? 'critical' : 'high'),
|
|
69
|
+
source_ids: sourceIds,
|
|
70
|
+
local_evidence_ids: sources.filter((source) => source.layer === 'local_project_evidence' && normalizeStringList(source.claim_ids).includes(candidate.id)).map((source) => source.id),
|
|
71
|
+
counterevidence_ids: counterIds,
|
|
72
|
+
triangulation: {
|
|
73
|
+
source_layers: layers,
|
|
74
|
+
independent_confirmation_count: layers.length,
|
|
75
|
+
conflicts: counterIds.length ? [`counterevidence:${counterIds.join(',')}`] : []
|
|
76
|
+
},
|
|
77
|
+
confidence: layers.length >= 3 && counterIds.length ? 'high' : layers.length >= 2 ? 'medium' : 'low',
|
|
78
|
+
falsifiable: true,
|
|
79
|
+
test_or_probe: candidate.test_or_probe || `Run a source-layer replication probe for ${candidate.id}.`
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
const unsupported = claims
|
|
83
|
+
.filter((claim) => {
|
|
84
|
+
const important = claim.importance === 'high' || claim.importance === 'critical';
|
|
85
|
+
return important && (claim.triangulation.source_layers.length < 2 || (claim.importance === 'critical' && claim.counterevidence_ids.length === 0));
|
|
86
|
+
})
|
|
87
|
+
.map((claim) => claim.id);
|
|
88
|
+
return normalizeClaimEvidenceMatrix({
|
|
89
|
+
schema: 'sks.claim-evidence-matrix.v1',
|
|
90
|
+
mission_id: input.plan?.mission_id || '',
|
|
91
|
+
claims,
|
|
92
|
+
key_claim_ids: claims.slice(0, 8).map((claim) => claim.id),
|
|
93
|
+
unsupported_claims: unsupported,
|
|
94
|
+
triangulated_claim_count: claims.filter((claim) => claim.triangulation.source_layers.length >= 2).length,
|
|
95
|
+
blockers: unsupported.map((id) => `unsupported_important_claim:${id}`)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function claimTextFromSource(source, claimId) {
|
|
99
|
+
const notes = String(source?.notes || '').trim();
|
|
100
|
+
if (notes)
|
|
101
|
+
return `${claimId}: ${notes.slice(0, 220)}`;
|
|
102
|
+
return `${claimId}: Evidence row ${source?.id || 'unknown'} contributes to this research claim.`;
|
|
103
|
+
}
|
|
104
|
+
function sourceById(sources, id) {
|
|
105
|
+
return sources.find((source) => String(source?.id || '') === id) || null;
|
|
106
|
+
}
|
|
107
|
+
function sourceLayersForSourceIds(sources, ids) {
|
|
108
|
+
const idSet = new Set(ids);
|
|
109
|
+
return [...new Set(sources.filter((source) => idSet.has(String(source?.id || ''))).map((source) => String(source?.layer || '')).filter(Boolean))];
|
|
110
|
+
}
|
|
111
|
+
function normalizeStringList(value) {
|
|
112
|
+
return [...new Set((Array.isArray(value) ? value : value == null ? [] : [value]).map((item) => String(item || '').trim()).filter(Boolean))];
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=research-claim-builder.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { appendJsonlBounded, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { runResearchStage } from './research-stage-runner.js';
|
|
4
|
+
export async function runResearchCycle(inputOrDir, legacyGraph = null, legacyOpts = {}) {
|
|
5
|
+
const input = typeof inputOrDir === 'string'
|
|
6
|
+
? {
|
|
7
|
+
root: process.cwd(),
|
|
8
|
+
dir: inputOrDir,
|
|
9
|
+
plan: null,
|
|
10
|
+
graph: legacyGraph,
|
|
11
|
+
cycle: Number(legacyOpts.cycle || 0),
|
|
12
|
+
backend: legacyOpts.mock ? 'mock' : 'deterministic',
|
|
13
|
+
timeoutMs: Number(legacyOpts.timeoutMs || 120000),
|
|
14
|
+
maxParallelStages: Number(legacyOpts.maxParallelStages || legacyOpts.maxParallel || 4),
|
|
15
|
+
mock: legacyOpts.mock === true
|
|
16
|
+
}
|
|
17
|
+
: inputOrDir;
|
|
18
|
+
const startedAt = nowIso();
|
|
19
|
+
const stages = normalizeStages(input.graph);
|
|
20
|
+
const pending = new Map(stages.map((stage) => [String(stage.id), stage]));
|
|
21
|
+
const completed = new Map();
|
|
22
|
+
const running = new Map();
|
|
23
|
+
const blockers = [];
|
|
24
|
+
const maxParallel = Math.max(1, Math.min(16, Number(input.maxParallelStages || 4)));
|
|
25
|
+
let maxObservedParallel = 0;
|
|
26
|
+
while (pending.size || running.size) {
|
|
27
|
+
const ready = readyStages([...pending.values()], completed);
|
|
28
|
+
while (running.size < maxParallel && ready.length) {
|
|
29
|
+
const stage = ready.shift();
|
|
30
|
+
if (!stage)
|
|
31
|
+
break;
|
|
32
|
+
pending.delete(String(stage.id));
|
|
33
|
+
const promise = runResearchStage({ ...input, stage }).catch((err) => failureStage(input, stage, err));
|
|
34
|
+
running.set(String(stage.id), promise);
|
|
35
|
+
maxObservedParallel = Math.max(maxObservedParallel, running.size);
|
|
36
|
+
}
|
|
37
|
+
if (!running.size) {
|
|
38
|
+
const blockedIds = [...pending.keys()];
|
|
39
|
+
blockers.push(...blockedIds.map((id) => `stage_dependencies_unresolved:${id}`));
|
|
40
|
+
for (const stage of pending.values()) {
|
|
41
|
+
const failed = failureStage(input, stage, new Error(`dependencies unresolved: ${(stage.dependencies || []).join(',')}`));
|
|
42
|
+
completed.set(String(stage.id), failed);
|
|
43
|
+
}
|
|
44
|
+
pending.clear();
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const done = await Promise.race([...running.entries()].map(async ([id, promise]) => ({ id, result: await promise })));
|
|
48
|
+
running.delete(done.id);
|
|
49
|
+
completed.set(done.id, done.result);
|
|
50
|
+
if (done.result.status !== 'passed' && pendingStageRequired(stages.find((stage) => String(stage.id) === done.id))) {
|
|
51
|
+
blockers.push(...(done.result.blockers.length ? done.result.blockers.map((blocker) => `${done.id}:${blocker}`) : [`${done.id}:stage_not_passed`]));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const stageResults = [...completed.values()];
|
|
55
|
+
const record = {
|
|
56
|
+
schema: 'sks.research-cycle-runner.v2',
|
|
57
|
+
cycle: input.cycle,
|
|
58
|
+
readonly: true,
|
|
59
|
+
started_at: startedAt,
|
|
60
|
+
completed_at: nowIso(),
|
|
61
|
+
stage_count: stageResults.length,
|
|
62
|
+
status: blockers.length ? 'blocked' : 'passed',
|
|
63
|
+
blockers: [...new Set(blockers)],
|
|
64
|
+
stages: stageResults.map((stage) => stage.stage_id),
|
|
65
|
+
stage_results: stageResults,
|
|
66
|
+
parallelism: {
|
|
67
|
+
max_parallel_stages: maxParallel,
|
|
68
|
+
max_observed_parallel: maxObservedParallel,
|
|
69
|
+
stage_count: stageResults.length,
|
|
70
|
+
critical_path_length: criticalPathLength(stages)
|
|
71
|
+
},
|
|
72
|
+
legacy_final_md_loop: false
|
|
73
|
+
};
|
|
74
|
+
await writeJsonAtomic(path.join(input.dir, 'research', `cycle-${input.cycle}`, 'research-cycle-runner.json'), record);
|
|
75
|
+
await writeJsonAtomic(path.join(input.dir, 'research-cycle-runner.json'), record);
|
|
76
|
+
await appendJsonlBounded(path.join(input.dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle_runner.completed', cycle: record.cycle, stage_count: record.stage_count, status: record.status, max_observed_parallel: maxObservedParallel });
|
|
77
|
+
return record;
|
|
78
|
+
}
|
|
79
|
+
export function readyStages(pending, completed) {
|
|
80
|
+
return pending.filter((stage) => (Array.isArray(stage.dependencies) ? stage.dependencies : []).every((id) => completed.has(String(id))));
|
|
81
|
+
}
|
|
82
|
+
function normalizeStages(graph) {
|
|
83
|
+
const stages = Array.isArray(graph?.work_items) ? graph.work_items : [];
|
|
84
|
+
return stages.map((stage, index) => ({
|
|
85
|
+
...stage,
|
|
86
|
+
id: String(stage?.id || `research-stage-${index + 1}`),
|
|
87
|
+
dependencies: Array.isArray(stage?.dependencies) ? stage.dependencies.map(String) : []
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function pendingStageRequired(stage) {
|
|
91
|
+
return stage?.required !== false;
|
|
92
|
+
}
|
|
93
|
+
function failureStage(input, stage, err) {
|
|
94
|
+
const ts = nowIso();
|
|
95
|
+
return {
|
|
96
|
+
schema: 'sks.research-stage-result.v1',
|
|
97
|
+
mission_id: String(input.plan?.mission_id || ''),
|
|
98
|
+
cycle: input.cycle,
|
|
99
|
+
stage_id: String(stage?.id || 'unknown'),
|
|
100
|
+
stage_kind: 'verification',
|
|
101
|
+
status: 'failed',
|
|
102
|
+
started_at: ts,
|
|
103
|
+
completed_at: ts,
|
|
104
|
+
input_artifacts: [],
|
|
105
|
+
output_artifacts: [],
|
|
106
|
+
backend: input.backend,
|
|
107
|
+
worker_result_path: null,
|
|
108
|
+
blockers: [err instanceof Error ? err.message : String(err)],
|
|
109
|
+
metrics: {}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function criticalPathLength(stages) {
|
|
113
|
+
const byId = new Map(stages.map((stage) => [String(stage.id), stage]));
|
|
114
|
+
const memo = new Map();
|
|
115
|
+
const visit = (id, seen = new Set()) => {
|
|
116
|
+
if (memo.has(id))
|
|
117
|
+
return memo.get(id);
|
|
118
|
+
if (seen.has(id))
|
|
119
|
+
return 1;
|
|
120
|
+
seen.add(id);
|
|
121
|
+
const stage = byId.get(id);
|
|
122
|
+
const deps = Array.isArray(stage?.dependencies) ? stage.dependencies.map(String) : [];
|
|
123
|
+
const value = 1 + (deps.length ? Math.max(...deps.map((dep) => visit(dep, new Set(seen)))) : 0);
|
|
124
|
+
memo.set(id, value);
|
|
125
|
+
return value;
|
|
126
|
+
};
|
|
127
|
+
return stages.length ? Math.max(...stages.map((stage) => visit(String(stage.id)))) : 0;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=research-cycle-runner.js.map
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { runCodexTask } from '../codex-control/codex-task-runner.js';
|
|
4
|
+
import { analyzeResearchReportQuality } from './research-report-quality.js';
|
|
5
|
+
import { validateClaimEvidenceMatrix } from './claim-evidence-matrix.js';
|
|
6
|
+
import { validateImplementationBlueprint } from './implementation-blueprint.js';
|
|
7
|
+
import { validateExperimentPlan } from './experiment-plan.js';
|
|
8
|
+
import { validateReplicationPack } from './replication-pack.js';
|
|
9
|
+
import { validateFalsificationCoverage } from './falsification.js';
|
|
10
|
+
export const RESEARCH_FINAL_REVIEW_ARTIFACT = 'research-final-review.json';
|
|
11
|
+
export const RESEARCH_STATIC_FINAL_REVIEW_ARTIFACT = 'research-final-review.static.json';
|
|
12
|
+
export const RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT = 'research-final-review.codex.json';
|
|
13
|
+
export async function readResearchFinalReview(dir) {
|
|
14
|
+
return readJson(path.join(dir, RESEARCH_FINAL_REVIEW_ARTIFACT), null);
|
|
15
|
+
}
|
|
16
|
+
export async function runResearchStaticFinalReview(dir, input = {}) {
|
|
17
|
+
const contract = input.contract || await readJson(path.join(dir, 'research-quality-contract.json'), null);
|
|
18
|
+
const sourceLedger = input.sourceLedger || await readJson(path.join(dir, 'source-ledger.json'), null);
|
|
19
|
+
const claimMatrix = input.claimMatrix || await readJson(path.join(dir, 'claim-evidence-matrix.json'), null);
|
|
20
|
+
const blueprint = input.blueprint || await readJson(path.join(dir, 'implementation-blueprint.json'), null);
|
|
21
|
+
const experimentPlan = input.experimentPlan || await readJson(path.join(dir, 'experiment-plan.json'), null);
|
|
22
|
+
const replicationPack = input.replicationPack || await readJson(path.join(dir, 'replication-pack.json'), null);
|
|
23
|
+
const falsificationLedger = input.falsificationLedger || await readJson(path.join(dir, 'falsification-ledger.json'), null);
|
|
24
|
+
const reportText = input.reportText || await readText(path.join(dir, 'research-report.md'), '');
|
|
25
|
+
const claimValidation = validateClaimEvidenceMatrix(claimMatrix, sourceLedger, falsificationLedger);
|
|
26
|
+
const blueprintValidation = validateImplementationBlueprint(blueprint, contract);
|
|
27
|
+
const experimentValidation = validateExperimentPlan(experimentPlan, contract);
|
|
28
|
+
const replicationValidation = validateReplicationPack(replicationPack);
|
|
29
|
+
const falsificationValidation = validateFalsificationCoverage(falsificationLedger, contract);
|
|
30
|
+
const reportQuality = analyzeResearchReportQuality(reportText);
|
|
31
|
+
const preliminaryReasons = Array.isArray(input.preliminaryReasons) ? input.preliminaryReasons : [];
|
|
32
|
+
const blockers = [
|
|
33
|
+
...preliminaryReasons,
|
|
34
|
+
...claimValidation.blockers,
|
|
35
|
+
...blueprintValidation.blockers,
|
|
36
|
+
...experimentValidation.blockers,
|
|
37
|
+
...replicationValidation.blockers,
|
|
38
|
+
...falsificationValidation.blockers,
|
|
39
|
+
...reportQuality.blockers
|
|
40
|
+
];
|
|
41
|
+
const uniqueBlockers = [...new Set(blockers)];
|
|
42
|
+
const review = {
|
|
43
|
+
schema: 'sks.research-final-reviewer.v1',
|
|
44
|
+
reviewed_at: nowIso(),
|
|
45
|
+
approved: uniqueBlockers.length === 0,
|
|
46
|
+
blockers: uniqueBlockers,
|
|
47
|
+
contract_summary: contract || null,
|
|
48
|
+
checks: {
|
|
49
|
+
claim_matrix: claimValidation,
|
|
50
|
+
implementation_blueprint: blueprintValidation,
|
|
51
|
+
experiment_plan: experimentValidation,
|
|
52
|
+
replication_pack: replicationValidation,
|
|
53
|
+
falsification: falsificationValidation,
|
|
54
|
+
report_quality: reportQuality
|
|
55
|
+
},
|
|
56
|
+
reviewer: 'research_final_reviewer_static_gate'
|
|
57
|
+
};
|
|
58
|
+
await writeJsonAtomic(path.join(dir, RESEARCH_STATIC_FINAL_REVIEW_ARTIFACT), review);
|
|
59
|
+
return review;
|
|
60
|
+
}
|
|
61
|
+
export async function runResearchCodexFinalReviewer(input) {
|
|
62
|
+
if (input.staticReview?.approved !== true) {
|
|
63
|
+
const skipped = {
|
|
64
|
+
schema: 'sks.research-codex-final-review.v1',
|
|
65
|
+
reviewed_at: nowIso(),
|
|
66
|
+
verdict: 'revise',
|
|
67
|
+
unsupported_claim_ids: [],
|
|
68
|
+
missing_evidence: [],
|
|
69
|
+
blueprint_findings: [],
|
|
70
|
+
falsification_findings: [],
|
|
71
|
+
required_revisions: ['static_review_failed'],
|
|
72
|
+
confidence: 'low',
|
|
73
|
+
skipped: true,
|
|
74
|
+
skip_reason: 'static_review_failed'
|
|
75
|
+
};
|
|
76
|
+
await writeJsonAtomic(path.join(input.dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), skipped);
|
|
77
|
+
return skipped;
|
|
78
|
+
}
|
|
79
|
+
if (input.mock === true) {
|
|
80
|
+
const approved = {
|
|
81
|
+
schema: 'sks.research-codex-final-review.v1',
|
|
82
|
+
reviewed_at: nowIso(),
|
|
83
|
+
verdict: 'approve',
|
|
84
|
+
unsupported_claim_ids: [],
|
|
85
|
+
missing_evidence: [],
|
|
86
|
+
blueprint_findings: ['mock final reviewer approves the complete package fixture'],
|
|
87
|
+
falsification_findings: ['mock counterevidence and falsification cases are present'],
|
|
88
|
+
required_revisions: [],
|
|
89
|
+
confidence: 'high',
|
|
90
|
+
mock: true
|
|
91
|
+
};
|
|
92
|
+
await writeJsonAtomic(path.join(input.dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), approved);
|
|
93
|
+
return approved;
|
|
94
|
+
}
|
|
95
|
+
const result = await runCodexTask({
|
|
96
|
+
route: '$Research',
|
|
97
|
+
tier: 'worker',
|
|
98
|
+
missionId: String(input.plan?.mission_id || 'research-final-review'),
|
|
99
|
+
workItemId: 'research_final_review',
|
|
100
|
+
cwd: input.root,
|
|
101
|
+
prompt: buildResearchFinalReviewPrompt(input.plan, input.staticReview),
|
|
102
|
+
outputSchema: researchCodexFinalReviewSchema,
|
|
103
|
+
outputSchemaId: 'sks.research-codex-final-review.v1',
|
|
104
|
+
sandboxPolicy: 'read-only',
|
|
105
|
+
requestedScopeContract: {
|
|
106
|
+
id: 'research-final-review',
|
|
107
|
+
route: '$Research',
|
|
108
|
+
read_only: true,
|
|
109
|
+
allowed_paths: [`.sneakoscope/missions/${input.plan?.mission_id || ''}/`],
|
|
110
|
+
write_paths: [],
|
|
111
|
+
allowed_write_prefixes: [`.sneakoscope/missions/${input.plan?.mission_id || ''}/`],
|
|
112
|
+
source_mutation_allowed: false
|
|
113
|
+
},
|
|
114
|
+
backendPreference: input.backendPreference || ['codex-sdk', 'python-codex-sdk'],
|
|
115
|
+
localLlmPolicy: { mode: 'disabled', requiresGptFinal: true },
|
|
116
|
+
allowLocalLlm: false,
|
|
117
|
+
mutationLedgerRoot: path.join(input.dir, 'research', 'final-review-codex-control'),
|
|
118
|
+
reliabilityPolicy: {
|
|
119
|
+
timeoutClass: 'standard',
|
|
120
|
+
idleTimeoutMs: input.timeoutMs || 120000
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const worker = await readJson(result.workerResultPath, null);
|
|
124
|
+
const review = normalizeCodexReview(worker, result);
|
|
125
|
+
await writeJsonAtomic(path.join(input.dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), review);
|
|
126
|
+
return review;
|
|
127
|
+
}
|
|
128
|
+
export async function runResearchFinalReviewer(dir, input = {}) {
|
|
129
|
+
const staticReview = await runResearchStaticFinalReview(dir, input);
|
|
130
|
+
const existingCodex = await readJson(path.join(dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), null);
|
|
131
|
+
const codexReview = existingCodex || (input.mock === true ? await runResearchCodexFinalReviewer({
|
|
132
|
+
root: input.root || process.cwd(),
|
|
133
|
+
dir,
|
|
134
|
+
plan: input.plan || await readJson(path.join(dir, 'research-plan.json'), null),
|
|
135
|
+
staticReview,
|
|
136
|
+
mock: true
|
|
137
|
+
}) : null);
|
|
138
|
+
const codexApproved = codexReview?.verdict === 'approve';
|
|
139
|
+
const codexRequired = input.codexRequired !== false;
|
|
140
|
+
const blockers = [
|
|
141
|
+
...(Array.isArray(staticReview?.blockers) ? staticReview.blockers : []),
|
|
142
|
+
...(codexRequired && !codexReview ? ['research_codex_final_review_missing'] : []),
|
|
143
|
+
...(codexReview && !codexApproved ? ['research_codex_final_review_not_approved'] : []),
|
|
144
|
+
...(Array.isArray(codexReview?.required_revisions) ? codexReview.required_revisions.map((revision) => `codex_revision:${revision}`) : [])
|
|
145
|
+
];
|
|
146
|
+
const review = {
|
|
147
|
+
schema: 'sks.research-final-reviewer.v2',
|
|
148
|
+
reviewed_at: nowIso(),
|
|
149
|
+
approved: staticReview?.approved === true && (!codexRequired || codexApproved) && blockers.length === 0,
|
|
150
|
+
blockers: [...new Set(blockers)],
|
|
151
|
+
static_review: staticReview,
|
|
152
|
+
codex_review: codexReview,
|
|
153
|
+
reviewer: 'research_final_reviewer_static_plus_codex_gate'
|
|
154
|
+
};
|
|
155
|
+
await writeJsonAtomic(path.join(dir, RESEARCH_FINAL_REVIEW_ARTIFACT), review);
|
|
156
|
+
return review;
|
|
157
|
+
}
|
|
158
|
+
function buildResearchFinalReviewPrompt(plan, staticReview) {
|
|
159
|
+
return [
|
|
160
|
+
'You are the Codex/GPT final reviewer for an SKS Research package.',
|
|
161
|
+
`Mission: ${plan?.mission_id || 'unknown'}`,
|
|
162
|
+
`Prompt: ${plan?.prompt || ''}`,
|
|
163
|
+
'',
|
|
164
|
+
'Review the mission artifacts read-only. Reject if claims lack evidence, blueprint steps are template-like, falsification is missing, or the package is only a short summary.',
|
|
165
|
+
'Return only JSON matching sks.research-codex-final-review.v1 with verdict approve, revise, or reject.',
|
|
166
|
+
'',
|
|
167
|
+
`Static review summary:\n${JSON.stringify(staticReview, null, 2).slice(0, 12000)}`
|
|
168
|
+
].join('\n');
|
|
169
|
+
}
|
|
170
|
+
function normalizeCodexReview(worker, result) {
|
|
171
|
+
if (!result?.ok) {
|
|
172
|
+
return {
|
|
173
|
+
schema: 'sks.research-codex-final-review.v1',
|
|
174
|
+
reviewed_at: nowIso(),
|
|
175
|
+
verdict: 'revise',
|
|
176
|
+
unsupported_claim_ids: [],
|
|
177
|
+
missing_evidence: [],
|
|
178
|
+
blueprint_findings: [],
|
|
179
|
+
falsification_findings: [],
|
|
180
|
+
required_revisions: Array.isArray(result?.blockers) ? result.blockers : ['codex_final_reviewer_unavailable'],
|
|
181
|
+
confidence: 'low',
|
|
182
|
+
worker_result_path: result?.workerResultPath || null
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
schema: 'sks.research-codex-final-review.v1',
|
|
187
|
+
reviewed_at: nowIso(),
|
|
188
|
+
verdict: ['approve', 'revise', 'reject'].includes(worker?.verdict) ? worker.verdict : 'revise',
|
|
189
|
+
unsupported_claim_ids: Array.isArray(worker?.unsupported_claim_ids) ? worker.unsupported_claim_ids.map(String) : [],
|
|
190
|
+
missing_evidence: Array.isArray(worker?.missing_evidence) ? worker.missing_evidence.map(String) : [],
|
|
191
|
+
blueprint_findings: Array.isArray(worker?.blueprint_findings) ? worker.blueprint_findings.map(String) : [],
|
|
192
|
+
falsification_findings: Array.isArray(worker?.falsification_findings) ? worker.falsification_findings.map(String) : [],
|
|
193
|
+
required_revisions: Array.isArray(worker?.required_revisions) ? worker.required_revisions.map(String) : [],
|
|
194
|
+
confidence: ['low', 'medium', 'high'].includes(worker?.confidence) ? worker.confidence : 'medium',
|
|
195
|
+
worker_result_path: result.workerResultPath
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export const researchCodexFinalReviewSchema = {
|
|
199
|
+
type: 'object',
|
|
200
|
+
required: ['schema', 'verdict', 'unsupported_claim_ids', 'missing_evidence', 'blueprint_findings', 'falsification_findings', 'required_revisions', 'confidence'],
|
|
201
|
+
properties: {
|
|
202
|
+
schema: { const: 'sks.research-codex-final-review.v1' },
|
|
203
|
+
verdict: { enum: ['approve', 'revise', 'reject'] },
|
|
204
|
+
unsupported_claim_ids: { type: 'array' },
|
|
205
|
+
missing_evidence: { type: 'array' },
|
|
206
|
+
blueprint_findings: { type: 'array' },
|
|
207
|
+
falsification_findings: { type: 'array' },
|
|
208
|
+
required_revisions: { type: 'array' },
|
|
209
|
+
confidence: { enum: ['low', 'medium', 'high'] }
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
//# sourceMappingURL=research-final-reviewer.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
3
|
+
export const IMPLEMENTATION_HANDOFF_PATCH_PLAN_ARTIFACT = 'implementation-handoff.patch-plan.json';
|
|
4
|
+
export const TEAM_HANDOFF_GOAL_ARTIFACT = 'team-handoff-goal.md';
|
|
5
|
+
export const DECISION_LOG_ARTIFACT = 'decision-log.md';
|
|
6
|
+
export async function writeResearchHandoffArtifacts(dir, plan = null, blueprint = null) {
|
|
7
|
+
const patchPlan = {
|
|
8
|
+
schema: 'sks.research-implementation-handoff-patch-plan.v1',
|
|
9
|
+
generated_at: nowIso(),
|
|
10
|
+
mission_id: plan?.mission_id || null,
|
|
11
|
+
implementation_allowed_in_research: false,
|
|
12
|
+
intended_route: '$Team',
|
|
13
|
+
prompt: plan?.prompt || '',
|
|
14
|
+
source_artifacts: [
|
|
15
|
+
'research-report.md',
|
|
16
|
+
'claim-evidence-matrix.json',
|
|
17
|
+
'implementation-blueprint.json',
|
|
18
|
+
'experiment-plan.json',
|
|
19
|
+
'replication-pack.json',
|
|
20
|
+
'source-quality-report.json'
|
|
21
|
+
],
|
|
22
|
+
proposed_changes: [],
|
|
23
|
+
notes: [
|
|
24
|
+
'This is a handoff artifact. Research records implementation guidance but does not mutate repository source.'
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
const goalLines = [
|
|
28
|
+
'# Research-To-Team Handoff Goal',
|
|
29
|
+
'',
|
|
30
|
+
`Mission: ${plan?.mission_id || 'unknown'}`,
|
|
31
|
+
`Prompt: ${plan?.prompt || ''}`,
|
|
32
|
+
'',
|
|
33
|
+
'Use the implementation blueprint, claim-evidence matrix, source-quality report, experiment plan, replication pack, and final reviewer output before changing code.',
|
|
34
|
+
'',
|
|
35
|
+
'Blueprint sections:',
|
|
36
|
+
...(Array.isArray(blueprint?.sections) ? blueprint.sections.map((section) => `- ${section.id}: ${section.title}`) : [])
|
|
37
|
+
];
|
|
38
|
+
const decisionLog = [
|
|
39
|
+
'# Research Decision Log',
|
|
40
|
+
'',
|
|
41
|
+
`Created: ${nowIso()}`,
|
|
42
|
+
'',
|
|
43
|
+
'- Research may write route-local artifacts only.',
|
|
44
|
+
'- Implementation decisions must be revalidated in the follow-up execution route.'
|
|
45
|
+
];
|
|
46
|
+
await writeJsonAtomic(path.join(dir, IMPLEMENTATION_HANDOFF_PATCH_PLAN_ARTIFACT), patchPlan);
|
|
47
|
+
await writeTextAtomic(path.join(dir, TEAM_HANDOFF_GOAL_ARTIFACT), `${goalLines.join('\n')}\n`);
|
|
48
|
+
await writeTextAtomic(path.join(dir, DECISION_LOG_ARTIFACT), `${decisionLog.join('\n')}\n`);
|
|
49
|
+
return { patch_plan: patchPlan, goal_artifact: TEAM_HANDOFF_GOAL_ARTIFACT, decision_log: DECISION_LOG_ARTIFACT };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=research-handoff.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const RESEARCH_PROMPT_CONTRACT_LINES = Object.freeze([
|
|
2
|
+
'QUALITY CONTRACT: satisfy research-quality-contract.json before setting research-gate.json passed=true.',
|
|
3
|
+
'CLAIM MATRIX: create claim-evidence-matrix.json with key claims, source ids, counterevidence ids, triangulation, and hypothesis status.',
|
|
4
|
+
'SOURCE QUALITY: create source-quality-report.json and record claim_ids on source-ledger sources.',
|
|
5
|
+
'BLUEPRINT: create implementation-blueprint.json and implementation-blueprint.md with at least eight sections.',
|
|
6
|
+
'EXPERIMENT: create experiment-plan.json/.md with at least five steps and a replication-pack.json.',
|
|
7
|
+
'FINAL REVIEW: create research-final-review.json and keep the gate blocked unless approved=true.',
|
|
8
|
+
'READ ONLY: do not mutate repository source during Research; write only route-local mission artifacts.'
|
|
9
|
+
]);
|
|
10
|
+
export function researchPromptContractText() {
|
|
11
|
+
return RESEARCH_PROMPT_CONTRACT_LINES.join('\n');
|
|
12
|
+
}
|
|
13
|
+
export function validateResearchPromptContract(promptText = '') {
|
|
14
|
+
const text = String(promptText || '');
|
|
15
|
+
const requiredTokens = ['research-quality-contract.json', 'claim-evidence-matrix.json', 'source-quality-report.json', 'implementation-blueprint.json', 'experiment-plan.json', 'replication-pack.json', 'research-final-review.json'];
|
|
16
|
+
const missing = requiredTokens.filter((token) => !text.includes(token));
|
|
17
|
+
return {
|
|
18
|
+
ok: missing.length === 0,
|
|
19
|
+
blockers: missing.map((token) => `research_prompt_contract_missing:${token}`),
|
|
20
|
+
required_tokens: requiredTokens,
|
|
21
|
+
missing_tokens: missing
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=research-prompt-contract.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readJson, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
export const RESEARCH_QUALITY_CONTRACT_ARTIFACT = 'research-quality-contract.json';
|
|
4
|
+
export const DEFAULT_RESEARCH_QUALITY_CONTRACT = {
|
|
5
|
+
schema: 'sks.research-quality-contract.v1',
|
|
6
|
+
min_sources_total: 12,
|
|
7
|
+
min_source_layers_covered: 5,
|
|
8
|
+
min_counterevidence_sources: 2,
|
|
9
|
+
min_trianguled_claims: 6,
|
|
10
|
+
min_key_claims: 8,
|
|
11
|
+
min_implementation_blueprint_sections: 8,
|
|
12
|
+
min_falsification_cases: 4,
|
|
13
|
+
min_experiment_steps: 5,
|
|
14
|
+
min_report_words: 2200,
|
|
15
|
+
required_artifacts: [
|
|
16
|
+
'research-report.md',
|
|
17
|
+
'implementation-blueprint.md',
|
|
18
|
+
'implementation-blueprint.json',
|
|
19
|
+
'claim-evidence-matrix.json',
|
|
20
|
+
'source-ledger.json',
|
|
21
|
+
'source-quality-report.json',
|
|
22
|
+
'falsification-ledger.json',
|
|
23
|
+
'experiment-plan.md',
|
|
24
|
+
'replication-pack.json',
|
|
25
|
+
'research-gate.json'
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
export function normalizeResearchQualityContract(input = {}) {
|
|
29
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
30
|
+
return {
|
|
31
|
+
...DEFAULT_RESEARCH_QUALITY_CONTRACT,
|
|
32
|
+
...source,
|
|
33
|
+
schema: 'sks.research-quality-contract.v1',
|
|
34
|
+
min_sources_total: positiveInt(source.min_sources_total, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_sources_total),
|
|
35
|
+
min_source_layers_covered: positiveInt(source.min_source_layers_covered, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_source_layers_covered),
|
|
36
|
+
min_counterevidence_sources: positiveInt(source.min_counterevidence_sources, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_counterevidence_sources),
|
|
37
|
+
min_trianguled_claims: positiveInt(source.min_trianguled_claims ?? source.min_triangulated_claims, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_trianguled_claims),
|
|
38
|
+
min_key_claims: positiveInt(source.min_key_claims, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_key_claims),
|
|
39
|
+
min_implementation_blueprint_sections: positiveInt(source.min_implementation_blueprint_sections, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_implementation_blueprint_sections),
|
|
40
|
+
min_falsification_cases: positiveInt(source.min_falsification_cases, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_falsification_cases),
|
|
41
|
+
min_experiment_steps: positiveInt(source.min_experiment_steps, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_experiment_steps),
|
|
42
|
+
min_report_words: positiveInt(source.min_report_words, DEFAULT_RESEARCH_QUALITY_CONTRACT.min_report_words),
|
|
43
|
+
required_artifacts: Array.isArray(source.required_artifacts) && source.required_artifacts.length
|
|
44
|
+
? source.required_artifacts.map(String)
|
|
45
|
+
: [...DEFAULT_RESEARCH_QUALITY_CONTRACT.required_artifacts]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function readResearchQualityContract(dir) {
|
|
49
|
+
const contract = await readJson(path.join(dir, RESEARCH_QUALITY_CONTRACT_ARTIFACT), null);
|
|
50
|
+
return normalizeResearchQualityContract(contract || {});
|
|
51
|
+
}
|
|
52
|
+
export async function writeResearchQualityContract(dir, contract = {}) {
|
|
53
|
+
const normalized = normalizeResearchQualityContract(contract);
|
|
54
|
+
await writeJsonAtomic(path.join(dir, RESEARCH_QUALITY_CONTRACT_ARTIFACT), normalized);
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
function positiveInt(value, fallback) {
|
|
58
|
+
const parsed = Number(value);
|
|
59
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=research-quality-contract.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const REQUIRED_RESEARCH_REPORT_HEADINGS = [
|
|
2
|
+
'Question',
|
|
3
|
+
'Methodology',
|
|
4
|
+
'Source Map',
|
|
5
|
+
'Key Claims',
|
|
6
|
+
'Evidence Matrix Summary',
|
|
7
|
+
'Counterevidence',
|
|
8
|
+
'Falsification',
|
|
9
|
+
'Implementation Blueprint',
|
|
10
|
+
'Experiment / Validation Plan',
|
|
11
|
+
'Limitations',
|
|
12
|
+
'References'
|
|
13
|
+
];
|
|
14
|
+
export function analyzeResearchReportQuality(text) {
|
|
15
|
+
const body = String(text || '');
|
|
16
|
+
const headings = body.split(/\r?\n/)
|
|
17
|
+
.map((line) => line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/)?.[1]?.trim())
|
|
18
|
+
.filter((value) => Boolean(value));
|
|
19
|
+
const lowerHeadings = headings.map((heading) => normalizeHeading(heading));
|
|
20
|
+
const headingsPresent = REQUIRED_RESEARCH_REPORT_HEADINGS.filter((heading) => lowerHeadings.some((value) => value.includes(normalizeHeading(heading))));
|
|
21
|
+
const missingHeadings = REQUIRED_RESEARCH_REPORT_HEADINGS.filter((heading) => !headingsPresent.includes(heading));
|
|
22
|
+
const implementationText = sectionText(body, 'Implementation Blueprint');
|
|
23
|
+
const referencesText = sectionText(body, 'References');
|
|
24
|
+
const referencesSourceIds = [...new Set([
|
|
25
|
+
...body.matchAll(/\b(?:source|src|mock-source|counter|mock-counter)-[A-Za-z0-9_.:-]+\b/g)
|
|
26
|
+
].map((match) => match[0]))];
|
|
27
|
+
const blockers = [
|
|
28
|
+
...missingHeadings.map((heading) => `research_report_heading_missing:${normalizeHeading(heading).replace(/\s+/g, '_')}`),
|
|
29
|
+
...(referencesSourceIds.length ? [] : ['research_report_references_missing_source_ids'])
|
|
30
|
+
];
|
|
31
|
+
return {
|
|
32
|
+
schema: 'sks.research-report-quality.v1',
|
|
33
|
+
word_count: countWords(body),
|
|
34
|
+
headings_present: headingsPresent,
|
|
35
|
+
missing_headings: missingHeadings,
|
|
36
|
+
implementation_section_words: countWords(implementationText),
|
|
37
|
+
references_source_ids: referencesText ? referencesSourceIds : [],
|
|
38
|
+
blockers,
|
|
39
|
+
ok: blockers.length === 0
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function countWords(text) {
|
|
43
|
+
return String(text || '').trim().split(/\s+/).filter(Boolean).length;
|
|
44
|
+
}
|
|
45
|
+
function normalizeHeading(value) {
|
|
46
|
+
return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
47
|
+
}
|
|
48
|
+
function sectionText(text, heading) {
|
|
49
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
50
|
+
const target = normalizeHeading(heading);
|
|
51
|
+
let capture = false;
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const match = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
55
|
+
if (match) {
|
|
56
|
+
const normalized = normalizeHeading(match[2] || '');
|
|
57
|
+
if (capture)
|
|
58
|
+
break;
|
|
59
|
+
capture = normalized.includes(target);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (capture)
|
|
63
|
+
out.push(line);
|
|
64
|
+
}
|
|
65
|
+
return out.join('\n');
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=research-report-quality.js.map
|