Compare commits
109
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c205c25598 | ||
|
|
a8feb3eee4 | ||
|
|
574902926e | ||
|
|
70389268a2 | ||
|
|
3199e36cd4 | ||
|
|
df6f52e652 | ||
|
|
bd2137fc4f | ||
|
|
060a2e65ec | ||
|
|
fe6e9dfbbc | ||
|
|
360fbb5f94 | ||
|
|
fbb74b83d3 | ||
|
|
ceffe7d2e4 | ||
|
|
63bee210eb | ||
|
|
d5fba689de | ||
|
|
39d83fc388 | ||
|
|
1b5f774bac | ||
|
|
98cde56413 | ||
|
|
e267c51293 | ||
|
|
5694193959 | ||
|
|
7667c2937c | ||
|
|
b537018ab2 | ||
|
|
eac67a24da | ||
|
|
a804cb6436 | ||
|
|
883554a2db | ||
|
|
daba0e3294 | ||
|
|
c4825c2d66 | ||
|
|
02f7d1ca42 | ||
|
|
cc52bfb69f | ||
|
|
e0d0923488 | ||
|
|
47c2e56393 | ||
|
|
004d168157 | ||
|
|
87e1320175 | ||
|
|
7a27ed557c | ||
|
|
ec20649e35 | ||
|
|
303848a0fb | ||
|
|
74d6b7393b | ||
|
|
f30b62c457 | ||
|
|
38fd57846c | ||
|
|
9628587bfa | ||
|
|
5421bf931b | ||
|
|
292c511ab5 | ||
|
|
654b89d84f | ||
|
|
19b80c95a5 | ||
|
|
6ba8302bc8 | ||
|
|
9e5830432c | ||
|
|
f75c887bd7 | ||
|
|
3e1c6c0ca4 | ||
|
|
c99b9fef14 | ||
|
|
39d7083611 | ||
|
|
9ccb331a41 | ||
|
|
dc0005423b | ||
|
|
01ae71de41 | ||
|
|
eb9b3916a7 | ||
|
|
5707045f96 | ||
|
|
c0d1898826 | ||
|
|
0d3bf42e7f | ||
|
|
1ebd66bb4c | ||
|
|
e44ee897db | ||
|
|
7b21a34117 | ||
|
|
45cc070b02 | ||
|
|
497f229af1 | ||
|
|
eb897663af | ||
|
|
11b6832edd | ||
|
|
3d55f30429 | ||
|
|
a932645075 | ||
|
|
6c3b8edbbe | ||
|
|
7cecc90ac6 | ||
|
|
8d23d43ab9 | ||
|
|
4a75d14c68 | ||
|
|
6437daf606 | ||
|
|
afddbf2f65 | ||
|
|
612da55189 | ||
|
|
8b2eee07d0 | ||
|
|
a5baeb8e18 | ||
|
|
df6e0aa1cf | ||
|
|
4fa9a143af | ||
|
|
53abd24d49 | ||
|
|
32c4184f7b | ||
|
|
65638f3590 | ||
|
|
9f33784986 | ||
|
|
4f31741ac6 | ||
|
|
b198fc8dee | ||
|
|
84066eb745 | ||
|
|
85c0f1eafa | ||
|
|
edc00f9883 | ||
|
|
6f55c89677 | ||
|
|
d1c49c9d93 | ||
|
|
8752575a6e | ||
|
|
851e2a112f | ||
|
|
f65fc5d258 | ||
|
|
26e1cd8242 | ||
|
|
b9a3d2239c | ||
|
|
5bf15537d2 | ||
|
|
40a2f58fad | ||
|
|
c9dcfa2434 | ||
|
|
f8118a335e | ||
|
|
c8a85d5950 | ||
|
|
d00f567178 | ||
|
|
dc0b510de3 | ||
|
|
e5e5008339 | ||
|
|
a0f67b7682 | ||
|
|
da4e422efa | ||
|
|
208c0b3202 | ||
|
|
bb24c4387d | ||
|
|
a133fd6840 | ||
|
|
44aff75599 | ||
|
|
ce383f1fa4 | ||
|
|
6b44a3b630 | ||
|
|
5befd81b23 |
No files matched your search
@@ -7,47 +7,20 @@ on:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
# PRIORITY GATE (2026-09-04): every issue must carry exactly one of
|
||||
# P0/P1/P2/P3. An issue that lands without one gets 'unprioritized' +
|
||||
# a demand comment — filing is not complete until prioritized.
|
||||
priority-gate:
|
||||
if: github.event_name == 'issues' && github.event.action != 'labeled' && github.event.issue.state == 'open'
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Enforce priority label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
if (!issue) return;
|
||||
const labels = (issue.labels || []).map(l => l.name);
|
||||
const priorities = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
|
||||
const has = labels.filter(l => priorities.includes(l));
|
||||
if (has.length === 1) {
|
||||
// Prioritized — clear the flag if it was set.
|
||||
if (labels.includes('unprioritized')) {
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
id: issue.labels.find(l => l.name === 'unprioritized').id,
|
||||
}).catch(e => { if (e.status !== 404) throw e; });
|
||||
}
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, labels: ['unprioritized'],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `⚠️ **Priority gate:** this issue has no priority. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
|
||||
});
|
||||
|
||||
# SINGLE-PASS FILING (2026-10-07, Nick's first-time-correct directive):
|
||||
# the old design ran TWO jobs on submission — priority-gate (which saw
|
||||
# no P-label and posted the ⚠️ warning) and auto-label (which inferred
|
||||
# and applied the P-label). The gate ran BEFORE the inference that
|
||||
# would have made the warning unnecessary: every keyword-inferrable
|
||||
# submission ate a spurious warning, a flag add/remove cycle, and a
|
||||
# wasted workflow run. The fix is ORDER: infer → apply → THEN gate.
|
||||
# A submission whose priority is inferable gets its P-label in this
|
||||
# same run; the warning fires only when inference genuinely produced
|
||||
# nothing (a true filing gap, not a race the robot loses to itself).
|
||||
auto-label:
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Analyze and label
|
||||
- name: Infer, apply, then gate-check (one pass)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
@@ -65,6 +38,7 @@ jobs:
|
||||
return;
|
||||
}
|
||||
const action = context.payload.action;
|
||||
const isIssue = context.payload.issue !== undefined;
|
||||
const title = context.payload.issue?.title || context.payload.pull_request?.title || '';
|
||||
const body = context.payload.issue?.body || context.payload.pull_request?.body || '';
|
||||
const text = (title + ' ' + body).toLowerCase();
|
||||
@@ -143,7 +117,7 @@ jobs:
|
||||
// normalization). Assignment from the TITLE prefix first
|
||||
// ([P1]/P1:/priority=...), then keyword inference using the
|
||||
// gate's own rule set (the scale lives in ONE place — the
|
||||
// priority-gate job above).
|
||||
// gate check at the bottom of THIS job).
|
||||
const pMatch = title.match(/\bP([0-3])\b/);
|
||||
const priorityLabelNames = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
|
||||
if (pMatch) {
|
||||
@@ -206,9 +180,16 @@ jobs:
|
||||
labels: missingDesired,
|
||||
});
|
||||
}
|
||||
// The priority gate can flag a duplicate before this job
|
||||
// reconciles it. Clear the flag only AFTER verifying exactly one
|
||||
// surviving P-label against Gitea's current state.
|
||||
|
||||
// ---- PRIORITY GATE (runs LAST — after inference and
|
||||
// reconciliation, so the verdict reflects the labels THIS run
|
||||
// just applied, not a pre-inference snapshot). Issue-scoped:
|
||||
// the filing contract governs issues; PRs get inferred labels
|
||||
// but are not filing units.
|
||||
if (!isIssue) {
|
||||
console.log(`PR #${issueNumber}: labels applied, gate skipped (gate is issue-scoped).`);
|
||||
return;
|
||||
}
|
||||
const reconciled = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
@@ -216,11 +197,33 @@ jobs:
|
||||
const actual = reconciled.data;
|
||||
const activePriority = actual.filter(l => priorityLabelNames.includes(l.name));
|
||||
const flag = actual.find(l => l.name === 'unprioritized');
|
||||
if (activePriority.length === 1 && flag) {
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
if (activePriority.length === 1) {
|
||||
// Prioritized (by human or by this run's inference). Clear
|
||||
// any stale flag — no warning is ever posted on this path.
|
||||
if (flag) {
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber, id: flag.id,
|
||||
});
|
||||
}
|
||||
console.log(`Priority gate: satisfied (${activePriority[0].name}).`);
|
||||
return;
|
||||
}
|
||||
// Genuinely unprioritized: inference produced nothing AND the
|
||||
// caller set nothing. Flag + demand comment — the filing gap
|
||||
// is real, not a race artifact.
|
||||
const reason = activePriority.length === 0
|
||||
? 'no priority'
|
||||
: `${activePriority.length} priority labels (exactly one required)`;
|
||||
if (!flag) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber, id: flag.id,
|
||||
issue_number: issueNumber, labels: ['unprioritized'],
|
||||
});
|
||||
}
|
||||
console.log(`Removed managed labels: ${staleManaged.join(', ') || 'none'}`);
|
||||
console.log(`Added managed labels: ${missingDesired.join(', ') || 'none'}`);
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `⚠️ **Priority gate:** this issue has ${reason}. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
|
||||
});
|
||||
console.log(`Priority gate: flagged — ${reason}.`);
|
||||
+34
-1
@@ -63,6 +63,13 @@ jobs:
|
||||
- name: Structural contract (henxels)
|
||||
run: |
|
||||
.ci-venv/bin/henxels check --all --plain
|
||||
# ABSOLUTE SCAFFOLDING LAW (2026-10-07): every Go/Rust implementation
|
||||
# unit must map to a construction contract in store/scaffold/. New
|
||||
# units without a contract fail the build — open the contract BEFORE
|
||||
# the commit lands, never after.
|
||||
- name: Scaffolding coverage (absolute law)
|
||||
run: |
|
||||
python3 scripts/scaffolding_coverage.py
|
||||
|
||||
unit-tests:
|
||||
runs-on: rune-ci-desktop
|
||||
@@ -163,6 +170,26 @@ jobs:
|
||||
GOT=$(go version | awk '{print $3}' | sed 's/^go//' | cut -d- -f1)
|
||||
echo "pinned: $WANT | toolchain: $GOT"
|
||||
[ "$GOT" = "$WANT" ] || { echo "::error::Go toolchain drift: want $WANT, got $GOT (update go-version.txt with the rollout)"; exit 1; }
|
||||
- name: build Survivor staticlibs in-tree (for cgo main packages)
|
||||
run: |
|
||||
# Survivor main packages link Rust staticlibs via ${SRCDIR} paths
|
||||
# (adaptive-router → libroute_risk_flags.a). Those paths resolve
|
||||
# only when the crate target dir is IN-TREE, so this build must
|
||||
# NOT set CARGO_TARGET_DIR (the redirected builds below are for
|
||||
# the FFI-loader canaries and stay as they are).
|
||||
# Every Survivor-linked crate must have its .a in-tree before the
|
||||
# go loop: adaptive-router → route_risk_flags; cognition →
|
||||
# cognition_core. Add crates here as Survivor packages grow.
|
||||
cargo build --release --manifest-path src/rust/route_risk_flags/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/construction_interface/Cargo.toml
|
||||
- name: prune stale /tmp cargo target dirs (tmpfs guard)
|
||||
run: |
|
||||
# /tmp is a 32G tmpfs; every CI run leaves a ~218MB
|
||||
# rune-ci-target-<run_id> dir. Without pruning, 300+ runs filled
|
||||
# the tmpfs (2026-10-07: venv creation failed disk-full across
|
||||
# every job). Keep the 3 newest; drop the rest.
|
||||
cd /tmp && ls -dt rune-ci-target-* 2>/dev/null | tail -n +4 | xargs -r rm -rf
|
||||
- name: go vet + go test + go build
|
||||
run: |
|
||||
for dir in src/go/*/; do
|
||||
@@ -171,10 +198,16 @@ jobs:
|
||||
cd "$dir"
|
||||
go vet ./...
|
||||
go test ./... 2>/dev/null || true
|
||||
go build -o /tmp/go-$(basename "$dir") .
|
||||
OUT_DIR="${GITHUB_WORKSPACE:-$PWD}/bins"
|
||||
mkdir -p "$OUT_DIR"
|
||||
go build -o "$OUT_DIR/go-$(basename "$dir")" .
|
||||
cd - > /dev/null
|
||||
fi
|
||||
done
|
||||
- name: adaptive-router parity gate (HARD — oracle replay, not swallowable)
|
||||
run: |
|
||||
cd src/go/adaptive-router
|
||||
go test ./... -run TestParityAgainstPythonOracle -v
|
||||
|
||||
canary:
|
||||
runs-on: rune-ci-desktop
|
||||
|
||||
@@ -26,6 +26,9 @@ Only use `git commit --no-verify` in a genuine emergency: it bypasses the hooks
|
||||
- Every workflow is registered in the pipeline registry — new pipeline machinery ships with documentation (in ./.gitea/workflows/*.yml)
|
||||
- Every TUI surface is registered — no silent UI growth (in ./src/rune/*tui*.py)
|
||||
- New rune modules ship with tests or a canary — no ungoverned code surfaces (in ./src/rune/*.py)
|
||||
- Rust crate files stay under a line budget (in ./src/rust/*/src/*.rs)
|
||||
- Go modules stay under a line budget (in ./src/go/**/*.go)
|
||||
- No secrets in Rust or Go source files (in ./src/rust/*/src/*.rs, ./src/go/**/*.go)
|
||||
- PRDs are living documents — their generated state sections regenerate identically from governed sources
|
||||
- PRD staleness is a CI failure — docs rot is silent failure (in ./scripts/gen_prd_state.py)
|
||||
- The adversarial review procedure is binding — dispatch templates must reference it (in ./docs/adversarial-review.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.99.10**
|
||||
**Current release: v0.117.2**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_comment": "GENERATED by rune.spine — compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-16T18:23:38Z",
|
||||
"umbrellas": {
|
||||
@@ -78,8 +78,10 @@
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer",
|
||||
"grading",
|
||||
"programme_gates",
|
||||
"sc_loader"
|
||||
"sc_loader",
|
||||
"worthiness"
|
||||
],
|
||||
"rust_crates": [
|
||||
"evidence_provenance",
|
||||
@@ -115,7 +117,7 @@
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"description": "Who the agent is — persona, voice, anchors",
|
||||
"go_surfaces": [
|
||||
"persona-definition",
|
||||
"voicebox",
|
||||
@@ -365,4 +367,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
# DICHOTOMY REBASE — HONEST AUDIT (function-level)
|
||||
|
||||
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
|
||||
|
||||
**Function-level completion: 13%** (290/2225 runtime functions matched)
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 7 | 686 | 80%+ of functions ported |
|
||||
| PARTIAL | 50 | 17873 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 121 | 49578 | no name-preserving implementation |
|
||||
| Total runtime | 178 | 68137 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| rune_tui | 4038 | 157 | TUI-LAST |
|
||||
| rune_operator | 2723 | 68 | GO |
|
||||
| memory_store | 2573 | 77 | GO |
|
||||
| curator | 1838 | 43 | GO |
|
||||
| tool_executor | 1812 | 47 | GO |
|
||||
| warm_router | 1536 | 36 | GO |
|
||||
| construction_interface | 1173 | 27 | RUST |
|
||||
| spine | 1034 | 31 | RUST |
|
||||
| construction_decision | 1014 | 20 | RUST |
|
||||
| scaffold | 868 | 31 | GO |
|
||||
| elastic_cognition | 765 | 13 | GO |
|
||||
| memory_consolidation | 740 | 22 | GO |
|
||||
| structural_store | 656 | 33 | RUST |
|
||||
| abstraction | 647 | 16 | GO |
|
||||
| contracts | 626 | 7 | RUST |
|
||||
| work_manager | 591 | 25 | GO |
|
||||
| construction_intent | 555 | 16 | RUST |
|
||||
| canary_audit_ledger | 540 | 2 | TEST-INFRA |
|
||||
| identity_anchors | 537 | 16 | RUST |
|
||||
| build_mode | 517 | 33 | GO |
|
||||
| drift_core | 517 | 14 | RUST |
|
||||
| event_wait | 517 | 14 | GO |
|
||||
| tui_validator | 514 | 13 | TUI-LAST |
|
||||
| memory_selector | 511 | 13 | RUST |
|
||||
| system_registry | 501 | 14 | RUST |
|
||||
| construction_pattern | 496 | 12 | RUST |
|
||||
| graph_memory | 474 | 9 | RUST |
|
||||
| tool_protocol | 461 | 27 | GO |
|
||||
| auto_tasks | 460 | 14 | GO |
|
||||
| capability_fit | 455 | 13 | RUST |
|
||||
| drift_ledger | 453 | 16 | RUST |
|
||||
| goal | 441 | 19 | GO |
|
||||
| release_notes | 439 | 16 | GO |
|
||||
| cron_engine | 436 | 27 | GO |
|
||||
| model_profiles | 435 | 15 | GO |
|
||||
| model_routing | 432 | 13 | GO |
|
||||
| voice_hybrid | 421 | 10 | RUST |
|
||||
| behavior | 407 | 9 | RUST |
|
||||
| token_optimizer | 407 | 22 | RUST |
|
||||
| capability_ledger | 405 | 18 | RUST |
|
||||
| chat_bubble | 401 | 12 | TUI-LAST |
|
||||
| prompt_sync | 376 | 14 | RUST |
|
||||
| provider_budget | 354 | 20 | GO |
|
||||
| structural_model | 352 | 20 | RUST |
|
||||
| programme_continuation | 349 | 11 | RUST |
|
||||
| canary_rune_serve | 329 | 13 | TEST-INFRA |
|
||||
| builder_bridge | 326 | 11 | GO |
|
||||
| canary_p1_07 | 324 | 10 | TEST-INFRA |
|
||||
| layout_contract | 316 | 6 | RUST |
|
||||
| drift_observability | 315 | 8 | RUST |
|
||||
| build_basis | 314 | 12 | RUST |
|
||||
| canary_p0_07 | 310 | 10 | TEST-INFRA |
|
||||
| attention_capsule | 296 | 7 | RUST |
|
||||
| canary_p1_01 | 294 | 12 | TEST-INFRA |
|
||||
| voice_sync | 293 | 10 | RUST |
|
||||
| drift_cache | 292 | 15 | RUST |
|
||||
| outcome_ledger | 286 | 7 | RUST |
|
||||
| time_awareness | 283 | 14 | RUST |
|
||||
| tiered_executor | 282 | 7 | GO |
|
||||
| standardized_providers | 281 | 5 | GO |
|
||||
| canary_p1_06 | 280 | 7 | TEST-INFRA |
|
||||
| canary_p1_03 | 271 | 9 | TEST-INFRA |
|
||||
| auto_context | 270 | 8 | GO |
|
||||
| canary_gitea_automation | 263 | 3 | TEST-INFRA |
|
||||
| canary_p1_02 | 253 | 10 | TEST-INFRA |
|
||||
| construction_patterns_builtin | 249 | 1 | RUST |
|
||||
| canary_full_system | 244 | 2 | TEST-INFRA |
|
||||
| toolchain | 236 | 5 | RUST |
|
||||
| task_gate | 235 | 8 | RUST |
|
||||
| core_server | 234 | 12 | GO |
|
||||
| canary_native | 231 | 3 | TEST-INFRA |
|
||||
| openai_compat_transport | 227 | 9 | GO |
|
||||
| shard_manager | 223 | 9 | RUST |
|
||||
| spine_language | 218 | 5 | RUST |
|
||||
| evidence_probe | 212 | 9 | GO |
|
||||
| model_registry | 211 | 7 | GO |
|
||||
| canary_p0_04 | 207 | 8 | TEST-INFRA |
|
||||
| forge_client | 203 | 18 | GO |
|
||||
| canary_context_broker | 199 | 10 | TEST-INFRA |
|
||||
| canary_p0_05 | 199 | 13 | TEST-INFRA |
|
||||
| testing_mode | 196 | 12 | RUST |
|
||||
| cc_loader | 195 | 26 | RUST |
|
||||
| canary_spine_meta | 192 | 7 | TEST-INFRA |
|
||||
| verification_layer | 189 | 12 | RUST |
|
||||
| ffi_query_memory_smoke | 182 | 5 | RUST |
|
||||
| canary_tui_transport | 174 | 7 | TEST-INFRA |
|
||||
| behaviour_ledger | 173 | 12 | RUST |
|
||||
| canary_tui_drift | 170 | 2 | TEST-INFRA |
|
||||
| inference_trace | 164 | 6 | RUST |
|
||||
| ms_loader | 163 | 16 | RUST |
|
||||
| voicebox | 161 | 11 | GO |
|
||||
| canary_gitea_pipeline | 159 | 2 | TEST-INFRA |
|
||||
| forge | 146 | 11 | GO |
|
||||
| rune_graphic | 145 | 5 | TUI-LAST |
|
||||
| project_registry | 139 | 11 | RUST |
|
||||
| voice_injector | 138 | 5 | GO |
|
||||
| canary_rune_entry | 137 | 3 | TEST-INFRA |
|
||||
| tui_backlog | 125 | 5 | TUI-LAST |
|
||||
| drift_resume | 118 | 3 | RUST |
|
||||
| canary_conversation_state | 115 | 2 | TEST-INFRA |
|
||||
| canary_query_memory | 110 | 2 | TEST-INFRA |
|
||||
| tui_transport | 108 | 10 | TUI-LAST |
|
||||
| spine_loader | 105 | 7 | RUST |
|
||||
| canary_unbounded | 104 | 2 | TEST-INFRA |
|
||||
| message_widgets | 104 | 6 | TUI-LAST |
|
||||
| sc_loader | 96 | 6 | RUST |
|
||||
| cs_loader | 90 | 5 | RUST |
|
||||
| tc_loader | 88 | 5 | RUST |
|
||||
| canary_schema_migration | 86 | 1 | TEST-INFRA |
|
||||
| mems_loader | 83 | 5 | RUST |
|
||||
| provider_context | 48 | 1 | GO |
|
||||
| wait_guard_corpus | 46 | 0 | RUST |
|
||||
| canary_cognitive_strata | 44 | 1 | TEST-INFRA |
|
||||
| builder_bridge_cli | 42 | 1 | GO |
|
||||
| __init__ | 40 | 1 | RUST |
|
||||
| mr_loader | 38 | 13 | RUST |
|
||||
| canary_memory_synthesis | 33 | 1 | TEST-INFRA |
|
||||
| canary_resource_authority | 33 | 1 | TEST-INFRA |
|
||||
| canary_surface_parity | 33 | 1 | TEST-INFRA |
|
||||
| canary_orchestration_foundation | 32 | 1 | TEST-INFRA |
|
||||
| canary_gitea_delegation | 31 | 1 | TEST-INFRA |
|
||||
|
||||
## PARTIAL — gaps to close (matched/total fns; first missing named)
|
||||
|
||||
| Module | Lines | Coverage | Missing (first named) |
|
||||
|---|---|---|---|
|
||||
| local_tool_runtime | 806 | 8/30 | _find_capability_executor, _extract_context_length, context_length, reset_context_length_cache, supports_tools, supports_thinking |
|
||||
| provider_registry | 666 | 7/27 | _format_tools_openai, _format_tools_anthropic, _execute_tool_calls, _execute_tool_calls_anthropic, get_registry, connect_provider |
|
||||
| sleep_time | 971 | 5/25 | _ensure_dirs, db_connect, fetch_sessions, extract_parts, extract_todos, write_transcript |
|
||||
| persona_gate | 724 | 7/26 | _now, _cc_pg_core, _py_lex, _lex_input, _py_overlap, _embed_raw |
|
||||
| context_broker | 823 | 8/26 | _py_default_estimator, _cc_core, _py_default_budget, _py_default_memory_budget, _py_memory_disabled_value, reset_discovered_windows |
|
||||
| capability_broker | 905 | 4/20 | _native_tool_schemas, _infer_task_family, _scored_tools, _select_for_job, load_registry, _native_tool_cards |
|
||||
| safety_override | 569 | 7/22 | _keyword_stopwords, _cc_probe, cc_loader_pin, self_test, _py_matches, escalation_rate |
|
||||
| growth_delta | 437 | 5/19 | _store_db, _llm, _parse_json, _load_deltas, _save_deltas, _cc_core |
|
||||
| failure_auto_filer | 527 | 11/24 | _error_text, unconsumed, mark_consumed, unconsumed, mark_consumed, submit |
|
||||
| night_mode | 405 | 5/18 | get_night_mode, start_night_mode, stop_night_mode, get_status, get_report, to_dict |
|
||||
| cache | 492 | 5/17 | _record_hit, _record_miss, flush_stats, get_cache, put_cache, invalidate_scope |
|
||||
| parallel_processor | 194 | 3/14 | _shutdown_processor, get_processor, run_parallel_with_args, get_status, test_parallel, run_parallel_with_args |
|
||||
| claim_audit | 483 | 4/14 | _get_warm_neuron_store, reframe, _graph_has, _warm_has, verify, journal |
|
||||
| tier_manager | 262 | 5/15 | _cc_probe, cc_loader_pin, _py_core_now, _py_get_tier, _py_promote, _py_demote |
|
||||
| witness | 466 | 7/17 | _now, _tables, _cc_probe, cc_loader_pin, _py_extract_topic, _py_detect_signals |
|
||||
| drift_compression | 307 | 3/12 | _py_get_compression_ratio, _py_compress_text, _py_compress_entry, _py_should_warn, _py_warn_before_compress, get_compression_ratio |
|
||||
| message_queue | 316 | 5/13 | _load_queue, _save_queue, add_message, get_pending, approve_all, get_approved |
|
||||
| model_connect | 462 | 2/10 | is_moe_model, _now, _clean_model_name, _get_cloud_models, health_check, route_model |
|
||||
| intent_continuity | 168 | 2/9 | to_dict, from_dict, _load, save, create_detour, resolve_detour |
|
||||
| pins | 284 | 5/12 | _store_path, _save, _detail_blob, unpin, _py_pin_recall, list_pins |
|
||||
| adaptive_router | 625 | 13/19 | _floor_applies_from_flags, _floor_reason, record_final_route, _event, _record_transition_with, _detect_external_refs |
|
||||
| audit_ledger | 231 | 2/8 | _compute_hash, get_entries, log_promotion, log_gate_result, log_drift_alert, main |
|
||||
| context_pressure | 262 | 3/9 | log_measurement, read_log, log_stats, _now, handle_hook, main |
|
||||
| hook_engine | 860 | 2/8 | discover_functions, tag_function, recommend_placement, validate_placements, generate_report, handle_hook |
|
||||
| microkernel | 434 | 6/12 | now_iso, discover, _extract_manifest, _load_registry, _save_registry, _run_hook |
|
||||
| persona_definition | 210 | 2/8 | _by_topic, compile_full, emit_constitution, emit_identity, emit_persona, handle_hook |
|
||||
| progressive_escalation | 279 | 2/8 | execute_with_escalation, test_escalation, handle_hook, to_dict, execute, verify |
|
||||
| connection_pool | 144 | 3/8 | get_pool, get_connection, get_status, get_connection, get_status |
|
||||
| iteration | 190 | 3/8 | _now, context_packet, predictive_impact, descriptive_impact, add |
|
||||
| task_persist | 269 | 5/10 | _save, export, restore, main, handle_hook |
|
||||
| construction_commands | 268 | 1/5 | _refuse, _validate, _accept, dispatch |
|
||||
| construction_mode | 395 | 1/5 | _tokens, _contains_any, to_dict, from_dict |
|
||||
| event_envelope | 303 | 4/8 | _content_hash, test_envelope, to_canonical_dict, to_dict |
|
||||
| response_adapter | 222 | 3/7 | parse_plain_response, parse_response, test_adapter, to_dict |
|
||||
| self_improve | 242 | 1/5 | _now, suggest_improvements, handle_hook, main |
|
||||
| subconscious_pass | 187 | 1/5 | main, _extract_topic, save_reflection, get_recent_findings |
|
||||
| construction_activation | 237 | 1/4 | _scaffold_for, _auto_structural_awareness, dispatch_from_decision |
|
||||
| dependency_tracker | 202 | 2/5 | check_propagation, get_full_map, main |
|
||||
| event_bus | 244 | 4/7 | _store_path, _content_hash, handle_hook |
|
||||
| salience_gate | 243 | 1/4 | test_salience, handle_hook, to_dict |
|
||||
| subsystem_health | 163 | 3/6 | set_auto_filer, record_failure, configure_logging |
|
||||
| warm_neuron_store | 122 | 3/6 | _db, neuron_slugs, handle_hook |
|
||||
| cognitive_budgeter | 208 | 1/3 | test_budgeter, to_dict |
|
||||
| construction_projection | 135 | 1/3 | _contract_entity, validate |
|
||||
| graph_projection | 108 | 3/5 | write_all, _now |
|
||||
| rune_profiler | 165 | 1/3 | profile_model_call, main |
|
||||
| stage_guards | 281 | 7/9 | to_dict, chk |
|
||||
| structural_migration | 77 | 2/4 | apply_migration, consent_brief |
|
||||
| construction_depth | 138 | 1/2 | to_dict |
|
||||
| recall_router | 162 | 2/3 | handle_hook |
|
||||
|
||||
## FULL — ported, parity-gated
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| recall_uplift | 141 | 1/1 | RUST |
|
||||
| rune_spinner | 130 | 4/5 | RUST |
|
||||
| wait_guard | 116 | 1/1 | RUST |
|
||||
| failure_recovery | 108 | 1/1 | RUST |
|
||||
| programme_gates | 85 | 4/5 | RUST |
|
||||
| impact_preview | 69 | 2/2 | RUST |
|
||||
| route_risk_flags | 37 | 1/1 | RUST |
|
||||
@@ -0,0 +1,60 @@
|
||||
# SUPERSESSION REPORT — programmatic evidence
|
||||
_Created 2026-10-07. Question: is curator superseded by memory_consolidation? Can supersession be determined programmatically?_
|
||||
|
||||
## Answer in one paragraph
|
||||
|
||||
No — they are NOT supersessions of each other. They are two different machines in one pipeline: **memory_consolidation compresses what already exists** (warm neurons, graph entities, abstraction facts → consolidated knowledge records), while **curator gates what gets written** (evidence → worthiness filter → graded routing → tiers). Both are wired into sleep_time's nightly run: consolidation runs in-process, curator runs as a subprocess. The real finding is darker: **curator has NEVER successfully run in production** — three of its four dependencies were never ported from the Odysseus branch, so every invocation died at import. Both diagnoses were reached programmatically.
|
||||
|
||||
## The programmatic supersession test (reusable)
|
||||
|
||||
Six signals, each binary and evidence-based (`/tmp/ss_engine.py`, promote to scripts/):
|
||||
|
||||
| Signal | Test | Weight |
|
||||
|---|---|---|
|
||||
| R1 Import graph | production importers vs canary-only importers | high |
|
||||
| R2 Subprocess summon | module executed as a script by another module | high |
|
||||
| R3 Output artifacts | documented output files exist with recent mtimes | high |
|
||||
| R4 Function overlap | name overlap with the suspected successor | low (weak signal) |
|
||||
| R5 Importability | `import X` succeeds (phantom dep = dead) | gate — fails = dead regardless |
|
||||
| R6 End-to-end probe | documented entry path executes with real input | decisive |
|
||||
|
||||
## Applied to curator vs memory_consolidation
|
||||
|
||||
| Signal | curator | memory_consolidation |
|
||||
|---|---|---|
|
||||
| R1 | 0 production importers | 2 (memory_selector, sleep_time) |
|
||||
| R2 | summoned as script by sleep_time + dependency_tracker | summoned by dependency_tracker |
|
||||
| R3 | **index/blocks_meta.json DOES NOT EXIST — no journal files — never wrote output** | graph.sqlite exists but **stale (Aug 22)**; consolidation/*.json never created |
|
||||
| R4 | 2% overlap — different function sets (confirms: different jobs) | 2% |
|
||||
| R5 | importable NOW (after repair); dead since #314 | importable |
|
||||
| R6 | dry-run probe NOW green (post-repair); was JSONDecodeError-crash | consolidate() wired, but its outputs absent too |
|
||||
|
||||
## Verdict
|
||||
|
||||
1. **NOT a supersession.** 2% function overlap + different roles: curator = write-path gate (worthiness → grade → tier), memory_consolidation = read-path compressor (existing memories → knowledge records). One writes, one compresses. Both belong.
|
||||
2. **Both are broken in production.** Curator never ran (phantom deps). memory_consolidation's outputs are stale/absent — its last graph write was Aug 22. The whole sleep-time memory pipeline has been quietly degraded.
|
||||
3. **Disposition: REPAIR, not retire.** grading.py + worthiness.py are now recovered from the Odysseus source branch (provenance, not reconstruction) with frozen tests; curator's dry-run executes end-to-end. Remaining phantom for curator: lexicon_evolution (guarded — degrades), aaak (guarded — optional compression). Both degradable, not blocking.
|
||||
|
||||
## The phantom-import census (the general finding)
|
||||
|
||||
16 phantom imports found across src/rune. The important ones:
|
||||
|
||||
| Module | Importers | Guarded? | Disposition |
|
||||
|---|---|---|---|
|
||||
| worthiness | curator | NO (hard crash) | ✅ RECOVERED from Odysseus + frozen tests |
|
||||
| grading | curator | NO | ✅ RECOVERED from Odysseus + frozen tests |
|
||||
| lexicon_evolution | curator | yes (degrades) | recover-or-rename decision needed |
|
||||
| aaak | curator, memory_store, warm_router | yes | optional compression; recover or accept degrade |
|
||||
| build_pipeline_policy | prompt_sync | NO | phantom — prompt_sync is dead on arrival |
|
||||
| gitea_auth | forge | NO | forge.py dead; is forge itself superseded? |
|
||||
| faster_whisper, pynput | voicebox | yes | optional deps — correct as-is |
|
||||
| research_lens | claim_audit | yes | lives in ~/.config/opencode/scripts — cross-repo dep |
|
||||
| ep_loader | 5 modules | false positive | resolves via sys.path hack to src/rust/ |
|
||||
| autonomy_loop, cognitive_loop, dream_cycle | rune_tui | yes | TUI plugins — recover or retire |
|
||||
| commitments, dedupe_policy, local_memory | warm_router, memory_store, sleep_time | yes | recover-or-accept-degrade |
|
||||
|
||||
**Hard-crash phantoms mean the owning module is dead code:** prompt_sync, forge, and (until today) curator.
|
||||
|
||||
## What the dictonomy rebase inherits
|
||||
|
||||
The Go/Rust port queue should SKIP dead code until its disposition is decided. A module that never ran has no behavioral parity to prove — porting it would faithfully reproduce a ghost. The audit instrument now needs an R5 importability column so dead modules are marked before they're counted.
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.99.10"
|
||||
version = "0.117.2"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ idna==3.20
|
||||
# anyio
|
||||
# httpx
|
||||
# yarl
|
||||
iniconfig==2.3.0
|
||||
iniconfig==2.3.1
|
||||
# via pytest
|
||||
jinja2==3.1.6
|
||||
# via diff-cover
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dichotomy_audit.py — the honest completion instrument for the dichotomy rebase.
|
||||
|
||||
Function-level, not directory-level: a module counts as covered only when its
|
||||
PYTHON FUNCTIONS have identifiable counterparts in the Go/Rust implementation.
|
||||
A Go directory with 3 structs and none of the module's 70 functions is a
|
||||
facade, and this audit names it as one.
|
||||
|
||||
Matching rule (name-preserving ports): a Python function name matches when
|
||||
its snake_case form, or its Go/Rust camelCase/PascalCase form, appears in the
|
||||
candidate implementation sources. Deliberately strict — novel Go names that
|
||||
don't echo the Python names do NOT count, because unverifiable "trust me,
|
||||
it's in there somewhere" is exactly the failure mode this instrument exists
|
||||
to kill.
|
||||
|
||||
Usage: python3 scripts/dichotomy_audit.py [--write]
|
||||
--write regenerates docs/dichotomy-rebase-honest-audit.md
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "src" / "rune"
|
||||
GO = ROOT / "src" / "go"
|
||||
RUST = ROOT / "src" / "rust"
|
||||
REPORT = ROOT / "docs" / "dichotomy-rebase-honest-audit.md"
|
||||
CLASSIFICATION = ROOT / "store" / "rebase" / "classification.json"
|
||||
|
||||
TEST_PREFIXES = ("test_",)
|
||||
# Modules that stay Python by decision (test infra stays pytest)
|
||||
TEST_INFRA = {
|
||||
"conftest", "test_truth", "test_adaptive_router", "test_canaries",
|
||||
}
|
||||
RETIRES = {"memory_env", "__main__"}
|
||||
|
||||
|
||||
def snake_to_camel(name: str) -> list[str]:
|
||||
"""Candidate identifier forms for a Python name in Go/Rust sources."""
|
||||
parts = name.split("_")
|
||||
camel = parts[0] + "".join(p.capitalize() for p in parts[1:])
|
||||
pascal = "".join(p.capitalize() for p in parts)
|
||||
return [name, camel, pascal]
|
||||
|
||||
|
||||
def module_functions(path: Path) -> list[str]:
|
||||
"""Every function and method name defined in the module."""
|
||||
try:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
|
||||
except SyntaxError:
|
||||
return []
|
||||
names = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if node.name.startswith("__") and node.name.endswith("__"):
|
||||
continue # dunders are runtime machinery, not port surface
|
||||
names.append(node.name)
|
||||
return names
|
||||
|
||||
|
||||
def load_classification() -> dict:
|
||||
if CLASSIFICATION.exists():
|
||||
return json.loads(CLASSIFICATION.read_text())
|
||||
return {}
|
||||
|
||||
|
||||
def implementation_corpus() -> dict[str, str]:
|
||||
"""module-kebab -> concatenated Go/Rust source text for that module."""
|
||||
corpus: dict[str, str] = {}
|
||||
|
||||
for d in sorted(GO.iterdir()):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
chunks = []
|
||||
for f in d.rglob("*.go"):
|
||||
if f.name.endswith("_test.go"):
|
||||
continue
|
||||
chunks.append(f.read_text(encoding="utf-8", errors="replace"))
|
||||
if chunks:
|
||||
corpus[d.name] = "\n".join(chunks)
|
||||
|
||||
# Survivor packages (per-crate Go membranes under cognition/)
|
||||
cog = GO / "cognition"
|
||||
if cog.exists():
|
||||
for d in sorted(cog.iterdir()):
|
||||
if d.is_dir():
|
||||
chunks = [f.read_text(encoding="utf-8", errors="replace")
|
||||
for f in d.rglob("*.go") if not f.name.endswith("_test.go")]
|
||||
if chunks:
|
||||
corpus[f"cognition-{d.name}"] = "\n".join(chunks)
|
||||
|
||||
for d in sorted(RUST.iterdir()):
|
||||
src = d / "src"
|
||||
if src.exists():
|
||||
chunks = [f.read_text(encoding="utf-8", errors="replace")
|
||||
for f in src.rglob("*.rs")]
|
||||
if chunks:
|
||||
corpus[d.name] = "\n".join(chunks)
|
||||
return corpus
|
||||
|
||||
|
||||
def module_to_keys(module_stem: str) -> list[str]:
|
||||
"""Candidate implementation keys for a Python module stem."""
|
||||
kebab = module_stem.replace("_", "-")
|
||||
keys = [kebab, module_stem]
|
||||
# Deep ports and multi-module binaries register aliases:
|
||||
aliases = {
|
||||
"rune_operator": ["operator", "operator-deep", "operator-support", "core-server"],
|
||||
"rune_tui": ["tui"],
|
||||
"rune_operator_cli": ["operator"],
|
||||
"memory_store": ["memory-ops", "memory-store-deep"],
|
||||
"warm_router": ["warm-router", "operator-support"],
|
||||
"tool_executor": ["tool-executor", "local-tool-runtime", "tool-protocol", "tool-protocol-rw"],
|
||||
"local_tool_runtime": ["local-tool-runtime", "operator"],
|
||||
"provider_registry": ["provider-registry", "model-connect"],
|
||||
"adaptive_router": ["adaptive-router", "operator-support"],
|
||||
"hook_engine": ["hook-engine"],
|
||||
"event_bus": ["event-bus"],
|
||||
"event_envelope": ["event-bus"],
|
||||
"message_queue": ["message-queue"],
|
||||
"subsystem_health": ["subsystem-health"],
|
||||
"parallel_processor": ["parallel-processor"],
|
||||
"connection_pool": ["connection-pool"],
|
||||
"recall_router": ["recall-router"],
|
||||
"cron_engine": ["cron-engine"],
|
||||
"cache": ["cache"],
|
||||
"night_mode": ["night-mode"],
|
||||
"work_manager": ["work-manager"],
|
||||
"failure_auto_filer": ["failure-filer"],
|
||||
"microkernel": ["microkernel"],
|
||||
"construction_interface": ["cognition-cognition", "cognition"],
|
||||
"construction_decision": ["cognition-cognition", "cognition"],
|
||||
"rune_spine": ["cognition-spine", "cognition"],
|
||||
"spine": ["cognition-spine", "cognition"],
|
||||
"memory_storage": ["cognition-storage", "cognition"],
|
||||
"memory_retrieval": ["cognition-retrieval", "cognition"],
|
||||
"scaffold_core": ["cognition-scaffold", "cognition"],
|
||||
"evidence_provenance": ["cognition-provenance", "cognition"],
|
||||
"memory_scoring": ["cognition-scoring", "cognition"],
|
||||
"capability_scoring": ["cognition-capability", "cognition"],
|
||||
"token_counting": ["cognition-token", "cognition"],
|
||||
"query_memory": ["cognition-query", "cognition"],
|
||||
}
|
||||
if module_stem in aliases:
|
||||
keys = aliases[module_stem] + keys
|
||||
# cognition_core broker.rs hosts many kernels
|
||||
if module_stem in {
|
||||
"tier_manager", "safety_override", "pins", "identity_anchors",
|
||||
"abstraction", "growth_delta", "witness", "persona_gate",
|
||||
"memory_consolidation", "drift_compression", "context_broker",
|
||||
"persona_definition", "goal",
|
||||
}:
|
||||
keys.append("cognition_core")
|
||||
keys.append("cognition-cognition")
|
||||
return keys
|
||||
|
||||
|
||||
def audit() -> dict:
|
||||
classification = load_classification()
|
||||
corpus = implementation_corpus()
|
||||
|
||||
rows = []
|
||||
for path in sorted(SRC.glob("*.py")):
|
||||
stem = path.stem
|
||||
if stem in RETIRES:
|
||||
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
|
||||
"fns": 0, "matched": 0, "side": "RETIRES", "status": "RETIRES", "missing": []})
|
||||
continue
|
||||
if stem.startswith(TEST_PREFIXES) or stem in TEST_INFRA:
|
||||
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
|
||||
"fns": 0, "matched": 0, "side": "TEST", "status": "TEST", "missing": []})
|
||||
continue
|
||||
fns = module_functions(path)
|
||||
lines = len(path.read_text(errors="replace").splitlines())
|
||||
entry = classification.get(stem) or classification.get(path.name) or {}
|
||||
side = entry.get("side", entry.get("language", "unclassified"))
|
||||
|
||||
sources = []
|
||||
for key in module_to_keys(stem):
|
||||
if key in corpus:
|
||||
sources.append(corpus[key])
|
||||
blob = "\n".join(sources)
|
||||
|
||||
matched, missing = 0, []
|
||||
for fn in fns:
|
||||
hits = any(cand in blob for cand in snake_to_camel(fn))
|
||||
if hits:
|
||||
matched += 1
|
||||
else:
|
||||
missing.append(fn)
|
||||
|
||||
ratio = matched / len(fns) if fns else (1.0 if lines < 30 else 0.0)
|
||||
if stem in RETIRES:
|
||||
status = "RETIRES"
|
||||
elif not blob:
|
||||
status = "NOT TOUCHED"
|
||||
elif ratio >= 0.8:
|
||||
status = "FULL"
|
||||
elif ratio >= 0.2:
|
||||
status = "PARTIAL"
|
||||
else:
|
||||
status = "NOT TOUCHED"
|
||||
rows.append({"module": stem, "lines": lines, "fns": len(fns),
|
||||
"matched": matched, "side": side, "status": status, "missing": missing[:12]})
|
||||
|
||||
return {"rows": rows}
|
||||
|
||||
|
||||
def summarize(result: dict) -> str:
|
||||
rows = result["rows"]
|
||||
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
|
||||
full = [r for r in runtime if r["status"] == "FULL"]
|
||||
partial = [r for r in runtime if r["status"] == "PARTIAL"]
|
||||
untouched = [r for r in runtime if r["status"] == "NOT TOUCHED"]
|
||||
total_fns = sum(r["fns"] for r in runtime)
|
||||
total_matched = sum(r["matched"] for r in runtime)
|
||||
total_lines = sum(r["lines"] for r in runtime)
|
||||
fn_pct = 100 * total_matched / total_fns if total_fns else 0
|
||||
|
||||
out = []
|
||||
out.append("# DICHOTOMY REBASE — HONEST AUDIT (function-level)")
|
||||
out.append("")
|
||||
out.append("_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._")
|
||||
out.append("")
|
||||
out.append(f"**Function-level completion: {fn_pct:.0f}%** ({total_matched}/{total_fns} runtime functions matched)")
|
||||
out.append("")
|
||||
out.append("| Status | Modules | Lines | Meaning |")
|
||||
out.append("|---|---|---|---|")
|
||||
out.append(f"| FULL | {len(full)} | {sum(r['lines'] for r in full)} | 80%+ of functions ported |")
|
||||
out.append(f"| PARTIAL | {len(partial)} | {sum(r['lines'] for r in partial)} | 20–80% — real logic exists, gaps named below |")
|
||||
out.append(f"| NOT TOUCHED | {len(untouched)} | {sum(r['lines'] for r in untouched)} | no name-preserving implementation |")
|
||||
out.append(f"| Total runtime | {len(runtime)} | {total_lines} | |")
|
||||
out.append("")
|
||||
|
||||
if untouched:
|
||||
out.append("## NOT TOUCHED — the port queue, in priority order (largest first)")
|
||||
out.append("")
|
||||
out.append("| Module | Lines | Fns | Side |")
|
||||
out.append("|---|---|---|---|")
|
||||
for r in sorted(untouched, key=lambda r: -r["lines"]):
|
||||
out.append(f"| {r['module']} | {r['lines']} | {r['fns']} | {r['side']} |")
|
||||
out.append("")
|
||||
if partial:
|
||||
out.append("## PARTIAL — gaps to close (matched/total fns; first missing named)")
|
||||
out.append("")
|
||||
out.append("| Module | Lines | Coverage | Missing (first named) |")
|
||||
out.append("|---|---|---|---|")
|
||||
for r in sorted(partial, key=lambda r: -(r["fns"] - r["matched"])):
|
||||
miss = ", ".join(r["missing"][:6]) if r["missing"] else "(structural)"
|
||||
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {miss} |")
|
||||
out.append("")
|
||||
out.append("## FULL — ported, parity-gated")
|
||||
out.append("")
|
||||
out.append("| Module | Lines | Fns | Side |")
|
||||
out.append("|---|---|---|---|")
|
||||
for r in sorted(full, key=lambda r: -r["lines"]):
|
||||
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {r['side']} |")
|
||||
out.append("")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
write = "--write" in sys.argv
|
||||
result = audit()
|
||||
report = summarize(result)
|
||||
if write:
|
||||
REPORT.write_text(report)
|
||||
print(f"written: {REPORT}")
|
||||
# stderr summary for the session log
|
||||
rows = result["rows"]
|
||||
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
|
||||
tf = sum(r["fns"] for r in runtime)
|
||||
tm = sum(r["matched"] for r in runtime)
|
||||
print(f"FUNCTION-LEVEL: {tm}/{tf} ({100*tm/tf if tf else 0:.0f}%) | "
|
||||
f"FULL {sum(1 for r in runtime if r['status']=='FULL')} | "
|
||||
f"PARTIAL {sum(1 for r in runtime if r['status']=='PARTIAL')} | "
|
||||
f"NOT TOUCHED {sum(1 for r in runtime if r['status']=='NOT TOUCHED')}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
import json, sys
|
||||
sys.path.insert(0, 'src')
|
||||
from rune.adaptive_router import (
|
||||
classify_route, derive_sufficiency, body_has_floor_trigger, floor_applies,
|
||||
escalate_route, downgrade_route, escalate_on_verification_failure,
|
||||
build_research_dispatch, FloorViolation,
|
||||
)
|
||||
|
||||
cases = []
|
||||
ESC_EVIDENCE = "the fix works locally but the production parser accepts different input shapes — input contract uncertain"
|
||||
|
||||
cr_cases = [
|
||||
("local_fix_happy", "fix the broken typewriter regex in warm_router, crash on empty input",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
("standard_change", "add a progress bar to the export dialog",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
("defect_word_absent", "update the version number in the footer",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=False, narrow_and_reversible=True)),
|
||||
("floor_new_subsystem", "adopt a new subsystem for memory indexing, replacement of spine",
|
||||
dict(has_local_repro=False, owning_authority_known=False)),
|
||||
("risk_new_authority", "this creates a new authority path for dispatch",
|
||||
dict(has_local_repro=False, owning_authority_known=False)),
|
||||
("uncertainty_competing", "there are competing designs here, expensive reversal likely",
|
||||
dict(has_local_repro=True, owning_authority_known=True)),
|
||||
("not_grounded", "add support for markdown export",
|
||||
dict(has_local_repro=False, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
("inspection_verb", "explain why the failing test fails",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
]
|
||||
for name, body, kw in cr_cases:
|
||||
d = classify_route(body, **kw)
|
||||
cases.append({"fn": "classify", "name": name,
|
||||
"input": {"body": body, **kw, "pipeline_sha": "UNKNOWN"},
|
||||
"expected": {"route": d.route, "confidence": d.confidence, "reasons": d.reasons,
|
||||
"risk": d.risk_flags, "uncertainty": d.uncertainty_flags,
|
||||
"floor": d.floor, "task_description": d.task_description}})
|
||||
|
||||
ds_cases = [
|
||||
("grounded_fix", "fix crash", "the parser crashes on empty input, see tests/test_parse.py"),
|
||||
("mechanism", "investigate", "performance is bad under load, benchmark needed"),
|
||||
("external_url", "read the docs", "see https://example.com/api docs for the endpoint"),
|
||||
("version_risk", "upgrade", "upgrade the library to latest version, current behaviour differs"),
|
||||
("inspection", "explain", "why does this happen"),
|
||||
("new_subsystem", "adopt", "this adopts a replacement architecture"),
|
||||
("neutral", "write", "a poem about the sea"),
|
||||
]
|
||||
for name, title, body in ds_cases:
|
||||
s = derive_sufficiency(title, body)
|
||||
cases.append({"fn": "derive", "name": name,
|
||||
"input": {"title": title, "body": body, "ambiguous": False},
|
||||
"expected": {"local_authority": s.local_authority,
|
||||
"external_dependency": s.external_dependency,
|
||||
"version_freshness_risk": s.version_freshness_risk,
|
||||
"verifiability": s.verifiability, "scope_novelty": s.scope_novelty,
|
||||
"unknowns": s.unknowns(),
|
||||
"task_class": s.provenance.get("task_class_language")}})
|
||||
|
||||
cases.append({"fn": "floor_trigger", "name": "trigger_yes",
|
||||
"input": {"body": "adopt a replacement for the cache"},
|
||||
"expected": {"result": body_has_floor_trigger("adopt a replacement for the cache")}})
|
||||
cases.append({"fn": "floor_trigger", "name": "trigger_no",
|
||||
"input": {"body": "fix the typo"},
|
||||
"expected": {"result": body_has_floor_trigger("fix the typo")}})
|
||||
cases.append({"fn": "floor_applies", "name": "res_yes", "input": {"route": "RESEARCHED_CHANGE", "flags": ["new_authority"]},
|
||||
"expected": {"result": floor_applies("RESEARCHED_CHANGE", ["new_authority"])}})
|
||||
cases.append({"fn": "floor_applies", "name": "local_no", "input": {"route": "LOCAL_FIX", "flags": ["new_authority"]},
|
||||
"expected": {"result": floor_applies("LOCAL_FIX", ["new_authority"])}})
|
||||
|
||||
base = classify_route("fix the crash", has_local_repro=True, owning_authority_known=True,
|
||||
testable_locally=True, narrow_and_reversible=True)
|
||||
esc = escalate_route(base, "RESEARCHED_CHANGE", "verify_fail", ESC_EVIDENCE)
|
||||
cases.append({"fn": "escalate", "name": "esc",
|
||||
"input": {"decision_route": base.route, "new_route": "RESEARCHED_CHANGE",
|
||||
"trigger": "verify_fail", "evidence": ESC_EVIDENCE},
|
||||
"expected": {"route": esc.route, "previous": esc.previous_route,
|
||||
"decided_by": esc.decided_by, "trigger": esc.trigger}})
|
||||
|
||||
try:
|
||||
downgrade_route(base, "STANDARD_CHANGE", "try", "ev")
|
||||
cases.append({"fn": "downgrade", "name": "dg_ok", "input": {"base_route": base.route, "base_floor": False,
|
||||
"new_route": "STANDARD_CHANGE"}, "expected": {"route": "STANDARD_CHANGE", "error": None}})
|
||||
except FloorViolation:
|
||||
cases.append({"fn": "downgrade", "name": "dg_ok", "input": {}, "expected": {"route": None, "error": "FloorViolation"}})
|
||||
|
||||
floored = classify_route("adopt a replacement for the memory index")
|
||||
try:
|
||||
downgrade_route(floored, "STANDARD_CHANGE", "efficiency", "faster")
|
||||
cases.append({"fn": "downgrade", "name": "dg_floored", "input": {"base_floor": True, "new_route": "STANDARD_CHANGE"},
|
||||
"expected": {"route": "STANDARD_CHANGE", "error": None}})
|
||||
except FloorViolation:
|
||||
cases.append({"fn": "downgrade", "name": "dg_floored", "input": {"base_floor": True, "new_route": "STANDARD_CHANGE"},
|
||||
"expected": {"route": None, "error": "FloorViolation"}})
|
||||
|
||||
try:
|
||||
escalate_on_verification_failure(base, "test failed", "/tmp/ar_probe.jsonl")
|
||||
cases.append({"fn": "evidence_short", "name": "short", "input": {"evidence": "test failed"},
|
||||
"expected": {"result": "ACCEPTED"}})
|
||||
except ValueError:
|
||||
cases.append({"fn": "evidence_short", "name": "short", "input": {"evidence": "test failed"},
|
||||
"expected": {"result": "REJECTED"}})
|
||||
|
||||
disp = build_research_dispatch("T-1", "research the memory index options", floored)
|
||||
cases.append({"fn": "dispatch", "name": "disp",
|
||||
"input": {"task_id": "T-1", "description": "research the memory index options"},
|
||||
"expected": {"task_id": disp["task_id"], "title": disp["title"], "route": disp["route"],
|
||||
"floor": disp["floor"], "executor_entry": disp["executor_entry"]}})
|
||||
|
||||
json.dump(cases, open('src/go/adaptive-router/truth.json', 'w'), indent=1)
|
||||
print(f"{len(cases)} truth cases with inputs")
|
||||
for c in cases:
|
||||
print(f" {c['fn']:14} {c['name']}")
|
||||
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_tool_executor_truth.py — regenerate the tool-executor truth batteries
|
||||
HERMETICALLY (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
|
||||
Why: the original batch-2/3/4 generators baked the generation host's temp
|
||||
paths (/mnt/workdrive/tmp*) into truth_fileops.json / truth_ops.json /
|
||||
truth_paths.json — inputs AND expectations. CI's runner user cannot read
|
||||
those paths, so the parity gate failed with permission-denied (go-surface,
|
||||
run 19792 job 56712) despite a locally green tree.
|
||||
|
||||
Design (the environment leak, closed):
|
||||
- Fixtures are created at generation time under roots the ORACLE ITSELF
|
||||
allows: read root = first creatable dir in ALLOWED_READ_DIRS (prefers
|
||||
/tmp), write root = first creatable dir in ALLOWED_WRITE_DIRS.
|
||||
- Every occurrence of those roots — in inputs AND expectations — is
|
||||
replaced with {{TMP}} / {{WROOT}} placeholders.
|
||||
- Each truth file carries a `fixtures` manifest (files + dirs) so the Go
|
||||
test recreates the SAME fixture content under ITS OWN roots at runtime.
|
||||
Both sides then run the same case semantics on their own environment;
|
||||
substituted bytes must agree exactly.
|
||||
- The grep-hit comparison stays order-insensitive (sorted line sets) on the
|
||||
Go side: the oracle's os.walk order is OS-scheduling dependent, not a
|
||||
parity-relevant property. Everything else is exact after substitution.
|
||||
|
||||
Oracle authority: src/rune/tool_executor.py is called LIVE for every case
|
||||
except the two cmd_gate verdicts whose oracle path would execute a shell
|
||||
command (plain_ok / shell_ok use harmless commands instead: "ls -la" and
|
||||
"true" — the gate branches they exercise do not inspect command content
|
||||
beyond non-emptiness).
|
||||
|
||||
Usage: python3 scripts/gen_tool_executor_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import tool_executor as te # noqa: E402 — the live oracle
|
||||
|
||||
|
||||
def mk_root(dirs, tag):
|
||||
"""First creatable temp dir under the given allowed dirs."""
|
||||
for d in dirs:
|
||||
try:
|
||||
return tempfile.mkdtemp(prefix="rune-truth-", dir=d)
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError(f"no creatable root among {dirs}")
|
||||
|
||||
|
||||
def build_fixtures(root):
|
||||
"""The shared fixture set: exactly what the cases observe."""
|
||||
(Path(root) / "w.txt").write_text(
|
||||
"".join(f"line {i}\n" for i in range(1, 21)), encoding="utf-8"
|
||||
)
|
||||
(Path(root) / "a.txt").write_text("the spine rebuild parity\n", encoding="utf-8")
|
||||
(Path(root) / "b.log").write_text("spine appears here too\n", encoding="utf-8")
|
||||
(Path(root) / "file.txt").write_text("content\n", encoding="utf-8")
|
||||
(Path(root) / "adir").mkdir()
|
||||
return {
|
||||
"files": [
|
||||
{"path": "{{TMP}}/w.txt",
|
||||
"content": "".join(f"line {i}\n" for i in range(1, 21))},
|
||||
{"path": "{{TMP}}/a.txt", "content": "the spine rebuild parity\n"},
|
||||
{"path": "{{TMP}}/b.log", "content": "spine appears here too\n"},
|
||||
{"path": "{{TMP}}/file.txt", "content": "content\n"},
|
||||
],
|
||||
"dirs": ["{{TMP}}/adir"],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
read_root = mk_root(te.ALLOWED_READ_DIRS, "read")
|
||||
write_root = mk_root(te.ALLOWED_WRITE_DIRS, "write")
|
||||
fixtures = build_fixtures(read_root)
|
||||
|
||||
wpath = f"{read_root}/w.txt"
|
||||
edited = "line 1\nCHANGED\n" + "".join(f"line {i}\n" for i in range(3, 21))
|
||||
|
||||
cases_fileops = [
|
||||
{
|
||||
"fn": "read_window", "name": "mid",
|
||||
"input": {"path": f"{wpath}", "offset": 3, "limit": 5},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "read_window", "name": "default",
|
||||
"input": {"path": f"{wpath}"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "diff", "name": "simple",
|
||||
"input": {"path": wpath, "new_content": edited},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
r = te._read_file({"path": wpath, "offset": 3, "limit": 5})
|
||||
assert r["success"] is True, r
|
||||
cases_fileops[0]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._read_file({"path": wpath})
|
||||
assert r["success"] is True, r
|
||||
cases_fileops[1]["expected"] = {
|
||||
"ok": True, "lines": len(r["result"].split("\n")),
|
||||
}
|
||||
|
||||
diff = te.generate_diff(Path(wpath), edited)
|
||||
dlines = diff.split("\n")
|
||||
cases_fileops[2]["expected"] = {
|
||||
"diff_head": dlines[:3],
|
||||
"has_minus": any(l.startswith("-line 2") for l in dlines),
|
||||
"has_plus": any(l.startswith("+CHANGED") for l in dlines),
|
||||
}
|
||||
|
||||
cases_ops = [
|
||||
{
|
||||
"fn": "list_dir", "name": "basic",
|
||||
"input": {"path": read_root},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "list_dir", "name": "pattern",
|
||||
"input": {"path": read_root, "pattern": "*.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "grep", "name": "hit",
|
||||
"input": {"path": read_root, "pattern": "spine"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "grep", "name": "miss",
|
||||
"input": {"path": read_root, "pattern": "absent-token"},
|
||||
"expected": None,
|
||||
},
|
||||
# The gate branches: pipe_refused and shell_empty return BEFORE any
|
||||
# execution (live oracle); plain_ok / shell_ok would execute, so they
|
||||
# run harmless commands ("ls -la", "true") — the gate branches they
|
||||
# exercise do not inspect command content beyond non-emptiness.
|
||||
{
|
||||
"fn": "cmd_gate", "name": "pipe_refused",
|
||||
"input": {"command": "ls -la | head"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "plain_ok",
|
||||
"input": {"command": "ls -la"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "shell_ok",
|
||||
"input": {"command": "true", "shell": True},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "shell_empty",
|
||||
"input": {"shell": True},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
r = te._list_directory({"path": read_root})
|
||||
assert r["success"] is True, r
|
||||
cases_ops[0]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._list_directory({"path": read_root, "pattern": "*.txt"})
|
||||
assert r["success"] is True, r
|
||||
cases_ops[1]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._search_files({"path": read_root, "pattern": "spine"})
|
||||
assert r["success"] is True and r["result"] != "(no matches)", r
|
||||
cases_ops[2]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._search_files({"path": read_root, "pattern": "absent-token"})
|
||||
assert r["success"] is True and r["result"] == "(no matches)", r
|
||||
cases_ops[3]["expected"] = {"ok": True, "result": "(no matches)"}
|
||||
|
||||
r = te._run_command({"command": "ls -la | head"})
|
||||
cases_ops[4]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is False, r
|
||||
|
||||
r = te._run_command({"command": "ls -la"})
|
||||
cases_ops[5]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is True, r
|
||||
|
||||
r = te._run_command({"command": "true", "shell": True})
|
||||
cases_ops[6]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is True, r
|
||||
|
||||
r = te._run_command({"shell": True})
|
||||
cases_ops[7]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is False, r
|
||||
|
||||
cases_paths = [
|
||||
{
|
||||
"fn": "resolve", "name": "abs",
|
||||
"input": {"path": f"{read_root}/file.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "hit",
|
||||
"input": {"path": f"{read_root}/file.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "miss",
|
||||
"input": {"path": f"{read_root}/missing.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "blocked",
|
||||
"input": {"path": "/etc/hostname"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "protected",
|
||||
"input": {"path": "/etc/x.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "outside",
|
||||
"input": {"path": "/usr/share/x.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "ok",
|
||||
"input": {"path": f"{write_root}/new.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
resolved = str(te.resolve_path(f"{read_root}/file.txt"))
|
||||
cases_paths[0]["expected"] = {"resolved": resolved}
|
||||
|
||||
ok, msg, _ = te.validate_read_path(f"{read_root}/file.txt")
|
||||
cases_paths[1]["expected"] = {"ok": ok}
|
||||
assert ok is True, msg
|
||||
|
||||
ok, msg, _ = te.validate_read_path(f"{read_root}/missing.txt")
|
||||
cases_paths[2]["expected"] = {"ok": ok, "msg": msg}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_read_path("/etc/hostname")
|
||||
cases_paths[3]["expected"] = {"ok": ok}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_write_path("/etc/x.txt")
|
||||
cases_paths[4]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_write_path("/usr/share/x.txt")
|
||||
cases_paths[5]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, resolved = te.validate_write_path(f"{write_root}/new.txt")
|
||||
cases_paths[6]["expected"] = {"ok": ok, "resolved": str(resolved)}
|
||||
assert ok is True, msg
|
||||
|
||||
def templated(obj):
|
||||
s = json.dumps(obj, ensure_ascii=False)
|
||||
s = s.replace(read_root, "{{TMP}}").replace(write_root, "{{WROOT}}")
|
||||
return json.loads(s)
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)",
|
||||
},
|
||||
"fixtures": fixtures,
|
||||
"cases": None,
|
||||
}
|
||||
|
||||
for name, cases in (
|
||||
("truth_fileops.json", cases_fileops),
|
||||
("truth_ops.json", cases_ops),
|
||||
("truth_paths.json", cases_paths),
|
||||
):
|
||||
doc = dict(out)
|
||||
doc["cases"] = templated(cases)
|
||||
text = json.dumps(doc, indent=1, ensure_ascii=False)
|
||||
# Self-check: no generation-time root may leak into the artifact.
|
||||
assert read_root not in text and write_root not in text, name
|
||||
target = REPO / "src/go/tool-executor" / name
|
||||
target.write_text(text + "\n", encoding="utf-8")
|
||||
print(f"wrote {target} ({len(doc['cases'])} cases)")
|
||||
|
||||
# Cleanup the generation-time fixtures; the truth carries the manifest.
|
||||
import shutil
|
||||
shutil.rmtree(read_root, ignore_errors=True)
|
||||
shutil.rmtree(write_root, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""scaffolding_coverage.py — CI gate: every Go/Rust implementation module
|
||||
must map to a construction contract in store/scaffold/.
|
||||
|
||||
The absolute-scaffolding law (Nick, 2026-10-07): scaffolding is not optional
|
||||
process, it is the record. A Go/Rust unit without a contract whose
|
||||
affected_surface covers it is ungoverned code — this gate fails the build.
|
||||
|
||||
Rules:
|
||||
* Every directory under src/go/ (and every crate under src/rust/) must
|
||||
appear in at least one contract's scope.
|
||||
* Contracts are read from store/scaffold/ (the durable trace).
|
||||
* EXEMPT: crates that pre-date the pipeline (the 10 original kernels) are
|
||||
mapped via the legacy bootstrap contract; test fixtures and _test.go are
|
||||
not units.
|
||||
|
||||
Exit 0 = all covered. Exit 1 = ungoverned units (listed).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCAFFOLD = ROOT / "store" / "scaffold"
|
||||
GO = ROOT / "src" / "go"
|
||||
RUST = ROOT / "src" / "rust"
|
||||
|
||||
# Units predating the absolute law, retro-adopted 2026-10-07 (contracts
|
||||
# BUILDER-2026-10-07T11:32-33Z-*). The map is the adoption record; new units
|
||||
# MUST get their contract BEFORE their first commit lands.
|
||||
RETRO_ADOPTED = {
|
||||
"adaptive-router", "memory-store-deep", "operator-deep", "curator-core",
|
||||
# Original kernels + infra from the pre-pipeline era (bootstrap contract):
|
||||
"event-bus", "message-queue", "subsystem-health", "parallel-processor",
|
||||
"connection-pool", "recall-router", "cron-engine", "cache", "night-mode",
|
||||
"work-manager", "failure-filer", "microkernel", "tool-executor",
|
||||
"rune-entry", "hook-engine", "model-connect", "operator", "operator-support",
|
||||
"memory-ops", "sleep-time", "core-server", "context-broker", "stage-guards",
|
||||
"tool-protocol", "tool-protocol-rw", "witness", "curator", "elastic-cognition",
|
||||
"persona-gate", "scaffold", "abstraction", "tui", "warm-router",
|
||||
"provider-registry", "local-tool-runtime", "memory-store-deep",
|
||||
"cognition",
|
||||
# Rust kernels (the 10 originals + W6 wave — bootstrap-era + parity ports)
|
||||
"route_risk_flags", "impact_preview", "programme_gates", "structural_migration",
|
||||
"rune_spinner", "warm_neuron_store", "graph_projection", "failure_recovery",
|
||||
"construction_depth", "intent_continuity", "wait_guard", "task_persist",
|
||||
"response_adapter", "salience_gate", "time_awareness", "recall_uplift",
|
||||
"verification_layer", "project_registry", "behaviour_ledger", "audit_ledger",
|
||||
"cognitive_budgeter", "progressive_escalation", "shard_manager", "task_gate",
|
||||
"drift_core", "self_improve", "token_optimizer", "dependency_tracker",
|
||||
"context_pressure", "inference_trace", "wait_guard_corpus", "rune_profiler",
|
||||
"construction_projection", "outcome_ledger", "construction_mode",
|
||||
"drift_resume", "toolchain", "construction_commands", "construction_activation",
|
||||
"subconscious_pass", "attention_capsule", "contracts", "capability_fit",
|
||||
"capability_ledger", "memory_selector", "graph_memory", "claim_audit",
|
||||
"build_basis", "prompt_sync", "voice_sync", "layout_contract",
|
||||
"system_registry", "voice_hybrid", "capability_broker", "structural_store",
|
||||
"spine", "behavior", "construction_intent", "construction_decision",
|
||||
"construction_pattern", "structural_model", "programme_continuation",
|
||||
"cognition_core", "memory_retrieval", "memory_storage", "scaffold_core",
|
||||
"evidence_provenance", "rune_spine", "memory_scoring", "capability_scoring",
|
||||
"token_counting", "query_memory",
|
||||
"drift_observability", "iteration", "spine_loader", "testing_mode",
|
||||
"construction_interface", "construction_patterns_builtin", "drift_cache",
|
||||
"drift_ledger", "scaffold-membrane",
|
||||
}
|
||||
|
||||
|
||||
def contract_scopes() -> set[str]:
|
||||
"""Every surface named in any persisted contract under store/scaffold/."""
|
||||
scopes: set[str] = set()
|
||||
if not SCAFFOLD.exists():
|
||||
return scopes
|
||||
for f in SCAFFOLD.rglob("*.json"):
|
||||
try:
|
||||
data = json.loads(f.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
blobs = _iter_dicts(data)
|
||||
for d in blobs:
|
||||
for key in ("scope_files", "affected_surface", "surfaces", "scope"):
|
||||
v = d.get(key)
|
||||
if isinstance(v, list):
|
||||
for item in v:
|
||||
if isinstance(item, str):
|
||||
scopes.add(item)
|
||||
scopes.add(item.rstrip("/").split("/")[-1])
|
||||
elif isinstance(v, str):
|
||||
scopes.add(v)
|
||||
scopes.add(v.rstrip("/").split("/")[-1])
|
||||
return scopes
|
||||
|
||||
|
||||
def _iter_dicts(obj):
|
||||
if isinstance(obj, dict):
|
||||
yield obj
|
||||
for v in obj.values():
|
||||
yield from _iter_dicts(v)
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
yield from _iter_dicts(v)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
scopes = contract_scopes()
|
||||
ungoverned = []
|
||||
|
||||
if GO.exists():
|
||||
for d in sorted(GO.iterdir()):
|
||||
if d.is_dir() and (d / "go.mod").exists():
|
||||
if d.name not in scopes and d.name not in RETRO_ADOPTED:
|
||||
ungoverned.append(f"src/go/{d.name}/")
|
||||
|
||||
if RUST.exists():
|
||||
for d in sorted(RUST.iterdir()):
|
||||
if (d / "Cargo.toml").exists():
|
||||
if d.name not in scopes and d.name not in RETRO_ADOPTED:
|
||||
ungoverned.append(f"src/rust/{d.name}/")
|
||||
|
||||
if ungoverned:
|
||||
print("UNGOVERNED UNITS — no construction contract covers them")
|
||||
print("(absolute-scaffolding law 2026-10-07: open a contract via")
|
||||
print(" construction_interface.submit_need BEFORE the next commit):")
|
||||
for u in ungoverned:
|
||||
print(f" {u}")
|
||||
return 1
|
||||
|
||||
print(f"scaffolding coverage: all Go/Rust units governed "
|
||||
f"({len(RETRO_ADOPTED)} retro-adopted/bootstrap, "
|
||||
f"{len(scopes)} contract scopes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,127 @@
|
||||
// cli.go — JSON CLI: one op per invocation (matches the Go binary family
|
||||
// contract). Op dispatch is a thin shell over the ported functions so the
|
||||
// truth test exercises the SAME code paths operators call.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var req map[string]interface{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
|
||||
emit(map[string]interface{}{"error": "bad JSON input: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
op, _ := req["op"].(string)
|
||||
raw, _ := json.Marshal(req)
|
||||
switch op {
|
||||
case "classify":
|
||||
var in ClassifyInput
|
||||
json.Unmarshal(raw, &in)
|
||||
d := ClassifyRoute(in)
|
||||
risk, unc := d.RiskFlags, d.UncertaintyFlags
|
||||
if risk == nil {
|
||||
risk = []string{}
|
||||
}
|
||||
if unc == nil {
|
||||
unc = []string{}
|
||||
}
|
||||
emit(map[string]interface{}{"route": d.Route, "confidence": d.Confidence, "reasons": d.Reasons,
|
||||
"risk": risk, "uncertainty": unc, "floor": d.Floor, "task_description": d.TaskDescription})
|
||||
case "derive":
|
||||
var in DeriveInput
|
||||
json.Unmarshal(raw, &in)
|
||||
s := DeriveSufficiency(in)
|
||||
unk := s.Unknowns()
|
||||
if unk == nil {
|
||||
unk = []string{}
|
||||
}
|
||||
emit(map[string]interface{}{"local_authority": s.LocalAuthority,
|
||||
"external_dependency": s.ExternalDependency,
|
||||
"version_freshness_risk": s.VersionFreshnessRisk,
|
||||
"verifiability": s.Verifiability, "scope_novelty": s.ScopeNovelty,
|
||||
"unknowns": unk, "task_class": s.Provenance["task_class_language"]})
|
||||
case "floor_trigger":
|
||||
body, _ := req["body"].(string)
|
||||
emit(map[string]interface{}{"result": BodyHasFloorTrigger(body)})
|
||||
case "floor_applies":
|
||||
route, _ := req["route"].(string)
|
||||
var flags []string
|
||||
flagsRaw, _ := json.Marshal(req["flags"])
|
||||
json.Unmarshal(flagsRaw, &flags)
|
||||
emit(map[string]interface{}{"result": FloorApplies(route, flags)})
|
||||
case "escalate":
|
||||
newRoute, _ := req["new_route"].(string)
|
||||
trigger, _ := req["trigger"].(string)
|
||||
evidence, _ := req["evidence"].(string)
|
||||
base := baseDecisionFrom(req)
|
||||
out, err := escalateRoute(base, newRoute, trigger, evidence)
|
||||
if err != nil {
|
||||
emit(map[string]interface{}{"error": "ValueError"})
|
||||
os.Exit(1)
|
||||
}
|
||||
emit(map[string]interface{}{"route": out.Route, "previous": out.PreviousRoute,
|
||||
"decided_by": out.DecidedBy, "trigger": out.Trigger})
|
||||
case "downgrade":
|
||||
newRoute, _ := req["new_route"].(string)
|
||||
trigger, _ := req["trigger"].(string)
|
||||
evidence, _ := req["evidence"].(string)
|
||||
base := baseDecisionFrom(req)
|
||||
out, err := downgradeRoute(base, newRoute, trigger, evidence)
|
||||
if err != nil {
|
||||
emit(map[string]interface{}{"error": "FloorViolation"})
|
||||
os.Exit(1)
|
||||
}
|
||||
emit(map[string]interface{}{"route": out.Route})
|
||||
case "verify_fail":
|
||||
evidence, _ := req["evidence"].(string)
|
||||
base := baseDecisionFrom(req)
|
||||
if _, err := EscalateOnVerificationFailure(base, evidence); err != nil {
|
||||
emit(map[string]interface{}{"result": "REJECTED"})
|
||||
return
|
||||
}
|
||||
emit(map[string]interface{}{"result": "ACCEPTED"})
|
||||
case "dispatch":
|
||||
taskID, _ := req["task_id"].(string)
|
||||
description, _ := req["description"].(string)
|
||||
base := baseDecisionFrom(req)
|
||||
emit(BuildResearchDispatch(taskID, description, base))
|
||||
default:
|
||||
emit(map[string]interface{}{"error": "unknown op: " + op})
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// baseDecisionFrom builds the minimal base decision the transition ops need
|
||||
// (the truth battery passes base_route/base_floor; full decisions come
|
||||
// through the "decision" key when the operator delegates).
|
||||
func baseDecisionFrom(req map[string]interface{}) RouteDecision {
|
||||
d := RouteDecision{Reasons: []string{}}
|
||||
if dec, ok := req["decision"].(map[string]interface{}); ok {
|
||||
b, _ := json.Marshal(dec)
|
||||
json.Unmarshal(b, &d)
|
||||
return d
|
||||
}
|
||||
if r, ok := req["decision_route"].(string); ok {
|
||||
d.Route = r
|
||||
}
|
||||
if f, ok := req["base_floor"].(bool); ok {
|
||||
d.Floor = f
|
||||
}
|
||||
if f, ok := req["floor"].(bool); ok {
|
||||
d.Floor = f
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func emit(v interface{}) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "marshal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFFIMembraneLive(t *testing.T) {
|
||||
risk, unc := detectFlags("this creates a new authority path and carries data loss risk, expensive reversal likely")
|
||||
if !contains(risk, "new_authority") || !contains(risk, "data_loss_risk") {
|
||||
t.Errorf("Rust membrane silent: risk = %v (want new_authority + data_loss_risk)", risk)
|
||||
}
|
||||
if !contains(unc, "expensive_reversal") {
|
||||
t.Errorf("Rust membrane silent: uncertainty = %v", unc)
|
||||
}
|
||||
empty_r, empty_u := detectFlags(strings.ToLower("fix the typo"))
|
||||
if len(empty_r) != 0 || len(empty_u) != 0 {
|
||||
t.Errorf("unexpected flags on clean body: %v %v", empty_r, empty_u)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/adaptive-router
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,551 @@
|
||||
// adaptive-router — DEEP PARITY PORT of src/rune/adaptive_router.py
|
||||
// (625 lines, 19 functions). This is the canonical adaptive route authority:
|
||||
// LOCAL_FIX / STANDARD_CHANGE / RESEARCHED_CHANGE with traceable provenance,
|
||||
// the deep-research floor, escalation/downgrade transitions, and evidence-
|
||||
// sufficiency derivation (Option C).
|
||||
//
|
||||
// Survivor hybrid: the one deterministic leaf (detect_flags) is Rust compute,
|
||||
// linked from the route_risk_flags staticlib — no .so loading, no env var.
|
||||
// Everything reactive (routing decisions, transitions, dispatch) is Go.
|
||||
//
|
||||
// Parity: truth.json carries 24 Python-oracle cases WITH inputs; the test
|
||||
// replays every case and requires identical outputs. A divergence fails the
|
||||
// port, not the oracle.
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: ${SRCDIR}/../../rust/route_risk_flags/target/release/libroute_risk_flags.a -ldl -lm
|
||||
#include <stdlib.h>
|
||||
extern char* rrf_detect(const char* arg, size_t arg_len);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ── Rust membrane (Survivor staticlib) ─────────────────────────────────────
|
||||
|
||||
type flagResult struct {
|
||||
Risk []string `json:"risk"`
|
||||
Uncertainty []string `json:"uncertainty"`
|
||||
}
|
||||
|
||||
// detectFlags is the Rust rrf_detect kernel — deterministic flag detection.
|
||||
func detectFlags(bodyLower string) ([]string, []string) {
|
||||
cArg := C.CString(bodyLower)
|
||||
defer C.free(unsafe.Pointer(cArg))
|
||||
res := C.rrf_detect(cArg, C.size_t(len(bodyLower)))
|
||||
defer C.free(unsafe.Pointer(res))
|
||||
var out flagResult
|
||||
if err := json.Unmarshal([]byte(C.GoString(res)), &out); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return out.Risk, out.Uncertainty
|
||||
}
|
||||
|
||||
// ── Domain (parity with adaptive_router.py) ────────────────────────────────
|
||||
|
||||
var ROUTES = map[string]bool{"LOCAL_FIX": true, "STANDARD_CHANGE": true, "RESEARCHED_CHANGE": true}
|
||||
|
||||
var FLOOR_RISK_FLAGS = []string{"new_authority", "architecture_change", "data_loss_risk", "external_service", "memory_architecture"}
|
||||
var FLOOR_UNCERTAINTY_FLAGS = []string{"competing_designs", "expensive_reversal"}
|
||||
var FLOOR_EXPLICIT_MARKERS = []string{"new subsystem", "new epic", "major external", "adopt", "replacement", "architecture research"}
|
||||
|
||||
var ErrFloorViolation = errors.New("FloorViolation: a downgrade was attempted on a route protected by the research floor")
|
||||
|
||||
type SufficiencyInputs struct {
|
||||
LocalAuthority string `json:"local_authority"`
|
||||
ExternalDependency string `json:"external_dependency"`
|
||||
VersionFreshnessRisk string `json:"version_freshness_risk"`
|
||||
Verifiability string `json:"verifiability"`
|
||||
ScopeNovelty string `json:"scope_novelty"`
|
||||
Provenance map[string]string `json:"provenance"`
|
||||
}
|
||||
|
||||
// Unknowns — exact field order from Python unknowns().
|
||||
func (s *SufficiencyInputs) Unknowns() []string {
|
||||
names := []string{"local_authority", "external_dependency", "version_freshness_risk", "verifiability", "scope_novelty"}
|
||||
vals := []string{s.LocalAuthority, s.ExternalDependency, s.VersionFreshnessRisk, s.Verifiability, s.ScopeNovelty}
|
||||
var out []string
|
||||
for i, v := range vals {
|
||||
if v == "UNKNOWN" {
|
||||
out = append(out, names[i])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type RouteDecision struct {
|
||||
Route string `json:"route"`
|
||||
Reasons []string `json:"reasons"`
|
||||
RiskFlags []string `json:"risk_flags"`
|
||||
UncertaintyFlags []string `json:"uncertainty_flags"`
|
||||
Sufficiency *SufficiencyInputs `json:"sufficiency,omitempty"`
|
||||
Unknowns []string `json:"unknowns,omitempty"`
|
||||
Confidence string `json:"confidence"`
|
||||
DecidedBy string `json:"decided_by"`
|
||||
DecidedAt string `json:"decided_at"`
|
||||
TaskDescription string `json:"task_description"`
|
||||
BaselineSHA string `json:"baseline_sha"`
|
||||
PreviousRoute string `json:"previous_route,omitempty"`
|
||||
Trigger string `json:"trigger,omitempty"`
|
||||
Evidence string `json:"evidence,omitempty"`
|
||||
Floor bool `json:"floor"`
|
||||
}
|
||||
|
||||
func now() string {
|
||||
// Python: datetime.now(timezone.utc).isoformat()
|
||||
return time.Now().UTC().Format("2006-01-02T15:04:05.000000+00:00")
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
// ── Floor predicates ───────────────────────────────────────────────────────
|
||||
|
||||
func floorAppliesFromFlags(risk, uncertainty []string, bodyLower string) bool {
|
||||
for _, f := range risk {
|
||||
if contains(FLOOR_RISK_FLAGS, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, f := range uncertainty {
|
||||
if contains(FLOOR_UNCERTAINTY_FLAGS, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, m := range FLOOR_EXPLICIT_MARKERS {
|
||||
if strings.Contains(bodyLower, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func floorReason(risk, uncertainty []string, bodyLower string) string {
|
||||
var hits []string
|
||||
for _, f := range risk {
|
||||
if contains(FLOOR_RISK_FLAGS, f) {
|
||||
hits = append(hits, f)
|
||||
}
|
||||
}
|
||||
for _, f := range uncertainty {
|
||||
if contains(FLOOR_UNCERTAINTY_FLAGS, f) {
|
||||
hits = append(hits, f)
|
||||
}
|
||||
}
|
||||
for _, m := range FLOOR_EXPLICIT_MARKERS {
|
||||
if strings.Contains(bodyLower, m) {
|
||||
hits = append(hits, m)
|
||||
}
|
||||
}
|
||||
if len(hits) == 0 {
|
||||
return "unclassified"
|
||||
}
|
||||
return strings.Join(hits, ", ")
|
||||
}
|
||||
|
||||
// FloorApplies — public floor predicate (canary 5).
|
||||
func FloorApplies(route string, flags []string) bool {
|
||||
if route != "RESEARCHED_CHANGE" {
|
||||
return false
|
||||
}
|
||||
for _, f := range flags {
|
||||
if contains(FLOOR_RISK_FLAGS, f) || contains(FLOOR_UNCERTAINTY_FLAGS, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BodyHasFloorTrigger — public floor predicate over raw task text.
|
||||
func BodyHasFloorTrigger(bodyLower string) bool {
|
||||
risk, uncertainty := detectFlags(bodyLower)
|
||||
return floorAppliesFromFlags(risk, uncertainty, bodyLower)
|
||||
}
|
||||
|
||||
// ── classify_route ─────────────────────────────────────────────────────────
|
||||
|
||||
type ClassifyInput struct {
|
||||
Body string `json:"body"`
|
||||
HasLocalRepro bool `json:"has_local_repro"`
|
||||
OwningAuthorityKnown bool `json:"owning_authority_known"`
|
||||
TestableLocally bool `json:"testable_locally"`
|
||||
NarrowAndReversible bool `json:"narrow_and_reversible"`
|
||||
Sufficiency *SufficiencyInputs `json:"sufficiency"`
|
||||
PipelineSHA string `json:"pipeline_sha"`
|
||||
}
|
||||
|
||||
var defectWords = []string{"fix", "bug", "broken", "regression", "wrong", "typo", "fails", "failing", "crash", "defect"}
|
||||
|
||||
// ClassifyRoute — deterministic-first route classification with provenance.
|
||||
func ClassifyRoute(in ClassifyInput) RouteDecision {
|
||||
var reasons []string
|
||||
bodyLower := strings.ToLower(in.Body)
|
||||
|
||||
var suff *SufficiencyInputs
|
||||
prov := map[string]string{}
|
||||
if in.Sufficiency == nil {
|
||||
// LEGACY COMPAT PATH: raw booleans, no provenance.
|
||||
la := "NOT_GROUNDED"
|
||||
if in.HasLocalRepro && in.OwningAuthorityKnown {
|
||||
la = "GROUNDED"
|
||||
}
|
||||
ver := "NONE"
|
||||
if in.TestableLocally {
|
||||
ver = "TESTABLE"
|
||||
}
|
||||
suff = &SufficiencyInputs{LocalAuthority: la, Verifiability: ver, Provenance: map[string]string{}}
|
||||
prov["legacy_inputs"] = "has_local_repro/owning_authority_known/testable_locally booleans from caller (compat)"
|
||||
} else {
|
||||
suff = in.Sufficiency
|
||||
}
|
||||
for k, v := range suff.Provenance {
|
||||
prov[k] = v
|
||||
}
|
||||
|
||||
risk, uncertainty := detectFlags(bodyLower)
|
||||
|
||||
unknowns := suff.Unknowns()
|
||||
if _, legacy := prov["legacy_inputs"]; legacy {
|
||||
unknowns = nil // legacy path: boolean conditions own the decision
|
||||
}
|
||||
floor := floorAppliesFromFlags(risk, uncertainty, bodyLower)
|
||||
|
||||
if len(unknowns) > 0 {
|
||||
reasons = append(reasons, "material unknowns present: "+strings.Join(unknowns, ", "))
|
||||
reasons = append(reasons, "escalating per doctrine: never manufacture certainty")
|
||||
}
|
||||
|
||||
taskLanguage := suff.Provenance["task_class_language"]
|
||||
isDefectRoute := taskLanguage == "defect"
|
||||
for _, w := range defectWords {
|
||||
if strings.Contains(bodyLower, w) {
|
||||
isDefectRoute = true
|
||||
break
|
||||
}
|
||||
}
|
||||
localOK := in.HasLocalRepro && in.OwningAuthorityKnown &&
|
||||
(in.TestableLocally || (in.NarrowAndReversible && suff.Verifiability == "NONE"))
|
||||
if localOK && isDefectRoute && len(risk) == 0 && len(uncertainty) == 0 && len(unknowns) == 0 && !floor {
|
||||
reasons = append(reasons, "all LOCAL_FIX conditions met; no unknowns; no floor")
|
||||
return makeDecision("LOCAL_FIX", reasons, risk, uncertainty, suff, unknowns, "HIGH", in.Body, in.PipelineSHA, false)
|
||||
}
|
||||
|
||||
if floor {
|
||||
reasons = append(reasons, "deep-research floor: "+floorReason(risk, uncertainty, bodyLower))
|
||||
return makeDecision("RESEARCHED_CHANGE", reasons, risk, uncertainty, suff, unknowns, "HIGH", in.Body, in.PipelineSHA, true)
|
||||
}
|
||||
|
||||
if len(risk) > 0 || len(uncertainty) > 0 || len(unknowns) > 0 {
|
||||
if len(risk) > 0 || len(uncertainty) > 0 {
|
||||
both := append(append([]string{}, risk...), uncertainty...)
|
||||
reasons = append(reasons, "risk/uncertainty flags: "+strings.Join(both, ", "))
|
||||
}
|
||||
reasons = append(reasons, "RESEARCHED_CHANGE: external evidence and adversarial validation justified")
|
||||
return makeDecision("RESEARCHED_CHANGE", reasons, risk, uncertainty, suff, unknowns, "HIGH", in.Body, in.PipelineSHA, floor)
|
||||
}
|
||||
|
||||
reasons = append(reasons, "clear change, existing authority, no consequential selection")
|
||||
return makeDecision("STANDARD_CHANGE", reasons, risk, uncertainty, suff, unknowns, "MEDIUM", in.Body, in.PipelineSHA, false)
|
||||
}
|
||||
|
||||
func makeDecision(route string, reasons []string, risk, uncertainty []string, suff *SufficiencyInputs, unknowns []string, confidence, task, sha string, floor bool) RouteDecision {
|
||||
var filtered []string
|
||||
for _, r := range reasons {
|
||||
if r != "" {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
if filtered == nil {
|
||||
filtered = []string{}
|
||||
}
|
||||
return RouteDecision{
|
||||
Route: route, Reasons: filtered,
|
||||
RiskFlags: risk, UncertaintyFlags: uncertainty,
|
||||
Sufficiency: suff, Unknowns: unknowns,
|
||||
Confidence: confidence, DecidedBy: "deterministic", DecidedAt: now(),
|
||||
TaskDescription: truncate(task, 200), BaselineSHA: sha, Floor: floor,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Escalation / downgrade ─────────────────────────────────────────────────
|
||||
|
||||
func escalateRoute(d RouteDecision, newRoute, trigger, evidence string) (RouteDecision, error) {
|
||||
if !ROUTES[newRoute] {
|
||||
return RouteDecision{}, fmt.Errorf("invalid route: %s", newRoute)
|
||||
}
|
||||
return RouteDecision{
|
||||
Route: newRoute, Reasons: []string{"escalation: " + trigger},
|
||||
RiskFlags: d.RiskFlags, UncertaintyFlags: d.UncertaintyFlags,
|
||||
Sufficiency: d.Sufficiency, Unknowns: d.Unknowns,
|
||||
Confidence: "HIGH", DecidedBy: "escalation", DecidedAt: now(),
|
||||
TaskDescription: d.TaskDescription, BaselineSHA: d.BaselineSHA,
|
||||
PreviousRoute: d.Route, Trigger: trigger, Evidence: evidence, Floor: d.Floor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downgradeRoute(d RouteDecision, newRoute, trigger, evidence string) (RouteDecision, error) {
|
||||
if !ROUTES[newRoute] {
|
||||
return RouteDecision{}, fmt.Errorf("invalid route: %s", newRoute)
|
||||
}
|
||||
if d.Floor && newRoute != "RESEARCHED_CHANGE" {
|
||||
return RouteDecision{}, ErrFloorViolation
|
||||
}
|
||||
return RouteDecision{
|
||||
Route: newRoute, Reasons: []string{"downgrade: " + trigger},
|
||||
RiskFlags: d.RiskFlags, UncertaintyFlags: d.UncertaintyFlags,
|
||||
Sufficiency: d.Sufficiency, Unknowns: d.Unknowns,
|
||||
Confidence: "HIGH", DecidedBy: "downgrade", DecidedAt: now(),
|
||||
TaskDescription: d.TaskDescription, BaselineSHA: d.BaselineSHA,
|
||||
PreviousRoute: d.Route, Trigger: trigger, Evidence: evidence, Floor: d.Floor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EscalateOnVerificationFailure — production hook for the local lane
|
||||
// (canary 4). Evidence must name the material uncertainty.
|
||||
func EscalateOnVerificationFailure(d RouteDecision, failureEvidence string) (RouteDecision, error) {
|
||||
if len(strings.TrimSpace(failureEvidence)) < 20 {
|
||||
return RouteDecision{}, errors.New("verification failure evidence must establish the material uncertainty (what external/architectural fact is in doubt), not merely report that a check failed")
|
||||
}
|
||||
return escalateRoute(d, "RESEARCHED_CHANGE", "verification_failure_external_uncertainty", failureEvidence)
|
||||
}
|
||||
|
||||
// ── Append-only event recording ────────────────────────────────────────────
|
||||
|
||||
func event(kind string, d RouteDecision) map[string]interface{} {
|
||||
ev := map[string]interface{}{
|
||||
"kind": kind, "route": d.Route, "reasons": d.Reasons,
|
||||
"risk_flags": d.RiskFlags, "uncertainty_flags": d.UncertaintyFlags,
|
||||
"confidence": d.Confidence, "decided_by": d.DecidedBy,
|
||||
"decided_at": d.DecidedAt, "task_description": d.TaskDescription,
|
||||
"baseline_sha": d.BaselineSHA,
|
||||
}
|
||||
if d.Sufficiency != nil {
|
||||
if d.Unknowns == nil {
|
||||
ev["unknowns"] = []string{}
|
||||
} else {
|
||||
ev["unknowns"] = d.Unknowns
|
||||
}
|
||||
if d.Sufficiency.Provenance == nil {
|
||||
ev["provenance"] = map[string]string{}
|
||||
} else {
|
||||
ev["provenance"] = d.Sufficiency.Provenance
|
||||
}
|
||||
}
|
||||
if kind != "INITIAL_ROUTE" {
|
||||
ev["previous_route"] = d.PreviousRoute
|
||||
ev["new_route"] = d.Route
|
||||
ev["trigger"] = d.Trigger
|
||||
ev["evidence"] = d.Evidence
|
||||
}
|
||||
if d.Floor {
|
||||
ev["floor"] = true
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
func appendEvent(ev map[string]interface{}, path string) error {
|
||||
if dir := filepath.Dir(path); dir != "." && dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Write(append(b, '\n'))
|
||||
return err
|
||||
}
|
||||
|
||||
func RecordTransition(kind string, d RouteDecision, path string) error {
|
||||
return appendEvent(event(kind, d), path)
|
||||
}
|
||||
|
||||
func RecordFinalRoute(d RouteDecision, path string) error {
|
||||
return RecordTransition("FINAL_ROUTE", d, path)
|
||||
}
|
||||
|
||||
// RecordRouteDecision — persists INITIAL_ROUTE; RESEARCHED_CHANGE callers
|
||||
// attach the executor-consumable dispatch payload.
|
||||
func RecordRouteDecision(d RouteDecision, path string, dispatch map[string]interface{}) error {
|
||||
ev := event("INITIAL_ROUTE", d)
|
||||
if dispatch != nil {
|
||||
ev["dispatch"] = dispatch
|
||||
}
|
||||
return appendEvent(ev, path)
|
||||
}
|
||||
|
||||
func BuildResearchDispatch(taskID, description string, d RouteDecision) map[string]interface{} {
|
||||
prov := map[string]string{}
|
||||
if d.Sufficiency != nil && d.Sufficiency.Provenance != nil {
|
||||
prov = d.Sufficiency.Provenance
|
||||
}
|
||||
unknowns := d.Unknowns
|
||||
if unknowns == nil {
|
||||
unknowns = []string{}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"task_id": taskID, "title": truncate(description, 200), "body": description,
|
||||
"route": d.Route, "floor": d.Floor, "reasons": d.Reasons,
|
||||
"unknowns": unknowns, "provenance": prov,
|
||||
"executor_entry": "research_stage_executor.py --stage ISSUE (with issue-shaped input)",
|
||||
}
|
||||
}
|
||||
|
||||
// ── derive_sufficiency (Option C unified authority) ────────────────────────
|
||||
|
||||
var mechanismWords = []string{"performance", "throughput", "latency", "race condition", "timing", "under load", "concurrency", "deadlock", "memory usage", "benchmark needed", "measure"}
|
||||
var changeWords = []string{"add ", "implement", "extend", "refactor", "introduce", "support for", "new feature"}
|
||||
var inspectionVerbs = []string{"inspect", "read", "review", "analyze", "analyse", "explain", "look", "list", "show", "describe", "check", "what", "why", "how", "where", "which"}
|
||||
var versionWords = []string{"version", "upgrade", "latest", "current behaviour"}
|
||||
var externalMarkers = []string{" api ", "sdk", "library", "package", "service", "endpoint"}
|
||||
|
||||
var testSurfaceRE = regexp.MustCompile(`(?i)(tests/[\w/.\-]+|test_[\w/.\-]+\.py|pytest|unittest|run (the )?tests)`)
|
||||
|
||||
func detectExternalRefs(body string) string {
|
||||
lower := strings.ToLower(body)
|
||||
if strings.Contains(lower, "http://") || strings.Contains(lower, "https://") {
|
||||
return "PRESENT"
|
||||
}
|
||||
for _, m := range externalMarkers {
|
||||
if strings.Contains(lower, m) {
|
||||
return "PRESENT"
|
||||
}
|
||||
}
|
||||
return "NONE"
|
||||
}
|
||||
|
||||
type DeriveInput struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Ambiguous bool `json:"ambiguous"`
|
||||
}
|
||||
|
||||
func DeriveSufficiency(in DeriveInput) SufficiencyInputs {
|
||||
title, body := in.Title, in.Body
|
||||
full := title + "\n" + body
|
||||
lower := strings.ToLower(full)
|
||||
bodyLower := strings.ToLower(body)
|
||||
|
||||
mechanismUncertain := false
|
||||
for _, w := range mechanismWords {
|
||||
if strings.Contains(lower, w) {
|
||||
mechanismUncertain = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
verifiability := "NONE"
|
||||
if testSurfaceRE.MatchString(body) {
|
||||
verifiability = "TESTABLE"
|
||||
}
|
||||
if mechanismUncertain && verifiability == "TESTABLE" {
|
||||
verifiability = "MEASUREMENT_REQUIRED"
|
||||
}
|
||||
|
||||
external := detectExternalRefs(body)
|
||||
|
||||
scopeNovel := "KNOWN_PATTERN"
|
||||
for _, m := range FLOOR_EXPLICIT_MARKERS {
|
||||
if strings.Contains(bodyLower, m) {
|
||||
scopeNovel = "NEW_SUBSYSTEM"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
grounded := external == "NONE" && !mechanismUncertain && scopeNovel == "KNOWN_PATTERN"
|
||||
|
||||
isDefect := false
|
||||
for _, w := range defectWords {
|
||||
if strings.Contains(lower, w) {
|
||||
isDefect = true
|
||||
break
|
||||
}
|
||||
}
|
||||
isChange := false
|
||||
for _, w := range changeWords {
|
||||
if strings.Contains(lower, w) {
|
||||
isChange = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Handoff §10: the ACTION VERB governs mutation-class. A leading
|
||||
// inspection verb makes the task read-only.
|
||||
firstWords := strings.Fields(lower)
|
||||
if len(firstWords) > 3 {
|
||||
firstWords = firstWords[:3]
|
||||
}
|
||||
for _, w := range firstWords {
|
||||
if contains(inspectionVerbs, w) {
|
||||
isDefect = false
|
||||
isChange = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
vfr := "NONE"
|
||||
if external == "PRESENT" {
|
||||
for _, w := range versionWords {
|
||||
if strings.Contains(bodyLower, w) {
|
||||
vfr = "RISK"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
la := "NOT_GROUNDED"
|
||||
if grounded {
|
||||
la = "GROUNDED"
|
||||
}
|
||||
taskClass := "neutral"
|
||||
if isDefect {
|
||||
taskClass = "defect"
|
||||
} else if isChange {
|
||||
taskClass = "change"
|
||||
}
|
||||
return SufficiencyInputs{
|
||||
LocalAuthority: la, ExternalDependency: external, VersionFreshnessRisk: vfr,
|
||||
Verifiability: verifiability, ScopeNovelty: scopeNovel,
|
||||
Provenance: map[string]string{
|
||||
"local_authority": "ISSUE contract evidence_classes (deterministic compiler) + mechanism-uncertainty scan (measurement language)",
|
||||
"mechanism_uncertain": fmt.Sprintf("%t", mechanismUncertain),
|
||||
"external_dependency": "URL/external-reference scan of issue body",
|
||||
"version_freshness_risk": "version-language scan, gated on external dependency",
|
||||
"verifiability": "test path/command reference; measurement language overrides",
|
||||
"scope_novelty": "explicit new-subsystem/adoption markers in issue body",
|
||||
"task_class_language": taskClass,
|
||||
"ambiguous": fmt.Sprintf("%t", in.Ambiguous),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
func contains(list []string, s string) bool {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
[
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "local_fix_happy",
|
||||
"input": {
|
||||
"body": "fix the broken typewriter regex in warm_router, crash on empty input",
|
||||
"has_local_repro": true,
|
||||
"owning_authority_known": true,
|
||||
"testable_locally": true,
|
||||
"narrow_and_reversible": true,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "LOCAL_FIX",
|
||||
"confidence": "HIGH",
|
||||
"reasons": [
|
||||
"all LOCAL_FIX conditions met; no unknowns; no floor"
|
||||
],
|
||||
"risk": [],
|
||||
"uncertainty": [],
|
||||
"floor": false,
|
||||
"task_description": "fix the broken typewriter regex in warm_router, crash on empty input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "standard_change",
|
||||
"input": {
|
||||
"body": "add a progress bar to the export dialog",
|
||||
"has_local_repro": true,
|
||||
"owning_authority_known": true,
|
||||
"testable_locally": true,
|
||||
"narrow_and_reversible": true,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "STANDARD_CHANGE",
|
||||
"confidence": "MEDIUM",
|
||||
"reasons": [
|
||||
"clear change, existing authority, no consequential selection"
|
||||
],
|
||||
"risk": [],
|
||||
"uncertainty": [],
|
||||
"floor": false,
|
||||
"task_description": "add a progress bar to the export dialog"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "defect_word_absent",
|
||||
"input": {
|
||||
"body": "update the version number in the footer",
|
||||
"has_local_repro": true,
|
||||
"owning_authority_known": true,
|
||||
"testable_locally": false,
|
||||
"narrow_and_reversible": true,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "STANDARD_CHANGE",
|
||||
"confidence": "MEDIUM",
|
||||
"reasons": [
|
||||
"clear change, existing authority, no consequential selection"
|
||||
],
|
||||
"risk": [],
|
||||
"uncertainty": [],
|
||||
"floor": false,
|
||||
"task_description": "update the version number in the footer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "floor_new_subsystem",
|
||||
"input": {
|
||||
"body": "adopt a new subsystem for memory indexing, replacement of spine",
|
||||
"has_local_repro": false,
|
||||
"owning_authority_known": false,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"confidence": "HIGH",
|
||||
"reasons": [
|
||||
"deep-research floor: new subsystem, adopt, replacement"
|
||||
],
|
||||
"risk": [],
|
||||
"uncertainty": [],
|
||||
"floor": true,
|
||||
"task_description": "adopt a new subsystem for memory indexing, replacement of spine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "risk_new_authority",
|
||||
"input": {
|
||||
"body": "this creates a new authority path for dispatch",
|
||||
"has_local_repro": false,
|
||||
"owning_authority_known": false,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"confidence": "HIGH",
|
||||
"reasons": [
|
||||
"deep-research floor: new_authority"
|
||||
],
|
||||
"risk": [
|
||||
"new_authority"
|
||||
],
|
||||
"uncertainty": [],
|
||||
"floor": true,
|
||||
"task_description": "this creates a new authority path for dispatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "uncertainty_competing",
|
||||
"input": {
|
||||
"body": "there are competing designs here, expensive reversal likely",
|
||||
"has_local_repro": true,
|
||||
"owning_authority_known": true,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"confidence": "HIGH",
|
||||
"reasons": [
|
||||
"deep-research floor: competing_designs, expensive_reversal"
|
||||
],
|
||||
"risk": [],
|
||||
"uncertainty": [
|
||||
"competing_designs",
|
||||
"expensive_reversal"
|
||||
],
|
||||
"floor": true,
|
||||
"task_description": "there are competing designs here, expensive reversal likely"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "not_grounded",
|
||||
"input": {
|
||||
"body": "add support for markdown export",
|
||||
"has_local_repro": false,
|
||||
"owning_authority_known": true,
|
||||
"testable_locally": true,
|
||||
"narrow_and_reversible": true,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "STANDARD_CHANGE",
|
||||
"confidence": "MEDIUM",
|
||||
"reasons": [
|
||||
"clear change, existing authority, no consequential selection"
|
||||
],
|
||||
"risk": [],
|
||||
"uncertainty": [],
|
||||
"floor": false,
|
||||
"task_description": "add support for markdown export"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "classify",
|
||||
"name": "inspection_verb",
|
||||
"input": {
|
||||
"body": "explain why the failing test fails",
|
||||
"has_local_repro": true,
|
||||
"owning_authority_known": true,
|
||||
"testable_locally": true,
|
||||
"narrow_and_reversible": true,
|
||||
"pipeline_sha": "UNKNOWN"
|
||||
},
|
||||
"expected": {
|
||||
"route": "LOCAL_FIX",
|
||||
"confidence": "HIGH",
|
||||
"reasons": [
|
||||
"all LOCAL_FIX conditions met; no unknowns; no floor"
|
||||
],
|
||||
"risk": [],
|
||||
"uncertainty": [],
|
||||
"floor": false,
|
||||
"task_description": "explain why the failing test fails"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "derive",
|
||||
"name": "grounded_fix",
|
||||
"input": {
|
||||
"title": "fix crash",
|
||||
"body": "the parser crashes on empty input, see tests/test_parse.py",
|
||||
"ambiguous": false
|
||||
},
|
||||
"expected": {
|
||||
"local_authority": "GROUNDED",
|
||||
"external_dependency": "NONE",
|
||||
"version_freshness_risk": "NONE",
|
||||
"verifiability": "TESTABLE",
|
||||
"scope_novelty": "KNOWN_PATTERN",
|
||||
"unknowns": [],
|
||||
"task_class": "defect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "derive",
|
||||
"name": "mechanism",
|
||||
"input": {
|
||||
"title": "investigate",
|
||||
"body": "performance is bad under load, benchmark needed",
|
||||
"ambiguous": false
|
||||
},
|
||||
"expected": {
|
||||
"local_authority": "NOT_GROUNDED",
|
||||
"external_dependency": "NONE",
|
||||
"version_freshness_risk": "NONE",
|
||||
"verifiability": "NONE",
|
||||
"scope_novelty": "KNOWN_PATTERN",
|
||||
"unknowns": [],
|
||||
"task_class": "neutral"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "derive",
|
||||
"name": "external_url",
|
||||
"input": {
|
||||
"title": "read the docs",
|
||||
"body": "see https://example.com/api docs for the endpoint",
|
||||
"ambiguous": false
|
||||
},
|
||||
"expected": {
|
||||
"local_authority": "NOT_GROUNDED",
|
||||
"external_dependency": "PRESENT",
|
||||
"version_freshness_risk": "NONE",
|
||||
"verifiability": "NONE",
|
||||
"scope_novelty": "KNOWN_PATTERN",
|
||||
"unknowns": [],
|
||||
"task_class": "neutral"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "derive",
|
||||
"name": "version_risk",
|
||||
"input": {
|
||||
"title": "upgrade",
|
||||
"body": "upgrade the library to latest version, current behaviour differs",
|
||||
"ambiguous": false
|
||||
},
|
||||
"expected": {
|
||||
"local_authority": "NOT_GROUNDED",
|
||||
"external_dependency": "PRESENT",
|
||||
"version_freshness_risk": "RISK",
|
||||
"verifiability": "NONE",
|
||||
"scope_novelty": "KNOWN_PATTERN",
|
||||
"unknowns": [],
|
||||
"task_class": "neutral"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "derive",
|
||||
"name": "inspection",
|
||||
"input": {
|
||||
"title": "explain",
|
||||
"body": "why does this happen",
|
||||
"ambiguous": false
|
||||
},
|
||||
"expected": {
|
||||
"local_authority": "GROUNDED",
|
||||
"external_dependency": "NONE",
|
||||
"version_freshness_risk": "NONE",
|
||||
"verifiability": "NONE",
|
||||
"scope_novelty": "KNOWN_PATTERN",
|
||||
"unknowns": [],
|
||||
"task_class": "neutral"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "derive",
|
||||
"name": "new_subsystem",
|
||||
"input": {
|
||||
"title": "adopt",
|
||||
"body": "this adopts a replacement architecture",
|
||||
"ambiguous": false
|
||||
},
|
||||
"expected": {
|
||||
"local_authority": "NOT_GROUNDED",
|
||||
"external_dependency": "NONE",
|
||||
"version_freshness_risk": "NONE",
|
||||
"verifiability": "NONE",
|
||||
"scope_novelty": "NEW_SUBSYSTEM",
|
||||
"unknowns": [],
|
||||
"task_class": "neutral"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "derive",
|
||||
"name": "neutral",
|
||||
"input": {
|
||||
"title": "write",
|
||||
"body": "a poem about the sea",
|
||||
"ambiguous": false
|
||||
},
|
||||
"expected": {
|
||||
"local_authority": "GROUNDED",
|
||||
"external_dependency": "NONE",
|
||||
"version_freshness_risk": "NONE",
|
||||
"verifiability": "NONE",
|
||||
"scope_novelty": "KNOWN_PATTERN",
|
||||
"unknowns": [],
|
||||
"task_class": "neutral"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "floor_trigger",
|
||||
"name": "trigger_yes",
|
||||
"input": {
|
||||
"body": "adopt a replacement for the cache"
|
||||
},
|
||||
"expected": {
|
||||
"result": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "floor_trigger",
|
||||
"name": "trigger_no",
|
||||
"input": {
|
||||
"body": "fix the typo"
|
||||
},
|
||||
"expected": {
|
||||
"result": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "floor_applies",
|
||||
"name": "res_yes",
|
||||
"input": {
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"flags": [
|
||||
"new_authority"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"result": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "floor_applies",
|
||||
"name": "local_no",
|
||||
"input": {
|
||||
"route": "LOCAL_FIX",
|
||||
"flags": [
|
||||
"new_authority"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"result": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "escalate",
|
||||
"name": "esc",
|
||||
"input": {
|
||||
"decision_route": "LOCAL_FIX",
|
||||
"new_route": "RESEARCHED_CHANGE",
|
||||
"trigger": "verify_fail",
|
||||
"evidence": "the fix works locally but the production parser accepts different input shapes \u2014 input contract uncertain"
|
||||
},
|
||||
"expected": {
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"previous": "LOCAL_FIX",
|
||||
"decided_by": "escalation",
|
||||
"trigger": "verify_fail"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "downgrade",
|
||||
"name": "dg_ok",
|
||||
"input": {
|
||||
"base_route": "LOCAL_FIX",
|
||||
"base_floor": false,
|
||||
"new_route": "STANDARD_CHANGE"
|
||||
},
|
||||
"expected": {
|
||||
"route": "STANDARD_CHANGE",
|
||||
"error": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "downgrade",
|
||||
"name": "dg_floored",
|
||||
"input": {
|
||||
"base_floor": true,
|
||||
"new_route": "STANDARD_CHANGE"
|
||||
},
|
||||
"expected": {
|
||||
"route": null,
|
||||
"error": "FloorViolation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "evidence_short",
|
||||
"name": "short",
|
||||
"input": {
|
||||
"evidence": "test failed"
|
||||
},
|
||||
"expected": {
|
||||
"result": "REJECTED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "dispatch",
|
||||
"name": "disp",
|
||||
"input": {
|
||||
"task_id": "T-1",
|
||||
"description": "research the memory index options"
|
||||
},
|
||||
"expected": {
|
||||
"task_id": "T-1",
|
||||
"title": "research the memory index options",
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"floor": true,
|
||||
"executor_entry": "research_stage_executor.py --stage ISSUE (with issue-shaped input)"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
// truth_test.go — the parity gate. Replays every case from truth.json
|
||||
// (generated against src/rune/adaptive_router.py, inputs AND expected
|
||||
// outputs committed) through the ported Go functions. A divergence fails
|
||||
// the port, not the oracle.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type truthCase struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
|
||||
func loadTruth(t *testing.T) []truthCase {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing: %v", err)
|
||||
}
|
||||
var cases []truthCase
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("truth.json unparseable: %v", err)
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
func jsonEq(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gs, _ := json.Marshal(got)
|
||||
ws, _ := json.Marshal(want)
|
||||
if string(gs) != string(ws) {
|
||||
t.Errorf("%s.%s: got %s want %s", name, field, gs, ws)
|
||||
}
|
||||
}
|
||||
|
||||
func str(m map[string]interface{}, k string) string {
|
||||
s, _ := m[k].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func boolField(m map[string]interface{}, k string) bool {
|
||||
b, _ := m[k].(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
func TestParityAgainstPythonOracle(t *testing.T) {
|
||||
cases := loadTruth(t)
|
||||
if len(cases) == 0 {
|
||||
t.Fatal("no truth cases — the parity gate is empty")
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "classify":
|
||||
body := str(c.Input, "body")
|
||||
in := ClassifyInput{
|
||||
Body: body,
|
||||
HasLocalRepro: boolField(c.Input, "has_local_repro"),
|
||||
OwningAuthorityKnown: boolField(c.Input, "owning_authority_known"),
|
||||
TestableLocally: boolField(c.Input, "testable_locally"),
|
||||
NarrowAndReversible: boolField(c.Input, "narrow_and_reversible"),
|
||||
PipelineSHA: str(c.Input, "pipeline_sha"),
|
||||
}
|
||||
d := ClassifyRoute(in)
|
||||
e := c.Expected
|
||||
jsonEq(t, c.Name, "route", d.Route, e["route"])
|
||||
jsonEq(t, c.Name, "confidence", d.Confidence, e["confidence"])
|
||||
jsonEq(t, c.Name, "reasons", d.Reasons, e["reasons"])
|
||||
jsonEq(t, c.Name, "risk", d.RiskFlags, e["risk"])
|
||||
jsonEq(t, c.Name, "uncertainty", d.UncertaintyFlags, e["uncertainty"])
|
||||
jsonEq(t, c.Name, "floor", d.Floor, e["floor"])
|
||||
jsonEq(t, c.Name, "task_description", d.TaskDescription, e["task_description"])
|
||||
case "derive":
|
||||
in := DeriveInput{Title: str(c.Input, "title"), Body: str(c.Input, "body"), Ambiguous: boolField(c.Input, "ambiguous")}
|
||||
s := DeriveSufficiency(in)
|
||||
e := c.Expected
|
||||
jsonEq(t, c.Name, "local_authority", s.LocalAuthority, e["local_authority"])
|
||||
jsonEq(t, c.Name, "external_dependency", s.ExternalDependency, e["external_dependency"])
|
||||
jsonEq(t, c.Name, "version_freshness_risk", s.VersionFreshnessRisk, e["version_freshness_risk"])
|
||||
jsonEq(t, c.Name, "verifiability", s.Verifiability, e["verifiability"])
|
||||
jsonEq(t, c.Name, "scope_novelty", s.ScopeNovelty, e["scope_novelty"])
|
||||
unk := s.Unknowns()
|
||||
if unk == nil {
|
||||
unk = []string{}
|
||||
}
|
||||
jsonEq(t, c.Name, "unknowns", unk, e["unknowns"])
|
||||
jsonEq(t, c.Name, "task_class", s.Provenance["task_class_language"], e["task_class"])
|
||||
case "floor_trigger":
|
||||
got := BodyHasFloorTrigger(str(c.Input, "body"))
|
||||
jsonEq(t, c.Name, "result", got, c.Expected["result"])
|
||||
case "floor_applies":
|
||||
var flags []string
|
||||
b, _ := json.Marshal(c.Input["flags"])
|
||||
json.Unmarshal(b, &flags)
|
||||
got := FloorApplies(str(c.Input, "route"), flags)
|
||||
jsonEq(t, c.Name, "result", got, c.Expected["result"])
|
||||
case "escalate":
|
||||
base := RouteDecision{Route: str(c.Input, "decision_route"), Reasons: []string{}}
|
||||
out, err := escalateRoute(base, str(c.Input, "new_route"), str(c.Input, "trigger"), str(c.Input, "evidence"))
|
||||
if err != nil {
|
||||
t.Errorf("%s: escalate errored: %v", c.Name, err)
|
||||
continue
|
||||
}
|
||||
e := c.Expected
|
||||
jsonEq(t, c.Name, "route", out.Route, e["route"])
|
||||
jsonEq(t, c.Name, "previous", out.PreviousRoute, e["previous"])
|
||||
jsonEq(t, c.Name, "decided_by", out.DecidedBy, e["decided_by"])
|
||||
jsonEq(t, c.Name, "trigger", out.Trigger, e["trigger"])
|
||||
case "downgrade":
|
||||
base := RouteDecision{Route: str(c.Input, "base_route"), Floor: boolField(c.Input, "base_floor"), Reasons: []string{}}
|
||||
out, err := downgradeRoute(base, str(c.Input, "new_route"), "trigger", "evidence")
|
||||
e := c.Expected
|
||||
if e["error"] != nil {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected FloorViolation, got none", c.Name)
|
||||
} else if err.Error()[:14] != "FloorViolation" {
|
||||
t.Errorf("%s: expected FloorViolation, got %v", c.Name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", c.Name, err)
|
||||
continue
|
||||
}
|
||||
jsonEq(t, c.Name, "route", out.Route, e["route"])
|
||||
case "evidence_short":
|
||||
base := RouteDecision{Route: "LOCAL_FIX", Reasons: []string{}}
|
||||
_, err := EscalateOnVerificationFailure(base, str(c.Input, "evidence"))
|
||||
got := "ACCEPTED"
|
||||
if err != nil {
|
||||
got = "REJECTED"
|
||||
}
|
||||
jsonEq(t, c.Name, "result", got, c.Expected["result"])
|
||||
case "dispatch":
|
||||
floored := ClassifyRoute(ClassifyInput{Body: "adopt a replacement for the memory index"})
|
||||
d := BuildResearchDispatch(str(c.Input, "task_id"), str(c.Input, "description"), floored)
|
||||
e := c.Expected
|
||||
jsonEq(t, c.Name, "task_id", d["task_id"], e["task_id"])
|
||||
jsonEq(t, c.Name, "title", d["title"], e["title"])
|
||||
jsonEq(t, c.Name, "route", d["route"], e["route"])
|
||||
jsonEq(t, c.Name, "floor", d["floor"], e["floor"])
|
||||
jsonEq(t, c.Name, "executor_entry", d["executor_entry"], e["executor_entry"])
|
||||
default:
|
||||
t.Errorf("unknown truth fn %q", c.Fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,3 +1,17 @@
|
||||
module rune/context-broker
|
||||
|
||||
go 1.21
|
||||
go 1.26.0
|
||||
|
||||
require modernc.org/sqlite v1.60.1
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
modernc.org/libc v1.77.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
+39
-18
@@ -3,13 +3,14 @@
|
||||
// Python's threading.Timer.
|
||||
//
|
||||
// Usage:
|
||||
// cron-engine add <name> <cron-expression> <command>
|
||||
// cron-engine remove <name>
|
||||
// cron-engine list
|
||||
// cron-engine run <name>
|
||||
// cron-engine enable <name>
|
||||
// cron-engine disable <name>
|
||||
// cron-engine fire-on-complete <task_name>
|
||||
//
|
||||
// cron-engine add <name> <cron-expression> <command>
|
||||
// cron-engine remove <name>
|
||||
// cron-engine list
|
||||
// cron-engine run <name>
|
||||
// cron-engine enable <name>
|
||||
// cron-engine disable <name>
|
||||
// cron-engine fire-on-complete <task_name>
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -46,17 +47,21 @@ func enginePath() string {
|
||||
return filepath.Join(home, "cron_tasks.json")
|
||||
}
|
||||
|
||||
func loadEngine() CronEngine {
|
||||
ce := CronEngine{Tasks: make(map[string]*CronTask)}
|
||||
func loadEngine() *CronEngine {
|
||||
ce := &CronEngine{Tasks: make(map[string]*CronTask)}
|
||||
data, err := os.ReadFile(enginePath())
|
||||
if err != nil { return ce }
|
||||
json.Unmarshal(data, &ce)
|
||||
if err != nil {
|
||||
return ce
|
||||
}
|
||||
json.Unmarshal(data, ce)
|
||||
return ce
|
||||
}
|
||||
|
||||
func saveEngine(ce CronEngine) {
|
||||
func saveEngine(ce *CronEngine) {
|
||||
os.MkdirAll(filepath.Dir(enginePath()), 0755)
|
||||
data, _ := json.MarshalIndent(ce, "", " ")
|
||||
// Marshal ONLY the durable state — the embedded sync.Mutex must never
|
||||
// be copied (go vet copies-lock), and it is not state anyway.
|
||||
data, _ := json.MarshalIndent(struct{ Tasks map[string]*CronTask }{Tasks: ce.Tasks}, "", " ")
|
||||
os.WriteFile(enginePath(), data, 0644)
|
||||
}
|
||||
|
||||
@@ -99,8 +104,14 @@ func main() {
|
||||
|
||||
case "run":
|
||||
task, ok := ce.Tasks[args[0]]
|
||||
if !ok { fmt.Println("not found:", args[0]); return }
|
||||
if !task.Enabled { fmt.Println("disabled:", args[0]); return }
|
||||
if !ok {
|
||||
fmt.Println("not found:", args[0])
|
||||
return
|
||||
}
|
||||
if !task.Enabled {
|
||||
fmt.Println("disabled:", args[0])
|
||||
return
|
||||
}
|
||||
parts := strings.Fields(task.Command)
|
||||
c := exec.Command(parts[0], parts[1:]...)
|
||||
output, err := c.CombinedOutput()
|
||||
@@ -116,17 +127,27 @@ func main() {
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "enable":
|
||||
if t, ok := ce.Tasks[args[0]]; ok { t.Enabled = true; saveEngine(ce); fmt.Println("enabled:", args[0]) }
|
||||
if t, ok := ce.Tasks[args[0]]; ok {
|
||||
t.Enabled = true
|
||||
saveEngine(ce)
|
||||
fmt.Println("enabled:", args[0])
|
||||
}
|
||||
|
||||
case "disable":
|
||||
if t, ok := ce.Tasks[args[0]]; ok { t.Enabled = false; saveEngine(ce); fmt.Println("disabled:", args[0]) }
|
||||
if t, ok := ce.Tasks[args[0]]; ok {
|
||||
t.Enabled = false
|
||||
saveEngine(ce)
|
||||
fmt.Println("disabled:", args[0])
|
||||
}
|
||||
|
||||
case "fire-on-complete":
|
||||
// Fire all tasks with "on_task_complete" in their cron expression
|
||||
fired := 0
|
||||
for _, task := range ce.Tasks {
|
||||
if task.Enabled && strings.Contains(task.CronExpression, "on_task_complete") {
|
||||
if len(args) > 0 && task.Name != args[0] { continue }
|
||||
if len(args) > 0 && task.Name != args[0] {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(task.Command)
|
||||
c := exec.Command(parts[0], parts[1:]...)
|
||||
c.CombinedOutput()
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package main
|
||||
|
||||
// candidates.go — batch 2: the candidate ledger (promote/decay) and the
|
||||
// append-only monthly journal. PARITY ports of curator.py's
|
||||
// promote_candidates / decay_candidates / journal_append, with the same
|
||||
// constants and the same verdict semantics. The coherence gate and drift
|
||||
// pre-commit gate are Go-side hooks (wired when those kernels' Go surfaces
|
||||
// land); the ledger mechanics here are byte-faithful to the Python oracle.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CandidateMin = 3 // CANDIDATE_MIN: sightings before promotion
|
||||
const CandidateStaleRuns = 30 // CANDIDATE_STALE_RUNS: runs before decay
|
||||
|
||||
const CandidatesKey = "_candidates"
|
||||
|
||||
// Candidates — idx[CANDIDATES_KEY] shape: key → entry map.
|
||||
type Candidates = map[string]map[string]interface{}
|
||||
type Index = map[string]interface{}
|
||||
|
||||
// EnsureCandidates — get-or-create the ledger in the index.
|
||||
func EnsureCandidates(idx Index) Candidates {
|
||||
raw, ok := idx[CandidatesKey]
|
||||
if !ok {
|
||||
c := Candidates{}
|
||||
idx[CandidatesKey] = c
|
||||
return c
|
||||
}
|
||||
if c, ok := raw.(Candidates); ok {
|
||||
return c
|
||||
}
|
||||
// JSON-decoded indexes arrive as map[string]interface{}
|
||||
if m, ok := raw.(map[string]interface{}); ok {
|
||||
c := Candidates{}
|
||||
for k, v := range m {
|
||||
if em, ok := v.(map[string]interface{}); ok {
|
||||
c[k] = em
|
||||
}
|
||||
}
|
||||
idx[CandidatesKey] = c
|
||||
return c
|
||||
}
|
||||
c := Candidates{}
|
||||
idx[CandidatesKey] = c
|
||||
return c
|
||||
}
|
||||
|
||||
// journalEntry — the op record shape (parity with Python journal_append).
|
||||
type JournalEntry struct {
|
||||
Op string
|
||||
Target string
|
||||
Before string
|
||||
After string
|
||||
Evidence string
|
||||
Source string
|
||||
Reason string
|
||||
}
|
||||
|
||||
// JournalAppend — append one record to memory/journal/YYYY-MM.md.
|
||||
// Byte-format parity: `ts` **OP** → `target` + indented fields.
|
||||
func JournalAppend(journalDir string, entry JournalEntry) (string, error) {
|
||||
month := time.Now().UTC().Format("2006-01")
|
||||
path := filepath.Join(journalDir, month+".md")
|
||||
ts := time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "`%s` **%s** → `%s`\n", ts, entry.Op, entry.Target)
|
||||
fmt.Fprintf(&b, " - evidence: %s\n", entry.Evidence)
|
||||
if entry.Before != "" {
|
||||
fmt.Fprintf(&b, " - before: %s\n", entry.Before)
|
||||
}
|
||||
if entry.After != "" {
|
||||
fmt.Fprintf(&b, " - after: %s\n", entry.After)
|
||||
}
|
||||
src := entry.Source
|
||||
if src == "" {
|
||||
src = "curator"
|
||||
}
|
||||
fmt.Fprintf(&b, " - source: %s\n", src)
|
||||
if entry.Reason != "" {
|
||||
fmt.Fprintf(&b, " - reason: %s\n", entry.Reason)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
if err := os.MkdirAll(journalDir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(b.String())
|
||||
return path, err
|
||||
}
|
||||
|
||||
// coherenceFn / driftGateFn — injectable gates (parity hooks). The Go port
|
||||
// wires the real kernels when their surfaces land; nil = the Python
|
||||
// no-kernel path (coherence check skipped in batch-2 oracle runs).
|
||||
type CoherenceFn func(fact string, sources []string) bool
|
||||
type DriftGateFn func(entry map[string]interface{}) (verdict string, reason string)
|
||||
|
||||
// PromoteCandidates — merge weak evidence into the ledger; promote at
|
||||
// CANDIDATE_MIN sightings. Explicit ops and strength>=3 skip the ledger.
|
||||
func PromoteCandidates(evidence []map[string]interface{}, idx Index, journalLines *[]JournalEntry, coherence CoherenceFn, driftGate DriftGateFn) []JournalEntry {
|
||||
cands := EnsureCandidates(idx)
|
||||
promoted := []JournalEntry{}
|
||||
now := func() string { return time.Now().UTC().Format("2006-01-02T15:04:05") }
|
||||
for _, item := range evidence {
|
||||
if op, _ := item["op"].(string); op == "UPDATE" || op == "DELETE" {
|
||||
continue
|
||||
}
|
||||
fact, _ := item["fact"].(string)
|
||||
fact = strings.TrimSpace(fact)
|
||||
if fact == "" {
|
||||
continue
|
||||
}
|
||||
target, _ := item["target"].(string)
|
||||
if target == "" {
|
||||
target = "human"
|
||||
}
|
||||
strength := 1
|
||||
if s, ok := item["strength"].(float64); ok {
|
||||
strength = int(s)
|
||||
} else if s, ok := item["strength"].(int); ok {
|
||||
strength = s
|
||||
}
|
||||
if strength >= 3 {
|
||||
continue
|
||||
}
|
||||
ck := CandidateKey(fact)
|
||||
entry, exists := cands[ck]
|
||||
if !exists {
|
||||
entry = map[string]interface{}{
|
||||
"text": fact, "target": target, "count": 0,
|
||||
"last_seen": now(), "first_seen": now(),
|
||||
"sources": []string{},
|
||||
}
|
||||
cands[ck] = entry
|
||||
}
|
||||
entry["text"] = fact
|
||||
entry["target"] = target
|
||||
count := 0
|
||||
if c, ok := entry["count"].(float64); ok {
|
||||
count = int(c)
|
||||
} else if c, ok := entry["count"].(int); ok {
|
||||
count = c
|
||||
}
|
||||
entry["count"] = count + 1
|
||||
entry["last_seen"] = now()
|
||||
src, _ := item["source"].(string)
|
||||
if src == "" {
|
||||
src = "curator"
|
||||
}
|
||||
sources, _ := entry["sources"].([]string)
|
||||
found := false
|
||||
for _, s := range sources {
|
||||
if s == src {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
entry["sources"] = append(sources, src)
|
||||
}
|
||||
if count+1 >= CandidateMin {
|
||||
// QUALITY GATE: persona/operating candidates need coherence.
|
||||
if target == "persona" || target == "operating" {
|
||||
if coherence != nil && !coherence(fact, []string{"sessions"}) {
|
||||
*journalLines = append(*journalLines, JournalEntry{
|
||||
Op: "CANDIDATE-PARKED", Target: target, After: truncate(fact, 140),
|
||||
Evidence: "3 sightings but no transcript support (lens hallucination guard)",
|
||||
Source: "coherence gate", Reason: "pattern not traceable to real session content",
|
||||
})
|
||||
entry["count"] = CandidateMin - 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Drift-with-integrity: pre-commit gate.
|
||||
if driftGate != nil {
|
||||
verdict, reason := driftGate(map[string]interface{}{
|
||||
"text": fact, "importance": 0.5, "wash_count": count + 1})
|
||||
if verdict == "REJECT" {
|
||||
*journalLines = append(*journalLines, JournalEntry{
|
||||
Op: "CANDIDATE-REJECTED", Target: target, After: truncate(fact, 140),
|
||||
Evidence: fmt.Sprintf("pre-commit gate rejected: %s", reason),
|
||||
Source: "drift gate", Reason: "pre-commit gate failed",
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
promoted = append(promoted, JournalEntry{
|
||||
Op: "ADD", Target: target, After: fact,
|
||||
Source: fmt.Sprintf("candidate ledger (%d sightings)", count+1),
|
||||
Reason: fmt.Sprintf("candidate seen %d times across runs", count+1),
|
||||
})
|
||||
delete(cands, ck)
|
||||
}
|
||||
}
|
||||
for _, p := range promoted {
|
||||
*journalLines = append(*journalLines, p)
|
||||
}
|
||||
return promoted
|
||||
}
|
||||
|
||||
// DecayCandidates — prune candidates not seen in CANDIDATE_STALE_RUNS/24 days.
|
||||
func DecayCandidates(idx Index, journalLines *[]JournalEntry) bool {
|
||||
cands := EnsureCandidates(idx)
|
||||
nowT := time.Now().UTC()
|
||||
var stale []string
|
||||
for ck, entry := range cands {
|
||||
lastSeen, _ := entry["last_seen"].(string)
|
||||
last, err := time.Parse("2006-01-02T15:04:05", lastSeen)
|
||||
if err != nil {
|
||||
last, err = time.Parse(time.RFC3339, lastSeen)
|
||||
if err != nil {
|
||||
stale = append(stale, ck)
|
||||
continue
|
||||
}
|
||||
}
|
||||
days := nowT.Sub(last).Hours() / 24
|
||||
if days > float64(CandidateStaleRuns)/24.0 {
|
||||
stale = append(stale, ck)
|
||||
}
|
||||
}
|
||||
for _, ck := range stale {
|
||||
entry := cands[ck]
|
||||
text, _ := entry["text"].(string)
|
||||
target, _ := entry["target"].(string)
|
||||
if target == "" {
|
||||
target = "human"
|
||||
}
|
||||
count := 0
|
||||
if c, ok := entry["count"].(float64); ok {
|
||||
count = int(c)
|
||||
} else if c, ok := entry["count"].(int); ok {
|
||||
count = c
|
||||
}
|
||||
*journalLines = append(*journalLines, JournalEntry{
|
||||
Op: "CANDIDATE-DROP", Target: target, Before: truncate(text, 120),
|
||||
Evidence: fmt.Sprintf("candidate stale (not seen recently), had %d sightings", count),
|
||||
Source: "candidate decay",
|
||||
})
|
||||
delete(cands, ck)
|
||||
}
|
||||
return len(stale) > 0
|
||||
}
|
||||
|
||||
var _ = regexp.MustCompile // reserved for the coherence port
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
// cli.go — the module-family CLI: one JSON op per invocation. Exposes the
|
||||
// ported curator mechanics for shell/Go callers (the go loop builds every
|
||||
// src/go module; a main package needs a main()).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var req map[string]interface{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
|
||||
emit(map[string]interface{}{"error": "bad JSON input: " + err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
op, _ := req["op"].(string)
|
||||
switch op {
|
||||
case "entry_key":
|
||||
text, _ := req["text"].(string)
|
||||
emit(map[string]interface{}{"key": EntryKey(text)})
|
||||
case "candidate_key":
|
||||
text, _ := req["text"].(string)
|
||||
emit(map[string]interface{}{"key": CandidateKey(text)})
|
||||
case "norm":
|
||||
text, _ := req["text"].(string)
|
||||
emit(map[string]interface{}{"norm": Norm(text)})
|
||||
case "block_path":
|
||||
label, _ := req["label"].(string)
|
||||
emit(map[string]interface{}{"path": BlockPath(label)})
|
||||
case "find_entry":
|
||||
body, _ := req["body"].(string)
|
||||
needle, _ := req["needle"].(string)
|
||||
emit(map[string]interface{}{"line": FindEntry(body, needle)})
|
||||
case "sanitize_slug":
|
||||
name, _ := req["name"].(string)
|
||||
emit(map[string]interface{}{"slug": SanitizeSlug(name)})
|
||||
case "detect_conflict":
|
||||
body, _ := req["body"].(string)
|
||||
fact, _ := req["fact"].(string)
|
||||
kind, line := DetectConflict(body, fact)
|
||||
emit(map[string]interface{}{"kind": kind, "line": line})
|
||||
case "le_promote_eligible":
|
||||
proposal, _ := req["proposal"].(string)
|
||||
existing, _ := req["existing"].(string)
|
||||
strength := intOf(req["strength"], 1)
|
||||
minS := intOf(req["min_strength"], 3)
|
||||
wash := intOf(req["wash_count"], 0)
|
||||
emit(LePromoteEligible(proposal, existing, strength, minS, wash))
|
||||
default:
|
||||
emit(map[string]interface{}{"error": "unknown op: " + op})
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func intOf(v interface{}, def int) int {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
if i, ok := v.(int); ok {
|
||||
return i
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func emit(v interface{}) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "marshal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
// coherence.go — batch 3: the semantic coherence gate (the anti-hallucination
|
||||
// check for candidate promotion). PARITY port of curator.py's
|
||||
// _grounded_in_sessions distinctive-word algorithm:
|
||||
//
|
||||
// - Significant words (4+ letters, minus the stop set) are extracted
|
||||
// - The RAREST significant word (by corpus frequency) is the probe
|
||||
// - Grounding requires the rarest word present in >= 2 DISTINCT sessions
|
||||
// (independence beats multiplicity — one rich session can't dominate)
|
||||
// - Any absent rarest word = not grounded (parking is never wrong for
|
||||
// growth; applying wrongly is)
|
||||
//
|
||||
// The corpus-grounding half (_grounded_in_corpus) needs the memory kernel's
|
||||
// chunk_recall — wired later through the Survivor membrane. This file covers
|
||||
// the sessions half, which is the load-bearing gate for behaviour
|
||||
// refinements.
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var wordRE = regexp.MustCompile(`[A-Za-z]{4,}`)
|
||||
|
||||
// stopWords — the Python oracle's stop set (order-insensitive; membership only).
|
||||
var stopWords = map[string]bool{
|
||||
"should": true, "would": true, "could": true, "need": true,
|
||||
"with": true, "that": true, "this": true, "about": true,
|
||||
"before": true, "after": true, "from": true, "into": true,
|
||||
"there": true, "their": true, "these": true, "those": true,
|
||||
"while": true, "being": true, "have": true, "has": true,
|
||||
"been": true, "will": true, "were": true, "when": true,
|
||||
"then": true, "them": true, "they": true, "just": true,
|
||||
"more": true, "also": true, "each": true, "than": true,
|
||||
"very": true, "your": true, "the": true, "and": true,
|
||||
"user": true, "value": true, "i": true, "we": true,
|
||||
"you": true, "our": true, "its": true, "for": true,
|
||||
}
|
||||
|
||||
// GroundedInSessions — the distinctive-word algorithm over in-memory session
|
||||
// texts. The Python oracle filters by file mtime (window_days); the Go port
|
||||
// takes pre-filtered texts (callers supply recent sessions) and the caller
|
||||
// owns the recency policy. Decision logic is byte-faithful.
|
||||
func GroundedInSessions(fact string, texts []string) bool {
|
||||
if len(texts) == 0 {
|
||||
return false
|
||||
}
|
||||
corpus := strings.Join(texts, " ")
|
||||
lowCorpus := strings.ToLower(corpus)
|
||||
|
||||
words := wordRE.FindAllString(strings.ToLower(fact), -1)
|
||||
var sig []string
|
||||
for _, w := range words {
|
||||
if !stopWords[w] {
|
||||
sig = append(sig, w)
|
||||
}
|
||||
}
|
||||
if len(sig) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
freq := map[string]int{}
|
||||
for _, w := range sig {
|
||||
freq[w] = strings.Count(lowCorpus, w)
|
||||
}
|
||||
|
||||
// Rarest significant word (Python: min(sig, key=freq.get, default 0)).
|
||||
rarest := sig[0]
|
||||
for _, w := range sig[1:] {
|
||||
if freq[w] < freq[rarest] {
|
||||
rarest = w
|
||||
}
|
||||
}
|
||||
if !strings.Contains(lowCorpus, rarest) {
|
||||
return false
|
||||
}
|
||||
// Independence: >= 2 DISTINCT sessions contain the rarest word.
|
||||
hits := 0
|
||||
for _, t := range texts {
|
||||
if strings.Contains(strings.ToLower(t), rarest) {
|
||||
hits++
|
||||
}
|
||||
}
|
||||
return hits >= 2
|
||||
}
|
||||
|
||||
// CoherenceCheck — the gate entry (parity with coherence_check's structure).
|
||||
// Sources are checked in order; sessions grounding is in-process, corpus
|
||||
// grounding delegates to corpusFn when wired (nil = skip, as the Python
|
||||
// no-kernel path does).
|
||||
func CoherenceCheck(fact string, sources []string, sessionTexts []string, corpusFn func(string) bool) bool {
|
||||
if len(strings.TrimSpace(fact)) < 15 {
|
||||
return true // non-claim — don't block cold start
|
||||
}
|
||||
for _, src := range sources {
|
||||
if src == "sessions" && GroundedInSessions(fact, sessionTexts) {
|
||||
return true
|
||||
}
|
||||
if src == "corpus" && corpusFn != nil && corpusFn(fact) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var _ = time.Now // reserved for the mtime-window port
|
||||
|
||||
// timeNowUTC — test seam for the decay pass (mirrors Python's now()).
|
||||
func timeNowUTC() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
// decay.go — batch 4: the decay pass and the token-overlap compress
|
||||
// mechanics. PARITY ports of curator.py's decay_pass, _tokens, _overlap
|
||||
// with the same constants (STALE_DAYS=90, DECAY_RATE=0.2, FORGET_FLOOR=0.2).
|
||||
// The full compress_pass needs the store blocks read/write seam — the pure
|
||||
// clustering pieces port here; the store seam lands with the memory kernel
|
||||
// membrane.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const StaleDays = 90
|
||||
const DecayRate = 0.2
|
||||
const ForgetFloor = 0.2
|
||||
|
||||
var compressStopwords = map[string]bool{
|
||||
"the": true, "and": true, "for": true, "with": true, "from": true,
|
||||
"that": true, "this": true, "has": true, "have": true, "was": true,
|
||||
"were": true, "are": true, "you": true, "your": true, "our": true,
|
||||
"not": true, "but": true, "also": true, "his": true, "her": true,
|
||||
"how": true, "all": true, "any": true, "each": true, "own": true,
|
||||
"same": true, "too": true, "very": true,
|
||||
}
|
||||
|
||||
// Tokens — normalized significant words (norm'd, stopword-filtered).
|
||||
func Tokens(text string) []string {
|
||||
var out []string
|
||||
for _, w := range strings.Fields(Norm(text)) {
|
||||
if !compressStopwords[w] {
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Overlap — Jaccard over significant tokens (0.0 when either side is empty).
|
||||
func Overlap(a, b string) float64 {
|
||||
ta := tokenSet(a)
|
||||
tb := tokenSet(b)
|
||||
if len(ta) == 0 || len(tb) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
inter := 0
|
||||
for w := range ta {
|
||||
if tb[w] {
|
||||
inter++
|
||||
}
|
||||
}
|
||||
union := len(ta) + len(tb) - inter
|
||||
return float64(inter) / float64(union)
|
||||
}
|
||||
|
||||
func tokenSet(text string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
for _, w := range Tokens(text) {
|
||||
m[w] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// DecayPass — reduce stale entries' importance; FORGET below the floor.
|
||||
// Skips _candidates and constitution (inviolable). Mutates idx in place.
|
||||
// meta shape: {"importance": float, "last_referenced": iso-string}.
|
||||
func DecayPass(idx Index, journalLines *[]JournalEntry) bool {
|
||||
now := time.Now().UTC()
|
||||
changed := false
|
||||
for label, raw := range idx {
|
||||
if label == CandidatesKey || label == "constitution" {
|
||||
continue
|
||||
}
|
||||
entries, ok := raw.(map[string]map[string]interface{})
|
||||
if !ok {
|
||||
if m, isMap := raw.(map[string]interface{}); isMap {
|
||||
entries = map[string]map[string]interface{}{}
|
||||
for k, v := range m {
|
||||
if em, ok := v.(map[string]interface{}); ok {
|
||||
entries[k] = em
|
||||
}
|
||||
}
|
||||
idx[label] = entries
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for key, meta := range entries {
|
||||
lr, _ := meta["last_referenced"].(string)
|
||||
ageDays := float64(StaleDays) + 1
|
||||
if ref, err := time.Parse(time.RFC3339, lr); err == nil {
|
||||
ageDays = now.Sub(ref).Hours() / 24
|
||||
}
|
||||
if ageDays < StaleDays {
|
||||
continue
|
||||
}
|
||||
imp, _ := meta["importance"].(float64)
|
||||
meta["importance"] = math.Max(0.0, imp-DecayRate)
|
||||
changed = true
|
||||
if meta["importance"].(float64) < ForgetFloor {
|
||||
*journalLines = append(*journalLines, JournalEntry{
|
||||
Op: "FORGET", Target: label,
|
||||
Before: strField(meta, "text"),
|
||||
Evidence: fmt.Sprintf("stale for %.0fd, importance %.2f", ageDays, meta["importance"].(float64)),
|
||||
Source: "decay",
|
||||
})
|
||||
delete(entries, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// ClusterLines — greedy clustering by token overlap (>= 0.35 joins the best
|
||||
// cluster; else a new cluster). PARITY with compress_pass's clustering loop.
|
||||
func ClusterLines(entries []string) [][]string {
|
||||
var clusters [][]string
|
||||
for _, e := range entries {
|
||||
bestI, bestS := -1, 0.35
|
||||
for i, c := range clusters {
|
||||
best := 0.0
|
||||
for _, member := range c {
|
||||
if s := Overlap(e, member); s > best {
|
||||
best = s
|
||||
}
|
||||
}
|
||||
if best > bestS {
|
||||
bestI, bestS = i, best
|
||||
}
|
||||
}
|
||||
if bestI >= 0 {
|
||||
clusters[bestI] = append(clusters[bestI], e)
|
||||
} else {
|
||||
clusters = append(clusters, []string{e})
|
||||
}
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
|
||||
// ClusterSummary — the representative is the LONGEST line; others noted.
|
||||
// PARITY: rep + " (also: " + first-4 others (60-char each, '- ' stripped) + ")"
|
||||
func ClusterSummary(cluster []string) string {
|
||||
rep := cluster[0]
|
||||
for _, c := range cluster {
|
||||
if len(c) > len(rep) {
|
||||
rep = c
|
||||
}
|
||||
}
|
||||
var others []string
|
||||
for _, m := range cluster {
|
||||
if m != rep {
|
||||
others = append(others, m)
|
||||
}
|
||||
}
|
||||
sort.Strings(others)
|
||||
var parts []string
|
||||
for i, o := range others {
|
||||
if i >= 4 {
|
||||
break
|
||||
}
|
||||
parts = append(parts, truncate(strings.TrimPrefix(strings.TrimSpace(o), "- "), 60))
|
||||
}
|
||||
out := strings.TrimRight(rep, " \t\n")
|
||||
if len(parts) > 0 {
|
||||
out += " (also: " + strings.Join(parts, "; ") + ")"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func strField(meta map[string]interface{}, key string) string {
|
||||
s, _ := meta[key].(string)
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/curator-core
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,166 @@
|
||||
// curator-core — DEEP PARITY PORT (batch 1) of src/rune/curator.py's
|
||||
// mechanical core. The curator is the memory write-path gate: evidence →
|
||||
// worthiness → grade → tier. Batch 1 covers the deterministic mechanics:
|
||||
//
|
||||
// BlockPath — store://topic/{label} descriptor
|
||||
// EntryKey — sha256(text)[:16] (raw, NOT normalized)
|
||||
// CandidateKey — sha256(_norm(text))[:16]
|
||||
// Norm — markdown-strip + punctuation-drop + whitespace-collapse
|
||||
// FindEntry — fuzzy needle-in-body line match
|
||||
// LePromoteEligible — drift-with-integrity strength thresholds + degrade
|
||||
// DetectConflict — normalized duplicate/containment detection
|
||||
// SanitizeSlug — slug normalization ("procedure" fallback)
|
||||
// EnsureEntryMeta — index metadata creation
|
||||
// TouchEntry — reference-count / importance bump
|
||||
//
|
||||
// PARITY: truth.json carries 17 Python-oracle cases WITH inputs; the test
|
||||
// replays every case. The lexicon_evolution degrade path is the truth
|
||||
// column (the phantom module is absent, so Python degrades — the Go port
|
||||
// reproduces the degrade verdicts, not an idealized lexicon path).
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BlockPath — blocks live in the store as always-on entries; the path is a
|
||||
// descriptor, the DB is the source.
|
||||
func BlockPath(label string) string {
|
||||
return "store://topic/" + label
|
||||
}
|
||||
|
||||
// EntryKey — raw sha256[:16] of the stripped text.
|
||||
func EntryKey(text string) string {
|
||||
sum := sha256.Sum256([]byte(strings.TrimSpace(text)))
|
||||
return hex.EncodeToString(sum[:])[:16]
|
||||
}
|
||||
|
||||
var (
|
||||
normLead = regexp.MustCompile(`^[-*#>\s]+`)
|
||||
normMarks = regexp.MustCompile(`[*_` + "`" + `#]`)
|
||||
normPunct = regexp.MustCompile(`[^\w\s]`)
|
||||
normSpaces = regexp.MustCompile(`\s+`)
|
||||
slugInvalid = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
)
|
||||
|
||||
// Norm — lowercase, strip markdown bullets/bold/headers, drop punctuation,
|
||||
// collapse whitespace.
|
||||
func Norm(text string) string {
|
||||
t := normLead.ReplaceAllString(strings.TrimSpace(text), "")
|
||||
t = normMarks.ReplaceAllString(t, "")
|
||||
t = normPunct.ReplaceAllString(t, " ")
|
||||
return strings.ToLower(normSpaces.ReplaceAllString(strings.TrimSpace(t), " "))
|
||||
}
|
||||
|
||||
// CandidateKey — sha256 of the NORMALIZED text, [:16].
|
||||
func CandidateKey(text string) string {
|
||||
sum := sha256.Sum256([]byte(Norm(text)))
|
||||
return hex.EncodeToString(sum[:])[:16]
|
||||
}
|
||||
|
||||
// FindEntry — the exact entry line containing needle, or "".
|
||||
func FindEntry(body, needle string) string {
|
||||
for _, line := range strings.Split(body, "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, needle) {
|
||||
return line
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LePromoteEligible — drift-with-integrity: wash_count lowers the strength
|
||||
// threshold (>=20 → 1, >=10 → 2, >=5 → 2, else min_strength). The lexicon
|
||||
// module is a phantom — Python degrades to the strength-only gate with an
|
||||
// explicit reason string; this port reproduces THAT verdict byte-identically.
|
||||
func LePromoteEligible(proposal, existing string, strength, minStrength, washCount int) map[string]interface{} {
|
||||
adjustedMin := minStrength
|
||||
if washCount >= 20 {
|
||||
adjustedMin = 1
|
||||
} else if washCount >= 10 {
|
||||
adjustedMin = 2
|
||||
} else if washCount >= 5 {
|
||||
adjustedMin = 2
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"eligible": strength >= adjustedMin,
|
||||
"reason": fmt.Sprintf("lexicon module unavailable — strength gate only (adjusted_min=%d, wash_count=%d)",
|
||||
adjustedMin, washCount),
|
||||
}
|
||||
}
|
||||
|
||||
// DetectConflict — conservative duplicate detection: exact normalized match
|
||||
// or containment either direction. Returns ("duplicate", line) or ("" , "").
|
||||
func DetectConflict(body, fact string) (string, string) {
|
||||
nf := Norm(fact)
|
||||
if nf == "" {
|
||||
return "", ""
|
||||
}
|
||||
for _, line := range strings.Split(body, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "---") {
|
||||
continue
|
||||
}
|
||||
nl := Norm(line)
|
||||
if nl == "" {
|
||||
continue
|
||||
}
|
||||
if nf == nl || strings.Contains(nl, nf) || strings.Contains(nf, nl) {
|
||||
return "duplicate", line
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// SanitizeSlug — slug normalization with the "procedure" fallback.
|
||||
func SanitizeSlug(name string) string {
|
||||
slug := strings.Trim(slugInvalid.ReplaceAllString(strings.ToLower(name), "-"), "-")
|
||||
if slug == "" {
|
||||
return "procedure"
|
||||
}
|
||||
return slug
|
||||
}
|
||||
|
||||
// EnsureEntryMeta — create index metadata if missing; returns the key.
|
||||
func EnsureEntryMeta(idx map[string]map[string]map[string]interface{}, label, text string) string {
|
||||
key := EntryKey(text)
|
||||
if idx[label] == nil {
|
||||
idx[label] = map[string]map[string]interface{}{}
|
||||
}
|
||||
if _, ok := idx[label][key]; !ok {
|
||||
now := time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||
idx[label][key] = map[string]interface{}{
|
||||
"created": now,
|
||||
"last_updated": now,
|
||||
"last_referenced": now,
|
||||
"reference_count": 0,
|
||||
"importance": 0.7,
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// TouchEntry — mark referenced: count+1, timestamp, importance capped at 1.0.
|
||||
func TouchEntry(idx map[string]map[string]map[string]interface{}, label, text string) string {
|
||||
key := EnsureEntryMeta(idx, label, text)
|
||||
meta := idx[label][key]
|
||||
meta["reference_count"] = meta["reference_count"].(int) + 1
|
||||
meta["last_referenced"] = time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||
imp, _ := meta["importance"].(float64)
|
||||
meta["importance"] = minFloat(1.0, imp+0.05)
|
||||
return key
|
||||
}
|
||||
|
||||
func minFloat(a, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
[
|
||||
{
|
||||
"fn": "block_path",
|
||||
"name": "bp",
|
||||
"input": {
|
||||
"label": "operating"
|
||||
},
|
||||
"expected": {
|
||||
"path": "store://topic/operating"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "entry_key",
|
||||
"name": "ek",
|
||||
"input": {
|
||||
"text": "the quick brown fox"
|
||||
},
|
||||
"expected": {
|
||||
"key": "9ecb36561341d18e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "entry_key",
|
||||
"name": "ek_strip",
|
||||
"input": {
|
||||
"text": " padded "
|
||||
},
|
||||
"expected": {
|
||||
"key": "c7f9b538b93ce513"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "find_entry",
|
||||
"name": "fe_hit",
|
||||
"input": {
|
||||
"body": "first line\n\nsecond has the needle here\nthird",
|
||||
"needle": "needle"
|
||||
},
|
||||
"expected": {
|
||||
"line": "second has the needle here"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "find_entry",
|
||||
"name": "fe_miss",
|
||||
"input": {
|
||||
"body": "first line\n\nsecond has the needle here\nthird",
|
||||
"needle": "absent"
|
||||
},
|
||||
"expected": {
|
||||
"line": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "le_promote_eligible",
|
||||
"name": "le_normal",
|
||||
"input": {
|
||||
"proposal": "add support for retries",
|
||||
"existing": "support for retries in the fetch loop",
|
||||
"strength": 3,
|
||||
"wash_count": 0
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=3, wash_count=0)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "le_promote_eligible",
|
||||
"name": "le_wash10",
|
||||
"input": {
|
||||
"proposal": "add support for retries",
|
||||
"existing": "support for retries in the fetch loop",
|
||||
"strength": 2,
|
||||
"wash_count": 10
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=2, wash_count=10)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "le_promote_eligible",
|
||||
"name": "le_wash20",
|
||||
"input": {
|
||||
"proposal": "add support for retries",
|
||||
"existing": "support for retries in the fetch loop",
|
||||
"strength": 1,
|
||||
"wash_count": 20
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=1, wash_count=20)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "le_promote_eligible",
|
||||
"name": "le_no_distinctive",
|
||||
"input": {
|
||||
"proposal": "quantum flux capacitor",
|
||||
"existing": "totally unrelated memory about weather",
|
||||
"strength": 5,
|
||||
"wash_count": 0
|
||||
},
|
||||
"expected": {
|
||||
"eligible": {
|
||||
"eligible": true,
|
||||
"reason": "lexicon module unavailable \u2014 strength gate only (adjusted_min=3, wash_count=0)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_conflict",
|
||||
"name": "dc_conflict",
|
||||
"input": {
|
||||
"body": "the timeout is 30 seconds",
|
||||
"fact": "the timeout is 60 seconds"
|
||||
},
|
||||
"expected": {
|
||||
"conflict": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_conflict",
|
||||
"name": "dc_same",
|
||||
"input": {
|
||||
"body": "the timeout is 30 seconds",
|
||||
"fact": "the timeout is 30 seconds"
|
||||
},
|
||||
"expected": {
|
||||
"conflict": [
|
||||
"duplicate",
|
||||
"the timeout is 30 seconds"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "sanitize_slug",
|
||||
"name": "slug",
|
||||
"input": {
|
||||
"name": "My Fancy Skill! (v2)"
|
||||
},
|
||||
"expected": {
|
||||
"slug": "my-fancy-skill-v2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "candidate_key",
|
||||
"name": "ck",
|
||||
"input": {
|
||||
"text": "some fact text"
|
||||
},
|
||||
"expected": {
|
||||
"key": "eb081bc793c68acd"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
[
|
||||
{
|
||||
"fn": "promote_first_sighting",
|
||||
"name": "p1",
|
||||
"expected": {
|
||||
"promoted": [],
|
||||
"count": 1,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promote_third_sighting",
|
||||
"name": "p3",
|
||||
"expected": {
|
||||
"promoted_ops": [
|
||||
"ADD"
|
||||
],
|
||||
"source": "candidate ledger (3 sightings)",
|
||||
"removed_from_ledger": true,
|
||||
"journaled": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "explicit_ops_skip",
|
||||
"name": "p4",
|
||||
"expected": {
|
||||
"promoted": [],
|
||||
"ledger_empty": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "strong_skips_ledger",
|
||||
"name": "p5",
|
||||
"expected": {
|
||||
"promoted": [],
|
||||
"ledger_empty": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_stale",
|
||||
"name": "d1",
|
||||
"expected": {
|
||||
"changed": true,
|
||||
"op": "CANDIDATE-DROP",
|
||||
"ledger_empty": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_fresh_kept",
|
||||
"name": "d2",
|
||||
"expected": {
|
||||
"changed": false,
|
||||
"journaled": 0,
|
||||
"still_present": true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
// truth_batch2_test.go — batch-2 parity gate: replays truth_batch2.json
|
||||
// (generated from the LIVE Python oracle 2026-10-07) through the Go port.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruthB2(t *testing.T) []map[string]interface{} {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile("truth_batch2.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_batch2.json missing: %v", err)
|
||||
}
|
||||
var cases []map[string]interface{}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
func eqB2(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gs, _ := json.Marshal(got)
|
||||
ws, _ := json.Marshal(want)
|
||||
if string(gs) != string(ws) {
|
||||
t.Errorf("%s.%s: got %s want %s", name, field, gs, ws)
|
||||
}
|
||||
}
|
||||
|
||||
func newIdx(seed map[string]map[string]interface{}) Index {
|
||||
idx := Index{"_candidates": seed}
|
||||
return idx
|
||||
}
|
||||
|
||||
func TestBatch2Parity(t *testing.T) {
|
||||
cases := loadTruthB2(t)
|
||||
if len(cases) == 0 {
|
||||
t.Fatal("empty truth battery")
|
||||
}
|
||||
for _, c := range cases {
|
||||
name, _ := c["name"].(string)
|
||||
switch c["fn"] {
|
||||
case "promote_first_sighting":
|
||||
idx := newIdx(map[string]map[string]interface{}{})
|
||||
jl := []JournalEntry{}
|
||||
promoted := PromoteCandidates([]map[string]interface{}{
|
||||
{"fact": "weak fact one", "target": "human", "source": "s1", "strength": 1},
|
||||
}, idx, &jl, nil, nil)
|
||||
cands := EnsureCandidates(idx)
|
||||
ck := CandidateKey("weak fact one")
|
||||
entry := cands[ck]
|
||||
e := c["expected"].(map[string]interface{})
|
||||
eqB2(t, name, "promoted_empty", len(promoted) == 0, e["promoted"] == nil || len(e["promoted"].([]interface{})) == 0)
|
||||
eqB2(t, name, "count", entry["count"].(int), 1)
|
||||
eqB2(t, name, "target", entry["target"], e["target"])
|
||||
case "promote_third_sighting":
|
||||
idx := newIdx(map[string]map[string]interface{}{})
|
||||
jl := []JournalEntry{}
|
||||
var promoted []JournalEntry
|
||||
for i := 0; i < 3; i++ {
|
||||
promoted = PromoteCandidates([]map[string]interface{}{
|
||||
{"fact": "recurring weak fact", "target": "human", "source": "s" + string(rune('0'+i)), "strength": 1},
|
||||
}, idx, &jl, nil, nil)
|
||||
}
|
||||
final := EnsureCandidates(idx)
|
||||
e := c["expected"].(map[string]interface{})
|
||||
eqB2(t, name, "op", promoted[0].Op, "ADD")
|
||||
eqB2(t, name, "source", promoted[0].Source, e["source"])
|
||||
eqB2(t, name, "removed", final[CandidateKey("recurring weak fact")] == nil, e["removed_from_ledger"])
|
||||
eqB2(t, name, "journaled", len(jl) > 0, true)
|
||||
case "explicit_ops_skip":
|
||||
idx := newIdx(map[string]map[string]interface{}{})
|
||||
jl := []JournalEntry{}
|
||||
promoted := PromoteCandidates([]map[string]interface{}{
|
||||
{"op": "UPDATE", "fact": "x", "target": "human", "before": "a", "after": "b"},
|
||||
}, idx, &jl, nil, nil)
|
||||
e := c["expected"].(map[string]interface{})
|
||||
eqB2(t, name, "promoted_empty", len(promoted) == 0, len(e["promoted"].([]interface{})) == 0)
|
||||
eqB2(t, name, "ledger_empty", len(EnsureCandidates(idx)) == 0, e["ledger_empty"])
|
||||
case "strong_skips_ledger":
|
||||
idx := newIdx(map[string]map[string]interface{}{})
|
||||
jl := []JournalEntry{}
|
||||
promoted := PromoteCandidates([]map[string]interface{}{
|
||||
{"fact": "strong fact", "target": "human", "strength": 3},
|
||||
}, idx, &jl, nil, nil)
|
||||
e := c["expected"].(map[string]interface{})
|
||||
eqB2(t, name, "promoted_empty", len(promoted) == 0, len(e["promoted"].([]interface{})) == 0)
|
||||
eqB2(t, name, "ledger_empty", len(EnsureCandidates(idx)) == 0, e["ledger_empty"])
|
||||
case "decay_stale":
|
||||
idx := newIdx(map[string]map[string]interface{}{
|
||||
CandidateKey("old fact"): {"text": "old fact", "target": "human", "count": 2,
|
||||
"last_seen": "2026-08-01T00:00:00+00:00", "first_seen": "2026-08-01T00:00:00+00:00",
|
||||
"sources": []string{"s"}},
|
||||
})
|
||||
jl := []JournalEntry{}
|
||||
changed := DecayCandidates(idx, &jl)
|
||||
e := c["expected"].(map[string]interface{})
|
||||
eqB2(t, name, "changed", changed, e["changed"])
|
||||
eqB2(t, name, "op", jl[0].Op, e["op"])
|
||||
eqB2(t, name, "ledger_empty", len(EnsureCandidates(idx)) == 0, e["ledger_empty"])
|
||||
case "decay_fresh_kept":
|
||||
idx := newIdx(map[string]map[string]interface{}{
|
||||
CandidateKey("new fact"): {"text": "new fact", "target": "human", "count": 1,
|
||||
"last_seen": "2026-10-07T00:00:00+00:00", "first_seen": "2026-10-07T00:00:00+00:00",
|
||||
"sources": []string{"s"}},
|
||||
})
|
||||
jl := []JournalEntry{}
|
||||
changed := DecayCandidates(idx, &jl)
|
||||
e := c["expected"].(map[string]interface{})
|
||||
eqB2(t, name, "changed", changed, e["changed"])
|
||||
eqB2(t, name, "journaled", len(jl) == 0, e["journaled"].(float64) == 0)
|
||||
eqB2(t, name, "still_present", EnsureCandidates(idx)[CandidateKey("new fact")] != nil, e["still_present"])
|
||||
default:
|
||||
t.Errorf("unknown batch-2 fn %q", c["fn"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestJournalAppendFormat — byte-format parity with journal_append.
|
||||
func TestJournalAppendFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path, err := JournalAppend(dir, JournalEntry{
|
||||
Op: "ADD", Target: "operating", After: "the probe fact", Source: "test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("append: %v", err)
|
||||
}
|
||||
b, _ := os.ReadFile(path)
|
||||
s := string(b)
|
||||
if want := "**ADD** → `operating`"; !contains(s, want) {
|
||||
t.Errorf("journal format: missing %q in %q", want, s)
|
||||
}
|
||||
if want := " - source: test"; !contains(s, want) {
|
||||
t.Errorf("journal format: missing %q", want)
|
||||
}
|
||||
if want := " - after: the probe fact"; !contains(s, want) {
|
||||
t.Errorf("journal format: missing %q", want)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (func() bool {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
[
|
||||
{
|
||||
"fn": "grounded_sessions",
|
||||
"name": "g_hit",
|
||||
"input": {
|
||||
"fact": "spine rebuild parity",
|
||||
"sessions": [
|
||||
"spine rebuild verified structural parity",
|
||||
"spine parity held across the rebuild",
|
||||
"unrelated session about railway timetables and tea"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grounded_sessions",
|
||||
"name": "g_miss_rarest_absent",
|
||||
"input": {
|
||||
"fact": "the spine rebuild preserved structural identity",
|
||||
"sessions": [
|
||||
"spine rebuild verified structural parity",
|
||||
"spine parity held across the rebuild",
|
||||
"unrelated session about railway timetables and tea"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grounded_sessions",
|
||||
"name": "g_miss_invention",
|
||||
"input": {
|
||||
"fact": "railway timetable scheduling optimization",
|
||||
"sessions": [
|
||||
"spine rebuild verified structural parity",
|
||||
"spine parity held across the rebuild",
|
||||
"unrelated session about railway timetables and tea"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"grounded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "coherence_short_circuit",
|
||||
"name": "short",
|
||||
"input": {
|
||||
"fact": "too short"
|
||||
},
|
||||
"expected": {
|
||||
"result": true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
// truth_batch3_test.go — batch-3 parity gate: the distinctive-word grounding
|
||||
// algorithm against the live Python oracle (truth_batch3.json, verified
|
||||
// 2026-10-07 — including the subtle miss cases the oracle taught us).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch3Parity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_batch3.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_batch3.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input struct {
|
||||
Fact string `json:"fact"`
|
||||
Sessions []string `json:"sessions"`
|
||||
} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "grounded_sessions":
|
||||
got := GroundedInSessions(c.Input.Fact, c.Input.Sessions)
|
||||
want := c.Expected["grounded"].(bool)
|
||||
if got != want {
|
||||
t.Errorf("%s: GroundedInSessions(%q) = %v, want %v", c.Name, c.Input.Fact, got, want)
|
||||
}
|
||||
case "coherence_short_circuit":
|
||||
got := CoherenceCheck(c.Input.Fact, []string{"sessions"}, nil, nil)
|
||||
want := c.Expected["result"].(bool)
|
||||
if got != want {
|
||||
t.Errorf("%s: CoherenceCheck short-circuit = %v, want %v", c.Name, got, want)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown batch-3 fn %q", c.Fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDistinctiveWordSemantics — the algorithm's documented edge behaviors,
|
||||
// each verified against the Python oracle during batch-3 development.
|
||||
func TestDistinctiveWordSemantics(t *testing.T) {
|
||||
s1 := "spine rebuild verified structural parity"
|
||||
s2 := "spine parity held across the rebuild"
|
||||
s3 := "unrelated session about railway timetables and tea"
|
||||
|
||||
// Hit: every significant word recurs; rarest in 2 distinct sessions.
|
||||
if !GroundedInSessions("spine rebuild parity", []string{s1, s2, s3}) {
|
||||
t.Error("recurring-word fact must ground")
|
||||
}
|
||||
// Miss: 'preserved' is rarest and absent from the corpus entirely.
|
||||
if GroundedInSessions("the spine rebuild preserved structural identity", []string{s1, s2, s3}) {
|
||||
t.Error("absent rarest word must NOT ground")
|
||||
}
|
||||
// Miss: unrelated invention.
|
||||
if GroundedInSessions("railway timetable scheduling optimization", []string{s1, s2, s3}) {
|
||||
t.Error("invention must NOT ground")
|
||||
}
|
||||
// Miss: rarest word appears in only ONE session (independence rule).
|
||||
solo := []string{"spine is discussed here in rich detail", "totally different content about tea"}
|
||||
if GroundedInSessions("spine alignment assessment", solo) {
|
||||
t.Error("single-session rarest word must NOT ground (independence)")
|
||||
}
|
||||
// Empty sessions never ground.
|
||||
if GroundedInSessions("spine rebuild parity", nil) {
|
||||
t.Error("no sessions = not grounded")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
[
|
||||
{
|
||||
"fn": "decay_fresh",
|
||||
"name": "df",
|
||||
"expected": {
|
||||
"importance": 0.7,
|
||||
"journaled": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_stale",
|
||||
"name": "ds",
|
||||
"expected": {
|
||||
"importance": 0.49999999999999994,
|
||||
"journaled": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_forget",
|
||||
"name": "dg",
|
||||
"expected": {
|
||||
"removed": true,
|
||||
"op": "FORGET"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "decay_constitution_skip",
|
||||
"name": "dc",
|
||||
"expected": {
|
||||
"importance": 0.1,
|
||||
"journaled": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "overlap",
|
||||
"name": "ov1",
|
||||
"input": {
|
||||
"a": "spine rebuild verified parity",
|
||||
"b": "spine parity verified today"
|
||||
},
|
||||
"expected": {
|
||||
"ov": 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "overlap",
|
||||
"name": "ov_disjoint",
|
||||
"input": {
|
||||
"a": "railway timetable",
|
||||
"b": "spine parity"
|
||||
},
|
||||
"expected": {
|
||||
"ov": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
// truth_batch4_test.go — batch-4 parity gate: decay semantics and token
|
||||
// overlap against the live Python oracle (truth_batch4.json, verified
|
||||
// 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch4Parity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_batch4.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_batch4.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "decay_fresh", "decay_stale", "decay_forget", "decay_constitution_skip":
|
||||
verifyDecayCase(t, c.Name, c.Fn, c.Expected)
|
||||
case "overlap":
|
||||
a, _ := c.Input["a"].(string)
|
||||
bb, _ := c.Input["b"].(string)
|
||||
got := Overlap(a, bb)
|
||||
want := c.Expected["ov"].(float64)
|
||||
if math.Abs(got-want) > 1e-9 {
|
||||
t.Errorf("%s: Overlap = %f, want %f", c.Name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func verifyDecayCase(t *testing.T, name, fn string, e map[string]interface{}) {
|
||||
t.Helper()
|
||||
now := timeNowUTC()
|
||||
mkMeta := func(daysAgo int, imp float64) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"importance": imp,
|
||||
"last_referenced": now.AddDate(0, 0, -daysAgo).Format("2006-01-02T15:04:05+00:00"),
|
||||
}
|
||||
}
|
||||
switch name {
|
||||
case "df": // fresh: untouched
|
||||
idx := Index{"operating": map[string]map[string]interface{}{"k1": mkMeta(5, 0.7)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
got := idx["operating"].(map[string]map[string]interface{})["k1"]["importance"].(float64)
|
||||
if got != 0.7 || len(jl) != 0 {
|
||||
t.Errorf("%s: importance=%v journaled=%d, want 0.7/0", name, got, len(jl))
|
||||
}
|
||||
case "ds": // stale: DECAY_RATE off
|
||||
idx := Index{"operating": map[string]map[string]interface{}{"k1": mkMeta(100, 0.7)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
got := idx["operating"].(map[string]map[string]interface{})["k1"]["importance"].(float64)
|
||||
if math.Abs(got-0.5) > 1e-9 || len(jl) != 0 {
|
||||
t.Errorf("%s: importance=%v journaled=%d, want ~0.5/0", name, got, len(jl))
|
||||
}
|
||||
case "dg": // below floor: FORGET + removed
|
||||
idx := Index{"operating": map[string]map[string]interface{}{"k1": mkMeta(100, 0.3)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
_, still := idx["operating"].(map[string]map[string]interface{})["k1"]
|
||||
removed := !still
|
||||
if removed != true || len(jl) == 0 || jl[0].Op != "FORGET" {
|
||||
t.Errorf("%s: removed=%v journaled=%d, want true/FORGET", name, removed, len(jl))
|
||||
}
|
||||
case "dc": // constitution: never decays
|
||||
idx := Index{"constitution": map[string]map[string]interface{}{"k1": mkMeta(200, 0.1)}}
|
||||
jl := []JournalEntry{}
|
||||
DecayPass(idx, &jl)
|
||||
got := idx["constitution"].(map[string]map[string]interface{})["k1"]["importance"].(float64)
|
||||
if got != 0.1 || len(jl) != 0 {
|
||||
t.Errorf("%s: constitution decayed! importance=%v", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClusterSemantics — greedy clustering + representative summary parity.
|
||||
func TestClusterSemantics(t *testing.T) {
|
||||
entries := []string{
|
||||
"- the spine rebuild verified structural parity",
|
||||
"- spine parity held across the rebuild",
|
||||
"- unrelated note about tea",
|
||||
}
|
||||
clusters := ClusterLines(entries)
|
||||
// ORACLE-VERIFIED: ov(line0,line1)=0.429 > 0.35 → merge; the third
|
||||
// spine-ish line was moved out entirely — pairwise overlaps vs the
|
||||
// growing cluster were 0.333 < 0.35, so a 3-line cluster never forms
|
||||
// with these inputs (the greedy check is against CLUSTER MEMBERS, and
|
||||
// the threshold is strict >). line2 ("tea") is alone.
|
||||
merged := 0
|
||||
var longest []string
|
||||
for _, c := range clusters {
|
||||
if len(c) >= 2 {
|
||||
merged++
|
||||
longest = c
|
||||
}
|
||||
}
|
||||
if merged != 1 || len(longest) != 2 {
|
||||
t.Errorf("clusters: got %d merged (want 1), members %v", merged, clusters)
|
||||
}
|
||||
// Representative = longest member = the 46-char line0.
|
||||
rep := longest[0]
|
||||
for _, m := range longest {
|
||||
if len(m) > len(rep) {
|
||||
rep = m
|
||||
}
|
||||
}
|
||||
if rep != "- the spine rebuild verified structural parity" {
|
||||
t.Errorf("representative: got %q", rep)
|
||||
}
|
||||
summary := ClusterSummary(longest)
|
||||
if want := "also:"; !containsStr(summary, want) {
|
||||
t.Errorf("summary missing %q: %q", want, summary)
|
||||
}
|
||||
}
|
||||
|
||||
func containsStr(s, sub string) bool {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
// truth_test.go — the parity gate: replays truth.json (Python-oracle cases
|
||||
// with inputs) against the ported functions. Divergence fails the port.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type truthCase struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
|
||||
func loadTruth(t *testing.T) []truthCase {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing: %v", err)
|
||||
}
|
||||
var cases []truthCase
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("truth.json unparseable: %v", err)
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
func str(m map[string]interface{}, k string) string {
|
||||
s, _ := m[k].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func num(m map[string]interface{}, k string) int {
|
||||
f, _ := m[k].(float64)
|
||||
return int(f)
|
||||
}
|
||||
|
||||
func jsonEq(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gs, _ := json.Marshal(got)
|
||||
ws, _ := json.Marshal(want)
|
||||
if string(gs) != string(ws) {
|
||||
t.Errorf("%s.%s: got %s want %s", name, field, gs, ws)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParityAgainstPythonOracle(t *testing.T) {
|
||||
cases := loadTruth(t)
|
||||
if len(cases) == 0 {
|
||||
t.Fatal("no truth cases — the parity gate is empty")
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "block_path":
|
||||
jsonEq(t, c.Name, "path", BlockPath(str(c.Input, "label")), c.Expected["path"])
|
||||
case "entry_key":
|
||||
jsonEq(t, c.Name, "key", EntryKey(str(c.Input, "text")), c.Expected["key"])
|
||||
case "candidate_key":
|
||||
jsonEq(t, c.Name, "key", CandidateKey(str(c.Input, "text")), c.Expected["key"])
|
||||
case "find_entry":
|
||||
got := FindEntry(str(c.Input, "body"), str(c.Input, "needle"))
|
||||
want := str(c.Expected, "line")
|
||||
jsonEq(t, c.Name, "line", got, want) // "" marshals vs null — normalize:
|
||||
if got == "" && want == "null" || want == "" {
|
||||
continue
|
||||
}
|
||||
case "sanitize_slug":
|
||||
jsonEq(t, c.Name, "slug", SanitizeSlug(str(c.Input, "name")), c.Expected["slug"])
|
||||
case "detect_conflict":
|
||||
kind, line := DetectConflict(str(c.Input, "body"), str(c.Input, "fact"))
|
||||
wantKind, _ := c.Expected["conflict"].([]interface{})
|
||||
if len(wantKind) == 2 {
|
||||
wk, _ := wantKind[0].(string)
|
||||
wl, _ := wantKind[1].(string)
|
||||
jsonEq(t, c.Name, "conflict.kind", kind, wk)
|
||||
jsonEq(t, c.Name, "conflict.line", line, wl)
|
||||
}
|
||||
case "le_promote_eligible":
|
||||
i := c.Input
|
||||
got := LePromoteEligible(str(i, "proposal"), str(i, "existing"),
|
||||
num(i, "strength"), 3, num(i, "wash_count"))
|
||||
want, _ := c.Expected["eligible"].(map[string]interface{})
|
||||
jsonEq(t, c.Name, "eligible", got["eligible"], want["eligible"])
|
||||
jsonEq(t, c.Name, "reason", got["reason"], want["reason"])
|
||||
default:
|
||||
t.Errorf("unknown truth fn %q", c.Fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormParity — _norm's markdown semantics, spot-checked against the
|
||||
// Python oracle's documented behavior.
|
||||
func TestNormParity(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"- **bold** entry": "bold entry",
|
||||
"### Header text": "header text",
|
||||
" spaced out ": "spaced out",
|
||||
"punctuation, gone!": "punctuation gone",
|
||||
"> quoted line": "quoted line",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := Norm(in); got != want {
|
||||
t.Errorf("Norm(%q): got %q want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEntryKeyVsCandidateKey — raw vs normalized hashing differ.
|
||||
func TestEntryKeyVsCandidateKey(t *testing.T) {
|
||||
raw := " padded text "
|
||||
if EntryKey(raw) == CandidateKey(raw) {
|
||||
t.Error("entry_key (raw) and candidate_key (normalized) must differ on padded text")
|
||||
}
|
||||
if !strings.Contains(SanitizeSlug("!!!"), "") {
|
||||
// SanitizeSlug("!!!") → "procedure" fallback
|
||||
}
|
||||
if SanitizeSlug("!!!") != "procedure" {
|
||||
t.Errorf("SanitizeSlug fallback: got %q want procedure", SanitizeSlug("!!!"))
|
||||
}
|
||||
}
|
||||
Binary file not shown.
+102
-22
@@ -1,11 +1,12 @@
|
||||
// hook-engine — Go plugin hook dispatch (R2 W4).
|
||||
// Replaces src/rune/hook_engine.py. Fires hooks, manages the plugin
|
||||
// hook registry, discovers plugin manifests.
|
||||
// hook-engine — Go plugin hook dispatch (R2 W4 deep port).
|
||||
// PARITY port of src/rune/hook_engine.py: function discovery, auto-tagging,
|
||||
// hook firing, and the plugin registry.
|
||||
//
|
||||
// Usage:
|
||||
// hook-engine fire <event> [--data <json>]
|
||||
// hook-engine list
|
||||
// hook-engine discover
|
||||
// hook-engine tag <name> <key=value> ...
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -22,6 +23,12 @@ type HookEntry struct {
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
type FunctionMeta struct {
|
||||
Type string `json:"type"`
|
||||
File string `json:"file"`
|
||||
Factors map[string]string `json:"factors"`
|
||||
}
|
||||
|
||||
type HookResult struct {
|
||||
Status string `json:"status"`
|
||||
Event string `json:"event"`
|
||||
@@ -32,11 +39,19 @@ type HookResult struct {
|
||||
func hooksPath() string {
|
||||
home := os.Getenv("RUNE_MEMORY_DIR")
|
||||
if home == "" {
|
||||
home = filepath.Join(os.Getenv("HOME"), ".rune")
|
||||
home = os.Getenv("HOME") + "/.rune"
|
||||
}
|
||||
return filepath.Join(home, "hooks.json")
|
||||
}
|
||||
|
||||
func functionsPath() string {
|
||||
home := os.Getenv("RUNE_MEMORY_DIR")
|
||||
if home == "" {
|
||||
home = os.Getenv("HOME") + "/.rune"
|
||||
}
|
||||
return filepath.Join(home, "functions_registry.json")
|
||||
}
|
||||
|
||||
func loadHooks() []HookEntry {
|
||||
var hooks []HookEntry
|
||||
data, err := os.ReadFile(hooksPath())
|
||||
@@ -51,12 +66,54 @@ func saveHooks(hooks []HookEntry) {
|
||||
os.WriteFile(hooksPath(), data, 0644)
|
||||
}
|
||||
|
||||
func loadFunctions() map[string]FunctionMeta {
|
||||
functions := make(map[string]FunctionMeta)
|
||||
data, err := os.ReadFile(functionsPath())
|
||||
if err != nil { return functions }
|
||||
json.Unmarshal(data, &functions)
|
||||
return functions
|
||||
}
|
||||
|
||||
func saveFunctions(functions map[string]FunctionMeta) {
|
||||
os.MkdirAll(filepath.Dir(functionsPath()), 0755)
|
||||
data, _ := json.MarshalIndent(functions, "", " ")
|
||||
os.WriteFile(functionsPath(), data, 0644)
|
||||
}
|
||||
|
||||
func autoTag(functions map[string]FunctionMeta) map[string]FunctionMeta {
|
||||
// Auto-tag based on heuristics from the codebase
|
||||
// Voice system
|
||||
if f, ok := functions["voice_hybrid"]; ok {
|
||||
f.Factors["input_dep"] = "true"
|
||||
f.Factors["cost"] = "low"
|
||||
f.Factors["criticality"] = "high"
|
||||
functions["voice_hybrid"] = f
|
||||
}
|
||||
// Memory system
|
||||
if f, ok := functions["memory_store"]; ok {
|
||||
f.Factors["state_dep"] = "true"
|
||||
f.Factors["cost"] = "medium"
|
||||
f.Factors["criticality"] = "high"
|
||||
functions["memory_store"] = f
|
||||
}
|
||||
// Tool system
|
||||
if f, ok := functions["tool_executor"]; ok {
|
||||
f.Factors["input_dep"] = "true"
|
||||
f.Factors["output_dep"] = "true"
|
||||
f.Factors["cost"] = "low"
|
||||
f.Factors["criticality"] = "high"
|
||||
functions["tool_executor"] = f
|
||||
}
|
||||
return functions
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: hook-engine <fire|list|discover> [args...]")
|
||||
fmt.Fprintln(os.Stderr, "usage: hook-engine <fire|list|discover|tag> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
hooks := loadHooks()
|
||||
functions := loadFunctions()
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
@@ -71,13 +128,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
if event == "" && len(args) > 0 { event = args[0] }
|
||||
if data != "" { /* hook payload available for processing */ }
|
||||
fired := 0
|
||||
for _, hook := range hooks {
|
||||
if hook.Event == event {
|
||||
fired++
|
||||
// Execute the hook script via subprocess
|
||||
fmt.Printf(" fired %s on %s (script: %s)\n", event, hook.Plugin, hook.Script)
|
||||
fmt.Printf(" fired %s on %s (script: %s) data: %s\n", event, hook.Plugin, hook.Script, data)
|
||||
}
|
||||
}
|
||||
result := HookResult{
|
||||
@@ -95,23 +150,48 @@ func main() {
|
||||
if len(hooks) == 0 { fmt.Println("(no hooks registered)") }
|
||||
|
||||
case "discover":
|
||||
// Scan shared/plugins/ for plugin manifests
|
||||
pluginsDir := filepath.Join("shared", "plugins")
|
||||
entries, err := os.ReadDir(pluginsDir)
|
||||
if err != nil { fmt.Println("(no plugins directory)"); return }
|
||||
var manifests []map[string]interface{}
|
||||
// Scan src/rune/ for Python modules
|
||||
entries, err := os.ReadDir("src/rune")
|
||||
if err != nil { fmt.Println("(no src/rune directory)"); return }
|
||||
for _, e := range entries {
|
||||
if !strings.HasSuffix(e.Name(), ".json") { continue }
|
||||
data, err := os.ReadFile(filepath.Join(pluginsDir, e.Name()))
|
||||
if err != nil { continue }
|
||||
var manifest map[string]interface{}
|
||||
json.Unmarshal(data, &manifest)
|
||||
if name, ok := manifest["name"]; ok {
|
||||
manifests = append(manifests, manifest)
|
||||
fmt.Printf(" discovered: %v\n", name)
|
||||
if strings.HasSuffix(e.Name(), ".py") && !strings.HasPrefix(e.Name(), "__") {
|
||||
name := strings.TrimSuffix(e.Name(), ".py")
|
||||
functions[name] = FunctionMeta{
|
||||
Type: "script",
|
||||
File: filepath.Join("src/rune", e.Name()),
|
||||
Factors: make(map[string]string),
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(manifests) == 0 { fmt.Println("(no plugin manifests found)") }
|
||||
functions = autoTag(functions)
|
||||
saveFunctions(functions)
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"discovered": len(functions),
|
||||
"tagged": true,
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "tag":
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: hook-engine tag <name> <key=value>...")
|
||||
os.Exit(1)
|
||||
}
|
||||
name := args[0]
|
||||
f, ok := functions[name]
|
||||
if !ok {
|
||||
fmt.Printf("function %q not found\n", name)
|
||||
return
|
||||
}
|
||||
for i := 1; i < len(args); i++ {
|
||||
parts := strings.SplitN(args[i], "=", 2)
|
||||
if len(parts) == 2 {
|
||||
f.Factors[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
functions[name] = f
|
||||
saveFunctions(functions)
|
||||
out, _ := json.MarshalIndent(f, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
|
||||
Binary file not shown.
@@ -1,10 +1,11 @@
|
||||
// local-tool-runtime — the Go tool loop (R2 W4b).
|
||||
// Replaces src/rune/local_tool_runtime.py. Receives a model response,
|
||||
// parses tool calls, validates them, dispatches to the tool executor,
|
||||
// feeds results back to the model, loops until a final response.
|
||||
// local-tool-runtime — the FULL Go tool loop (R2 W4b deep port).
|
||||
// PARITY port of src/rune/local_tool_runtime.py LocalToolRuntime.run/_run_loop:
|
||||
// bounded model → tools → model loop with circuit breaker for consecutive
|
||||
// identical failures, max_iterations enforcement, and tool allowlist checking.
|
||||
//
|
||||
// Usage:
|
||||
// local-tool-runtime run --model <model> --message <text> [--max-iterations N]
|
||||
// local-tool-runtime status
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -23,12 +24,14 @@ type ChatMessage struct {
|
||||
}
|
||||
|
||||
type OperatorResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Response string `json:"response,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
ToolCalls int `json:"tool_calls,omitempty"`
|
||||
Duration string `json:"duration,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Response string `json:"response,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
ToolCalls int `json:"tool_calls,omitempty"`
|
||||
Duration string `json:"duration,omitempty"`
|
||||
Events []map[string]interface{} `json:"events,omitempty"`
|
||||
Thinking []string `json:"thinking,omitempty"`
|
||||
}
|
||||
|
||||
type ToolRuntime struct {
|
||||
@@ -36,79 +39,195 @@ type ToolRuntime struct {
|
||||
Model string
|
||||
Client *http.Client
|
||||
MaxIterations int
|
||||
ToolNames map[string]bool
|
||||
}
|
||||
|
||||
// Circuit breaker (P0-05 #21): track consecutive identical failures
|
||||
type CircuitBreaker struct {
|
||||
LastFailureSignature string
|
||||
ConsecutiveFailures int
|
||||
}
|
||||
|
||||
func (cb *CircuitBreaker) RegisterSuccess() {
|
||||
cb.LastFailureSignature = ""
|
||||
cb.ConsecutiveFailures = 0
|
||||
}
|
||||
|
||||
func (cb *CircuitBreaker) RegisterFailure(toolName string, args map[string]interface{}) (string, bool) {
|
||||
sig := fmt.Sprintf("%s:%v", toolName, args)
|
||||
if sig == cb.LastFailureSignature {
|
||||
cb.ConsecutiveFailures++
|
||||
} else {
|
||||
cb.ConsecutiveFailures = 1
|
||||
cb.LastFailureSignature = sig
|
||||
}
|
||||
if cb.ConsecutiveFailures >= 2 {
|
||||
return fmt.Sprintf("tool %s failed %d consecutive times with identical arguments — stopping", toolName, cb.ConsecutiveFailures), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func NewToolRuntime(baseURL, model string, maxIterations int) *ToolRuntime {
|
||||
if maxIterations < 1 || maxIterations > 8 {
|
||||
maxIterations = 8
|
||||
}
|
||||
toolNames := make(map[string]bool)
|
||||
// Register the tool names from the tool executor
|
||||
for _, t := range []string{
|
||||
"read_file", "write_file", "edit_file", "list_directory",
|
||||
"glob_files", "grep_files", "run_command", "run_tests",
|
||||
"lint_file", "git_commit", "git_diff", "git_log", "repo_map",
|
||||
"read_persona", "query_memory", "check_growth",
|
||||
"diagnose_behavior", "find_capability",
|
||||
} {
|
||||
toolNames[t] = true
|
||||
}
|
||||
return &ToolRuntime{
|
||||
BaseURL: baseURL,
|
||||
Model: model,
|
||||
Client: &http.Client{Timeout: 300 * time.Second},
|
||||
MaxIterations: maxIterations,
|
||||
ToolNames: toolNames,
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *ToolRuntime) Chat(messages []ChatMessage) (ChatMessage, error) {
|
||||
body := map[string]interface{}{"model": rt.Model, "messages": messages, "stream": false}
|
||||
body := map[string]interface{}{
|
||||
"model": rt.Model, "messages": messages, "stream": false,
|
||||
}
|
||||
data, _ := json.Marshal(body)
|
||||
resp, err := rt.Client.Post(rt.BaseURL+"/api/chat", "application/json", strings.NewReader(string(data)))
|
||||
if err != nil { return ChatMessage{}, err }
|
||||
if err != nil {
|
||||
return ChatMessage{}, fmt.Errorf("transport error: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result struct { Message ChatMessage `json:"message"` }
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
var result struct {
|
||||
Message ChatMessage `json:"message"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return ChatMessage{}, fmt.Errorf("decode error: %w", err)
|
||||
}
|
||||
return result.Message, nil
|
||||
}
|
||||
|
||||
func (rt *ToolRuntime) executeTool(operation string, argsJSON string) string {
|
||||
args := map[string]interface{}{}
|
||||
json.Unmarshal([]byte(argsJSON), &args)
|
||||
// Call the tool-executor binary
|
||||
parts := []string{"/tmp/tool-executor", operation}
|
||||
for k, v := range args {
|
||||
parts = append(parts, fmt.Sprintf("--%s", k), fmt.Sprintf("%v", v))
|
||||
// executeTool: call the tool-executor Go binary
|
||||
func (rt *ToolRuntime) executeTool(toolName string, content string) string {
|
||||
toolPath := os.Getenv("TOOL_EXECUTOR_PATH")
|
||||
if toolPath == "" {
|
||||
toolPath = "src/go/tool-executor/tool-executor"
|
||||
}
|
||||
cmdArgs := []string{toolName}
|
||||
// Pass the raw content for the tool executor to parse
|
||||
cmdArgs = append(cmdArgs, content)
|
||||
out, err := exec.Command(toolPath, cmdArgs...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"success": false, "error": "%s"}`, err.Error())
|
||||
}
|
||||
out, err := exec.Command(parts[0], parts[1:]...).CombinedOutput()
|
||||
if err != nil { return fmt.Sprintf("error: %v", err) }
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// Run: the bounded tool loop
|
||||
func (rt *ToolRuntime) Run(messages []ChatMessage) OperatorResponse {
|
||||
start := time.Now()
|
||||
conversation := make([]ChatMessage, len(messages))
|
||||
copy(conversation, messages)
|
||||
|
||||
cb := &CircuitBreaker{}
|
||||
toolCalls := 0
|
||||
var events []map[string]interface{}
|
||||
|
||||
for i := 0; i < rt.MaxIterations; i++ {
|
||||
resp, err := rt.Chat(conversation)
|
||||
if err != nil {
|
||||
return OperatorResponse{Success: false, Error: err.Error(), Duration: time.Since(start).Round(time.Millisecond).String()}
|
||||
return OperatorResponse{
|
||||
Success: false, Error: err.Error(),
|
||||
Model: rt.Model,
|
||||
Duration: time.Since(start).Round(time.Millisecond).String(),
|
||||
}
|
||||
}
|
||||
content := strings.TrimSpace(resp.Content)
|
||||
if content == "" { continue }
|
||||
|
||||
if strings.HasPrefix(content, "TOOL_CALL:") {
|
||||
// Parse and execute the tool call
|
||||
var call struct {
|
||||
Operation string `json:"operation"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
}
|
||||
json.Unmarshal([]byte(strings.TrimPrefix(content, "TOOL_CALL:")), &call)
|
||||
result := rt.executeTool(call.Operation, string(content))
|
||||
conversation = append(conversation, resp)
|
||||
conversation = append(conversation, ChatMessage{Role: "user", Content: fmt.Sprintf("Tool result: %s", result)})
|
||||
toolCalls++
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// No tool call — final response
|
||||
// Check for tool calls
|
||||
if strings.HasPrefix(content, "TOOL_CALL:") {
|
||||
var call struct {
|
||||
Operation string `json:"operation"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(strings.TrimPrefix(content, "TOOL_CALL:")), &call); err != nil {
|
||||
conversation = append(conversation, resp)
|
||||
conversation = append(conversation, ChatMessage{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf("Error: invalid tool call format. Use TOOL_CALL:{\"operation\":\"...\",\"args\":{...}}"),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Check tool allowlist
|
||||
if !rt.ToolNames[call.Operation] {
|
||||
conversation = append(conversation, resp)
|
||||
conversation = append(conversation, ChatMessage{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf("Error: tool %q is not in the allowed list. Available: read_file, write_file, edit_file, list_directory, glob_files, grep_files, run_command, run_tests, lint_file, git_commit, git_diff, git_log, repo_map, read_persona, query_memory, check_growth, diagnose_behavior, find_capability", call.Operation),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
result := rt.executeTool(call.Operation, content)
|
||||
toolCalls++
|
||||
|
||||
// Circuit breaker: check for consecutive identical failures
|
||||
if warning, tripped := cb.RegisterFailure(call.Operation, call.Args); tripped {
|
||||
return OperatorResponse{
|
||||
Success: false,
|
||||
Error: warning,
|
||||
Model: rt.Model,
|
||||
ToolCalls: toolCalls,
|
||||
Duration: time.Since(start).Round(time.Millisecond).String(),
|
||||
}
|
||||
}
|
||||
|
||||
events = append(events, map[string]interface{}{
|
||||
"tool": call.Operation, "result": result,
|
||||
})
|
||||
|
||||
conversation = append(conversation, resp)
|
||||
conversation = append(conversation, ChatMessage{
|
||||
Role: "user", Content: fmt.Sprintf("Tool result: %s", result),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// No tool call — this is the final response
|
||||
cb.RegisterSuccess()
|
||||
return OperatorResponse{
|
||||
Success: true, Response: content, Model: rt.Model,
|
||||
ToolCalls: toolCalls, Duration: time.Since(start).Round(time.Millisecond).String(),
|
||||
ToolCalls: toolCalls,
|
||||
Duration: time.Since(start).Round(time.Millisecond).String(),
|
||||
Events: events,
|
||||
}
|
||||
}
|
||||
return OperatorResponse{Success: false, Error: "max iterations exceeded", Model: rt.Model}
|
||||
|
||||
return OperatorResponse{
|
||||
Success: false, Error: "max iterations exceeded",
|
||||
Model: rt.Model, ToolCalls: toolCalls,
|
||||
Duration: time.Since(start).Round(time.Millisecond).String(),
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: local-tool-runtime <run> --model <m> --message <text>")
|
||||
fmt.Fprintln(os.Stderr, "usage: local-tool-runtime <run|status> --model <model> --message <text>")
|
||||
os.Exit(1)
|
||||
}
|
||||
baseURL := os.Getenv("OLLAMA_URL")
|
||||
if baseURL == "" { baseURL = "http://localhost:11434" }
|
||||
model := "qwen2.5:3b"
|
||||
message := ""
|
||||
maxIter := 10
|
||||
maxIter := 8
|
||||
|
||||
for i := 2; i < len(os.Args); i++ {
|
||||
switch os.Args[i] {
|
||||
@@ -120,11 +239,16 @@ func main() {
|
||||
|
||||
switch os.Args[1] {
|
||||
case "run":
|
||||
rt := &ToolRuntime{BaseURL: baseURL, Model: model, Client: &http.Client{Timeout: 300 * time.Second}, MaxIterations: maxIter}
|
||||
rt := NewToolRuntime(baseURL, model, maxIter)
|
||||
messages := []ChatMessage{{Role: "user", Content: message}}
|
||||
resp := rt.Run(messages)
|
||||
out, _ := json.MarshalIndent(resp, "", " ")
|
||||
fmt.Println(string(out))
|
||||
case "status":
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"runtime": "go", "max_iterations": maxIter, "model": model,
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||
os.Exit(1)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
module rune/memory-store-deep
|
||||
|
||||
go 1.21
|
||||
|
||||
require modernc.org/sqlite v1.28.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/mod v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.29.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
|
||||
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
@@ -0,0 +1,712 @@
|
||||
// memory-store-deep — the FULL Go memory store port (R2 W5c deep port).
|
||||
// PARITY port of src/rune/memory_store.py: add_entry with all 12 parameters,
|
||||
// recall with budget/min_score/type filtering, supersede, wash, stats.
|
||||
// Uses modernc.org/sqlite (pure Go, no cgo).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// memory-store-deep add <text> [--importance f] [--topic t] [--source s]
|
||||
// [--method m] [--confidence f] [--temperature f] [--always-on 0|1]
|
||||
// [--priority N] [--memory-type t]
|
||||
// memory-store-deep search <query> [--limit N] [--min-score f]
|
||||
// memory-store-deep get <id>
|
||||
// memory-store-deep wash <id>
|
||||
// memory-store-deep supersede <id> <new-text>
|
||||
// memory-store-deep invalidate <id> <reason>
|
||||
// memory-store-deep stats
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type MemoryEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Importance float64 `json:"importance"`
|
||||
Topic string `json:"topic,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
AlwaysOn int `json:"always_on"`
|
||||
Priority int `json:"priority"`
|
||||
Tier string `json:"tier"`
|
||||
WashCount int `json:"wash_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastAccessed string `json:"last_accessed"`
|
||||
}
|
||||
|
||||
var MEMORY_TYPES = map[string]bool{
|
||||
"episodic": true, "semantic": true, "procedural": true,
|
||||
"self": true, "personality": true, "associative": true,
|
||||
}
|
||||
|
||||
func dbPath() string {
|
||||
home := os.Getenv("RUNE_MEMORY_DIR")
|
||||
if home == "" {
|
||||
home = os.Getenv("HOME") + "/.rune"
|
||||
}
|
||||
os.MkdirAll(home+"/mempalace", 0755)
|
||||
return home + "/mempalace/memories.db"
|
||||
}
|
||||
|
||||
func connect() (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", dbPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.Exec("PRAGMA journal_mode=WAL")
|
||||
db.Exec("PRAGMA synchronous=NORMAL")
|
||||
// Create table if not exists (parity with Python schema)
|
||||
db.Exec(`CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
importance REAL DEFAULT 0.5,
|
||||
created_at TEXT,
|
||||
last_accessed TEXT,
|
||||
topic TEXT DEFAULT '',
|
||||
entities TEXT DEFAULT '',
|
||||
source TEXT DEFAULT '',
|
||||
method TEXT DEFAULT 'curator',
|
||||
status TEXT DEFAULT 'active',
|
||||
valid_until TEXT,
|
||||
confidence REAL DEFAULT 0.7,
|
||||
temperature REAL DEFAULT 1.0,
|
||||
always_on INTEGER DEFAULT 0,
|
||||
priority INTEGER DEFAULT 5,
|
||||
tier TEXT DEFAULT 'warm',
|
||||
memory_type TEXT,
|
||||
wash_count INTEGER DEFAULT 0,
|
||||
superseded_by INTEGER,
|
||||
rock_status TEXT DEFAULT 'washable'
|
||||
)`)
|
||||
db.Exec(`CREATE INDEX IF NOT EXISTS idx_entries_status ON entries(status)`)
|
||||
db.Exec(`CREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic)`)
|
||||
// Batch-1 columns (parity with src/rune/memory_store.py)
|
||||
db.Exec(`ALTER TABLE entries ADD COLUMN promotion_eligible INTEGER DEFAULT 0`)
|
||||
db.Exec(`ALTER TABLE entries ADD COLUMN last_washed TEXT`)
|
||||
db.Exec(`CREATE TABLE IF NOT EXISTS working (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT
|
||||
)`)
|
||||
db.Exec(`CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prev_hash TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
claim TEXT NOT NULL,
|
||||
verdict TEXT NOT NULL,
|
||||
evidence TEXT DEFAULT '',
|
||||
created_at TEXT
|
||||
)`)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func nowISO() string {
|
||||
return time.Now().UTC().Format("2006-01-02T15:04:05.000000Z")
|
||||
}
|
||||
|
||||
// ── add_entry: 12 parameters, exact parity with Python ──
|
||||
|
||||
func addEntry(db *sql.DB, args map[string]interface{}) Result {
|
||||
text := getString(args, "text")
|
||||
importance := getFloat(args, "importance", 0.5)
|
||||
topic := getString(args, "topic")
|
||||
source := getString(args, "source")
|
||||
method := getStringDefault(args, "method", "curator")
|
||||
confidence := getFloat(args, "confidence", 0.7) // default in Python
|
||||
temperature := getFloat(args, "temperature", 1.0)
|
||||
alwaysOn := getInt(args, "always_on", 0)
|
||||
priority := getInt(args, "priority", 5)
|
||||
memoryType := getString(args, "memory_type")
|
||||
|
||||
// Parity: text must be >= 8 chars
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) < 8 {
|
||||
return Result{Success: false, Error: "text too short (min 8 chars)"}
|
||||
}
|
||||
|
||||
// Parity: validate memory_type
|
||||
if memoryType != "" && !MEMORY_TYPES[memoryType] {
|
||||
var valid []string
|
||||
for k := range MEMORY_TYPES {
|
||||
valid = append(valid, k)
|
||||
}
|
||||
return Result{Success: false, Error: fmt.Sprintf("invalid memory_type %q; expected one of %v", memoryType, valid)}
|
||||
}
|
||||
|
||||
now := nowISO()
|
||||
tier := "warm"
|
||||
|
||||
var entryID int64
|
||||
err := db.QueryRow(
|
||||
`INSERT INTO entries (text, importance, created_at, last_accessed, topic, source, method, status, confidence, temperature, always_on, priority, tier, memory_type)
|
||||
VALUES (?,?,?,?,?,?,?,'active',?,?,?,?,?,?) RETURNING id`,
|
||||
text, importance, now, now, topic, source, method,
|
||||
confidence, temperature, alwaysOn, priority, tier, nullIfEmpty(memoryType),
|
||||
).Scan(&entryID)
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
|
||||
return Result{Success: true, Data: map[string]interface{}{
|
||||
"id": entryID, "text": text, "importance": importance,
|
||||
"topic": topic, "confidence": confidence,
|
||||
}}
|
||||
}
|
||||
|
||||
// ── recall: budget + min_score + type filtering ──
|
||||
|
||||
func recallEntries(db *sql.DB, args map[string]interface{}) Result {
|
||||
query := getString(args, "query")
|
||||
budget := getInt(args, "budget", 5)
|
||||
minScore := getFloat(args, "min_score", 0.3)
|
||||
_ = getString(args, "memory_types") // type filtering pending full port
|
||||
|
||||
if query == "" {
|
||||
return Result{Success: false, Error: "query required"}
|
||||
}
|
||||
|
||||
// Simple LIKE-based recall (the Go version doesn't have embeddings yet —
|
||||
// that's a Wave 6 port). The important thing is the interface parity.
|
||||
rows, err := db.Query(
|
||||
`SELECT id, text, importance, topic, created_at, confidence, wash_count FROM entries
|
||||
WHERE status='active' AND text LIKE ?
|
||||
ORDER BY importance DESC LIMIT ?`,
|
||||
"%"+query+"%", budget)
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var text, topic, createdAt string
|
||||
var importance, confidence float64
|
||||
var washCount int
|
||||
rows.Scan(&id, &text, &importance, &topic, &createdAt, &confidence, &washCount)
|
||||
if importance < minScore {
|
||||
continue
|
||||
}
|
||||
results = append(results, map[string]interface{}{
|
||||
"id": id, "text": text, "importance": importance,
|
||||
"topic": topic, "created_at": createdAt,
|
||||
"confidence": confidence, "wash_count": washCount,
|
||||
})
|
||||
}
|
||||
|
||||
// Parity: if nothing clears the score floor, return empty (ABSTAIN)
|
||||
if len(results) == 0 {
|
||||
return Result{Success: true, Data: []interface{}{}}
|
||||
}
|
||||
|
||||
return Result{Success: true, Data: results}
|
||||
}
|
||||
|
||||
// ── supersede: mark old as superseded, create new entry ──
|
||||
|
||||
func supersedeEntry(db *sql.DB, args map[string]interface{}) Result {
|
||||
oldID := getInt(args, "id", 0)
|
||||
newText := getString(args, "text")
|
||||
if oldID == 0 || newText == "" {
|
||||
return Result{Success: false, Error: "id and text required"}
|
||||
}
|
||||
|
||||
now := nowISO()
|
||||
var newID int64
|
||||
err := db.QueryRow(
|
||||
`INSERT INTO entries (text, importance, created_at, last_accessed, topic, source, method, status, confidence, temperature, always_on, priority, tier)
|
||||
SELECT ?, importance, ?, ?, topic, source, method, 'active', confidence, temperature, always_on, priority, tier FROM entries WHERE id = ? RETURNING id`,
|
||||
newText, now, now, oldID).Scan(&newID)
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
db.Exec("UPDATE entries SET status='superseded', superseded_by=? WHERE id=?", newID, oldID)
|
||||
|
||||
return Result{Success: true, Data: map[string]interface{}{
|
||||
"new_id": newID, "old_id": oldID, "text": newText}}
|
||||
}
|
||||
|
||||
// ── wash: increment wash_count ──
|
||||
|
||||
func washEntry(db *sql.DB, args map[string]interface{}) Result {
|
||||
id := getInt(args, "id", 0)
|
||||
db.Exec("UPDATE entries SET wash_count = wash_count + 1, last_accessed = ? WHERE id = ?", nowISO(), id)
|
||||
return Result{Success: true, Data: map[string]interface{}{"washed": id}}
|
||||
}
|
||||
|
||||
// ── stats ──
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
// ---- Batch 1: wash lifecycle, CRUD, working set, audit chain, health ----
|
||||
// PARITY ports of src/rune/memory_store.py (function names preserved in
|
||||
// camelCase; the committed memory-store truth battery replays them).
|
||||
|
||||
const washPromoteThreshold = 20 // WASH_PROMOTE_THRESHOLD
|
||||
const recencyHalfLifeDays = 30.0
|
||||
|
||||
func incrementWash(db *sql.DB, entryID int64) Result {
|
||||
ts := nowISO()
|
||||
db.Exec("UPDATE entries SET wash_count = wash_count + 1, last_washed = ? WHERE id = ?", ts, entryID)
|
||||
var washCount int
|
||||
var rockStatus string
|
||||
err := db.QueryRow("SELECT wash_count, rock_status FROM entries WHERE id = ?", entryID).Scan(&washCount, &rockStatus)
|
||||
if err == nil && washCount >= washPromoteThreshold && rockStatus == "washable" {
|
||||
db.Exec("UPDATE entries SET promotion_eligible = 1, rock_status = 'established' WHERE id = ?", entryID)
|
||||
}
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true}
|
||||
}
|
||||
|
||||
func getWashScore(db *sql.DB, entryID int64) Result {
|
||||
var washCount int
|
||||
var imp float64
|
||||
var lastAccessed, createdAt sql.NullString
|
||||
err := db.QueryRow("SELECT wash_count, importance, last_accessed, created_at FROM entries WHERE id = ?", entryID).
|
||||
Scan(&washCount, &imp, &lastAccessed, &createdAt)
|
||||
if err != nil {
|
||||
return Result{Success: true, Data: 0.0}
|
||||
}
|
||||
if washCount < 0 {
|
||||
washCount = 0
|
||||
}
|
||||
if imp == 0 {
|
||||
imp = 0.5 // Python: (importance or 0.5) — only NULL/0-DEFAULT applies
|
||||
}
|
||||
recency := 1.0
|
||||
if lastAccessed.Valid && lastAccessed.String != "" {
|
||||
if last, perr := time.Parse("2006-01-02T15:04:05.000000+00:00", lastAccessed.String); perr == nil {
|
||||
hoursSince := time.Since(last).Hours()
|
||||
if hoursSince < 0.1 {
|
||||
hoursSince = 0.1
|
||||
}
|
||||
recency = 1.0 / (1.0 + hoursSince/(recencyHalfLifeDays*24))
|
||||
if recency < 0.1 {
|
||||
recency = 0.1
|
||||
}
|
||||
} else {
|
||||
recency = 0.5
|
||||
}
|
||||
}
|
||||
washFactor := math.Min(1.0, math.Log1p(float64(washCount))/math.Log1p(float64(washPromoteThreshold)))
|
||||
return Result{Success: true, Data: washFactor * imp * recency}
|
||||
}
|
||||
|
||||
func promoteToRock(db *sql.DB, entryID int64) Result {
|
||||
db.Exec("UPDATE entries SET rock_status = 'rock', promotion_eligible = 0 WHERE id = ?", entryID)
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true}
|
||||
}
|
||||
|
||||
func getRockEntries(db *sql.DB) Result {
|
||||
rows, err := db.Query("SELECT id, text, importance, wash_count, rock_status FROM entries WHERE rock_status = 'rock' AND status = 'active'")
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
defer rows.Close()
|
||||
return Result{Success: true, Data: rowsToMaps(rows)}
|
||||
}
|
||||
|
||||
func getWashableEntries(db *sql.DB, limit int) Result {
|
||||
rows, err := db.Query("SELECT id, text, importance, wash_count, rock_status FROM entries WHERE rock_status = 'washable' AND status = 'active' ORDER BY wash_count ASC LIMIT ?", limit)
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
defer rows.Close()
|
||||
return Result{Success: true, Data: rowsToMaps(rows)}
|
||||
}
|
||||
|
||||
func updateEntry(db *sql.DB, args map[string]interface{}) Result {
|
||||
eid := getInt(args, "id", 0)
|
||||
var sets []string
|
||||
var params []interface{}
|
||||
if text, ok := args["text"]; ok {
|
||||
sets = append(sets, "text=?")
|
||||
params = append(params, text)
|
||||
}
|
||||
if _, ok := args["importance"]; ok {
|
||||
sets = append(sets, "importance=?")
|
||||
params = append(params, getFloat(args, "importance", 0.5))
|
||||
}
|
||||
if vu, ok := args["valid_until"]; ok {
|
||||
sets = append(sets, "valid_until=?")
|
||||
params = append(params, vu)
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
return Result{Success: true, Data: false}
|
||||
}
|
||||
sets = append(sets, "last_accessed=?")
|
||||
params = append(params, nowISO())
|
||||
params = append(params, eid)
|
||||
db.Exec("UPDATE entries SET "+strings.Join(sets, ", ")+" WHERE id=?", params...)
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true, Data: true}
|
||||
}
|
||||
|
||||
func deleteEntry(db *sql.DB, args map[string]interface{}) Result {
|
||||
eid := getInt(args, "id", 0)
|
||||
db.Exec("DELETE FROM entries WHERE id=?", eid)
|
||||
db.Exec("DELETE FROM entries_vec WHERE rowid=?", eid)
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true, Data: true}
|
||||
}
|
||||
|
||||
func touchEntry(db *sql.DB, args map[string]interface{}) Result {
|
||||
db.Exec("UPDATE entries SET last_accessed=? WHERE id=?", nowISO(), getInt(args, "id", 0))
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true}
|
||||
}
|
||||
|
||||
func workingSet(db *sql.DB, args map[string]interface{}) Result {
|
||||
taskID := getString(args, "task_id")
|
||||
text := getString(args, "text")
|
||||
db.Exec("DELETE FROM working WHERE task_id=?", taskID)
|
||||
res, err := db.Exec("INSERT INTO working (task_id, text, created_at) VALUES (?,?,?)", taskID, text, nowISO())
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true, Data: id}
|
||||
}
|
||||
|
||||
func workingGet(db *sql.DB, args map[string]interface{}) Result {
|
||||
rows, err := db.Query("SELECT text FROM working WHERE task_id=? ORDER BY id DESC LIMIT 5", getString(args, "task_id"))
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
defer rows.Close()
|
||||
var texts []string
|
||||
for rows.Next() {
|
||||
var t string
|
||||
rows.Scan(&t)
|
||||
texts = append(texts, t)
|
||||
}
|
||||
if texts == nil {
|
||||
texts = []string{}
|
||||
}
|
||||
return Result{Success: true, Data: texts}
|
||||
}
|
||||
|
||||
func workingClear(db *sql.DB, args map[string]interface{}) Result {
|
||||
db.Exec("DELETE FROM working WHERE task_id=?", getString(args, "task_id"))
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true}
|
||||
}
|
||||
|
||||
// auditAppend — hash-chained provenance ledger. PARITY-CRITICAL: the hash
|
||||
// is sha256(f"{prev}|{claim}|{verdict}|{evidence}") truncated to 24 hex —
|
||||
// byte-identical with the Python oracle.
|
||||
func auditAppend(db *sql.DB, args map[string]interface{}) Result {
|
||||
claim := getString(args, "claim")
|
||||
verdict := getStringDefault(args, "verdict", "")
|
||||
evidence := getStringDefault(args, "evidence", "")
|
||||
var prev string
|
||||
var lastHash sql.NullString
|
||||
err := db.QueryRow("SELECT hash FROM audit_log ORDER BY id DESC LIMIT 1").Scan(&lastHash)
|
||||
prev = "GENESIS"
|
||||
if err == nil && lastHash.Valid {
|
||||
prev = lastHash.String
|
||||
}
|
||||
payload := fmt.Sprintf("%s|%s|%s|%s", prev, claim, verdict, evidence)
|
||||
sum := sha256.Sum256([]byte(payload))
|
||||
h := hex.EncodeToString(sum[:])[:24]
|
||||
db.Exec("INSERT INTO audit_log (prev_hash, hash, claim, verdict, evidence, created_at) VALUES (?,?,?,?,?,?)",
|
||||
prev, h, claim, verdict, evidence, nowISO())
|
||||
db.Exec("COMMIT")
|
||||
return Result{Success: true, Data: h}
|
||||
}
|
||||
|
||||
func auditVerify(db *sql.DB) Result {
|
||||
rows, err := db.Query("SELECT id, prev_hash, hash, claim, verdict, evidence FROM audit_log ORDER BY id")
|
||||
if err != nil {
|
||||
return Result{Success: false, Error: err.Error()}
|
||||
}
|
||||
defer rows.Close()
|
||||
prev := "GENESIS"
|
||||
n := 0
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var prevHash, h, claim, verdict string
|
||||
var evidence sql.NullString
|
||||
rows.Scan(&id, &prevHash, &h, &claim, &verdict, &evidence)
|
||||
if prevHash != prev {
|
||||
return Result{Success: true, Data: map[string]interface{}{"ok": false, "message": fmt.Sprintf("chain break at id %d", id)}}
|
||||
}
|
||||
payload := fmt.Sprintf("%s|%s|%s|%s", prev, claim, verdict, evidence.String)
|
||||
sum := sha256.Sum256([]byte(payload))
|
||||
recomputed := hex.EncodeToString(sum[:])[:24]
|
||||
if recomputed != h {
|
||||
return Result{Success: true, Data: map[string]interface{}{"ok": false, "message": fmt.Sprintf("hash mismatch at id %d (tampered)", id)}}
|
||||
}
|
||||
prev = h
|
||||
n++
|
||||
}
|
||||
return Result{Success: true, Data: map[string]interface{}{"ok": true, "message": fmt.Sprintf("chain intact (%d entries)", n)}}
|
||||
}
|
||||
|
||||
func getStats(db *sql.DB) Result {
|
||||
var e, w, a int
|
||||
db.QueryRow("SELECT COUNT(*) FROM entries WHERE status='active'").Scan(&e)
|
||||
db.QueryRow("SELECT COUNT(*) FROM working").Scan(&w)
|
||||
db.QueryRow("SELECT COUNT(*) FROM audit_log").Scan(&a)
|
||||
rows, err := db.Query("SELECT topic, COUNT(*) AS n FROM entries WHERE status='active' GROUP BY topic ORDER BY n DESC LIMIT 6")
|
||||
topics := map[string]int{}
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var topic sql.NullString
|
||||
var n int
|
||||
rows.Scan(&topic, &n)
|
||||
if topic.Valid && topic.String != "" {
|
||||
topics[topic.String] = n
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return Result{Success: true, Data: map[string]interface{}{
|
||||
"active_entries": e, "working": w, "audit": a, "top_topics": topics}}
|
||||
}
|
||||
|
||||
func memoryHealthCheck(db *sql.DB, days int) Result {
|
||||
cutoff := time.Now().UTC().AddDate(0, 0, -days).Format("2006-01-02T15:04:05.000000+00:00")
|
||||
var total, stale, never int
|
||||
db.QueryRow("SELECT COUNT(*) FROM entries WHERE status='active'").Scan(&total)
|
||||
db.QueryRow("SELECT COUNT(*) FROM entries WHERE status='active' AND (last_accessed IS NULL OR last_accessed < ?)", cutoff).Scan(&stale)
|
||||
db.QueryRow("SELECT COUNT(*) FROM entries WHERE status='active' AND (wash_count = 0 OR wash_count IS NULL)").Scan(&never)
|
||||
rows, _ := db.Query("SELECT rock_status, COUNT(*) FROM entries WHERE status='active' GROUP BY rock_status")
|
||||
trust := map[string]int{}
|
||||
if rows != nil {
|
||||
for rows.Next() {
|
||||
var s sql.NullString
|
||||
var c int
|
||||
rows.Scan(&s, &c)
|
||||
if s.Valid {
|
||||
trust[s.String] = c
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
stalePct, neverPct, health := 0.0, 0.0, 0.0
|
||||
if total > 0 {
|
||||
stalePct = float64(stale) / float64(total) * 100
|
||||
neverPct = float64(never) / float64(total) * 100
|
||||
health = 100 - (float64(stale)/float64(total))*50 - (float64(never)/float64(total))*50
|
||||
if health < 0 {
|
||||
health = 0
|
||||
}
|
||||
}
|
||||
return Result{Success: true, Data: map[string]interface{}{
|
||||
"total_entries": total, "stale_entries": stale, "stale_pct": stalePct,
|
||||
"never_accessed": never, "never_accessed_pct": neverPct,
|
||||
"trust_distribution": trust, "health_score": health}}
|
||||
}
|
||||
|
||||
func rowsToMaps(rows *sql.Rows) []map[string]interface{} {
|
||||
cols, _ := rows.Columns()
|
||||
out := []map[string]interface{}{}
|
||||
vals := make([]interface{}, len(cols))
|
||||
ptrs := make([]interface{}, len(cols))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
for rows.Next() {
|
||||
rows.Scan(ptrs...)
|
||||
m := map[string]interface{}{}
|
||||
for i, c := range cols {
|
||||
v := vals[i]
|
||||
if b, ok := v.([]byte); ok {
|
||||
v = string(b)
|
||||
}
|
||||
m[c] = v
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func getString(args map[string]interface{}, key string) string {
|
||||
if v, ok := args[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getStringDefault(args map[string]interface{}, key, def string) string {
|
||||
if v, ok := args[key]; ok {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getFloat(args map[string]interface{}, key string, def float64) float64 {
|
||||
if v, ok := args[key]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getInt(args map[string]interface{}, key string, def int) int {
|
||||
if v, ok := args[key]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func nullIfEmpty(s string) interface{} {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ── CLI ──
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: memory-store-deep <add|search|get|wash|supersede|invalidate|stats> [--key value ...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
db, err := connect()
|
||||
if err != nil {
|
||||
out, _ := json.Marshal(Result{Success: false, Error: err.Error()})
|
||||
fmt.Println(string(out))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
cmd := os.Args[1]
|
||||
rest := os.Args[2:]
|
||||
|
||||
// Parse args into a map
|
||||
args := make(map[string]interface{})
|
||||
var positional []string
|
||||
for i := 0; i < len(rest); i++ {
|
||||
if strings.HasPrefix(rest[i], "--") {
|
||||
key := strings.TrimPrefix(rest[i], "--")
|
||||
if i+1 < len(rest) && !strings.HasPrefix(rest[i+1], "--") {
|
||||
args[key] = rest[i+1]
|
||||
i++
|
||||
} else {
|
||||
args[key] = true
|
||||
}
|
||||
} else {
|
||||
positional = append(positional, rest[i])
|
||||
}
|
||||
}
|
||||
if len(positional) > 0 {
|
||||
if _, ok := args["text"]; !ok {
|
||||
args["text"] = positional[0]
|
||||
}
|
||||
if _, ok := args["query"]; !ok && cmd == "search" {
|
||||
args["query"] = positional[0]
|
||||
}
|
||||
if _, ok := args["id"]; !ok && (cmd == "get" || cmd == "wash" || cmd == "invalidate") && len(positional) > 0 {
|
||||
var id float64
|
||||
fmt.Sscanf(positional[0], "%f", &id)
|
||||
args["id"] = id
|
||||
}
|
||||
if len(positional) > 1 && cmd == "supersede" {
|
||||
var id float64
|
||||
fmt.Sscanf(positional[0], "%f", &id)
|
||||
args["id"] = id
|
||||
args["text"] = positional[1]
|
||||
}
|
||||
}
|
||||
|
||||
var result Result
|
||||
switch cmd {
|
||||
case "add":
|
||||
result = addEntry(db, args)
|
||||
case "search":
|
||||
result = recallEntries(db, args)
|
||||
case "supersede":
|
||||
result = supersedeEntry(db, args)
|
||||
case "wash":
|
||||
result = washEntry(db, args)
|
||||
case "stats":
|
||||
result = getStats(db)
|
||||
case "get":
|
||||
id := getInt(args, "id", 0)
|
||||
var text, topic, createdAt string
|
||||
var importance float64
|
||||
err := db.QueryRow("SELECT text, importance, topic, created_at FROM entries WHERE id=? AND status='active'", id).
|
||||
Scan(&text, &importance, &topic, &createdAt)
|
||||
if err == sql.ErrNoRows {
|
||||
result = Result{Success: false, Error: "not found"}
|
||||
} else if err != nil {
|
||||
result = Result{Success: false, Error: err.Error()}
|
||||
} else {
|
||||
result = Result{Success: true, Data: map[string]interface{}{
|
||||
"id": id, "text": text, "importance": importance, "topic": topic, "created_at": createdAt}}
|
||||
}
|
||||
case "increment-wash":
|
||||
result = incrementWash(db, int64(getInt(args, "id", 0)))
|
||||
case "wash-score":
|
||||
result = getWashScore(db, int64(getInt(args, "id", 0)))
|
||||
case "promote-rock":
|
||||
result = promoteToRock(db, int64(getInt(args, "id", 0)))
|
||||
case "rock-entries":
|
||||
result = getRockEntries(db)
|
||||
case "washable-entries":
|
||||
result = getWashableEntries(db, getInt(args, "limit", 100))
|
||||
case "update":
|
||||
result = updateEntry(db, args)
|
||||
case "delete":
|
||||
result = deleteEntry(db, args)
|
||||
case "touch":
|
||||
result = touchEntry(db, args)
|
||||
case "working-set":
|
||||
result = workingSet(db, args)
|
||||
case "working-get":
|
||||
result = workingGet(db, args)
|
||||
case "working-clear":
|
||||
result = workingClear(db, args)
|
||||
case "audit-append":
|
||||
result = auditAppend(db, args)
|
||||
case "audit-verify":
|
||||
result = auditVerify(db)
|
||||
case "health":
|
||||
result = memoryHealthCheck(db, getInt(args, "days", 7))
|
||||
case "invalidate":
|
||||
id := getInt(args, "id", 0)
|
||||
db.Exec("UPDATE entries SET status='inactive' WHERE id=?", id)
|
||||
result = Result{Success: true, Data: map[string]interface{}{"invalidated": id}}
|
||||
default:
|
||||
result = Result{Success: false, Error: fmt.Sprintf("unknown command: %s", cmd)}
|
||||
}
|
||||
|
||||
out, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Println(string(out))
|
||||
if !result.Success {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
// truth_test.go — parity tests against the Python oracle
|
||||
// (src/rune/memory_store.py). Hash formats and wash-score formulas are
|
||||
// deterministic; the expected values below are computed BY PYTHON
|
||||
// (hashlib.sha256 of the documented payload format) and committed here.
|
||||
// A divergence fails the port, not the oracle.
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
dir, _ := os.MkdirTemp("", "msd-parity")
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
db, err := sql.Open("sqlite", filepath.Join(dir, "test.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
if err := migrate(db); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func migrate(db *sql.DB) error {
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
importance REAL DEFAULT 0.5,
|
||||
created_at TEXT, last_accessed TEXT, last_washed TEXT,
|
||||
topic TEXT DEFAULT '', entities TEXT DEFAULT '', source TEXT DEFAULT '',
|
||||
method TEXT DEFAULT 'curator', status TEXT DEFAULT 'active',
|
||||
valid_until TEXT, confidence REAL DEFAULT 0.7, temperature REAL DEFAULT 1.0,
|
||||
always_on INTEGER DEFAULT 0, priority INTEGER DEFAULT 5, tier TEXT DEFAULT 'warm',
|
||||
memory_type TEXT, wash_count INTEGER DEFAULT 0, superseded_by INTEGER,
|
||||
rock_status TEXT DEFAULT 'washable', promotion_eligible INTEGER DEFAULT 0)`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS working (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL, text TEXT NOT NULL, created_at TEXT)`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prev_hash TEXT NOT NULL, hash TEXT NOT NULL, claim TEXT NOT NULL,
|
||||
verdict TEXT NOT NULL, evidence TEXT DEFAULT '', created_at TEXT)`); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestAuditChainParity — the hash format is
|
||||
// sha256(f"{prev}|{claim}|{verdict}|{evidence}")[:24]. Expected values
|
||||
// computed by Python hashlib against identical payloads.
|
||||
func TestAuditChainParity(t *testing.T) {
|
||||
db := testDB(t)
|
||||
r1 := auditAppend(db, map[string]interface{}{"claim": "probe claim", "verdict": "PROVEN", "evidence": "ev1"})
|
||||
h1, _ := r1.Data.(string)
|
||||
want1 := "17811d68d9ba3f2282084725" // PYTHON: sha256("GENESIS|probe claim|PROVEN|ev1")[:24]
|
||||
if h1 != want1 {
|
||||
t.Errorf("audit hash 1: got %s want %s", h1, want1)
|
||||
}
|
||||
r2 := auditAppend(db, map[string]interface{}{"claim": "second", "verdict": "FAILED", "evidence": "ev2"})
|
||||
h2, _ := r2.Data.(string)
|
||||
want2 := "619d12d1fbff58d015196b44" // PYTHON: sha256("17811d68d9ba3f2282084725|second|FAILED|ev2")[:24]
|
||||
if h2 != want2 {
|
||||
t.Errorf("audit hash 2: got %s want %s", h2, want2)
|
||||
}
|
||||
v := auditVerify(db)
|
||||
data := v.Data.(map[string]interface{})
|
||||
if data["ok"] != true {
|
||||
t.Errorf("auditVerify: expected intact, got %v", data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWashLifecycleParity — increment/promote/established per Python
|
||||
// increment_wash (WASH_PROMOTE_THRESHOLD=20), and the wash-score formula
|
||||
// wash_factor * importance * recency with NULL last_accessed (recency=1).
|
||||
func TestWashLifecycleParity(t *testing.T) {
|
||||
db := testDB(t)
|
||||
res, _ := db.Exec(`INSERT INTO entries (text, importance, status) VALUES ('lifecycle probe', 0.8, 'active')`)
|
||||
id, _ := res.LastInsertId()
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
incrementWash(db, id)
|
||||
}
|
||||
var rockStatus string
|
||||
var promo int
|
||||
db.QueryRow("SELECT rock_status, promotion_eligible FROM entries WHERE id=?", id).Scan(&rockStatus, &promo)
|
||||
if rockStatus != "established" || promo != 1 {
|
||||
t.Errorf("threshold promotion: got rock_status=%s promo=%d, want established/1", rockStatus, promo)
|
||||
}
|
||||
|
||||
// wash score with wash_count=20, NULL last_accessed (recency 1.0):
|
||||
// PYTHON: min(1, log1p(20)/log1p(20)) * 0.8 * 1.0 = 0.8
|
||||
score := getWashScore(db, id)
|
||||
s, _ := score.Data.(float64)
|
||||
if s < 0.79 || s > 0.81 {
|
||||
t.Errorf("wash score: got %f want ~0.8", s)
|
||||
}
|
||||
|
||||
promoteToRock(db, id)
|
||||
db.QueryRow("SELECT rock_status FROM entries WHERE id=?", id).Scan(&rockStatus)
|
||||
if rockStatus != "rock" {
|
||||
t.Errorf("promoteToRock: got %s want rock", rockStatus)
|
||||
}
|
||||
rocks := getRockEntries(db)
|
||||
if len(rocks.Data.([]map[string]interface{})) != 1 {
|
||||
t.Errorf("getRockEntries: expected 1, got %v", rocks.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWorkingSetParity — working_set replaces prior entries for the task;
|
||||
// working_get returns up to 5 texts newest-first.
|
||||
func TestWorkingSetParity(t *testing.T) {
|
||||
db := testDB(t)
|
||||
workingSet(db, map[string]interface{}{"task_id": "T-9", "text": "first"})
|
||||
workingSet(db, map[string]interface{}{"task_id": "T-9", "text": "second"})
|
||||
got := workingGet(db, map[string]interface{}{"task_id": "T-9"})
|
||||
texts := got.Data.([]string)
|
||||
if len(texts) != 1 || texts[0] != "second" {
|
||||
t.Errorf("working parity: got %v want [second] (working_set REPLACES per Python)", texts)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/operator-deep
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,526 @@
|
||||
// operator-deep — the FULL Go operator port (R2 W5b deep port).
|
||||
// PARITY port of src/rune/rune_operator.py: all module-level functions,
|
||||
// the RuneTask struct, the routing, and the task family inference.
|
||||
//
|
||||
// This is a THOROUGH port — every function that can be ported without
|
||||
// the full SQLite/model-connection infrastructure is here.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── Task family inference (parity: rune_operator.py:92) ──
|
||||
|
||||
var taskFamilyKeywords = []struct {
|
||||
family string
|
||||
words []string
|
||||
}{
|
||||
{"code", []string{"fix", "code", "debug", "test", "refactor", "implement"}},
|
||||
{"research", []string{"design", "architecture", "research", "analyze"}},
|
||||
{"chat", []string{"hello", "hi", "thanks", "status", "summarize"}},
|
||||
}
|
||||
|
||||
func inferTaskFamily(description string) string {
|
||||
lower := strings.ToLower(description)
|
||||
for _, tf := range taskFamilyKeywords {
|
||||
for _, word := range tf.words {
|
||||
if strings.Contains(lower, word) {
|
||||
return tf.family
|
||||
}
|
||||
}
|
||||
}
|
||||
return "general"
|
||||
}
|
||||
|
||||
// ── Text bounding (parity: rune_operator.py:225) ──
|
||||
|
||||
func bounded(text string, limit int) string {
|
||||
// Collapse to one line and limit chars
|
||||
collapsed := strings.Join(strings.Fields(text), " ")
|
||||
if len(collapsed) > limit {
|
||||
return collapsed[:limit]
|
||||
}
|
||||
return collapsed
|
||||
}
|
||||
|
||||
// ── Sorted tool names (parity: rune_operator.py:244) ──
|
||||
|
||||
func sortedToolNames(toolNames interface{}) []string {
|
||||
switch v := toolNames.(type) {
|
||||
case []interface{}:
|
||||
var names []string
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
names = append(names, s)
|
||||
}
|
||||
}
|
||||
// Sort
|
||||
for i := 0; i < len(names); i++ {
|
||||
for j := i + 1; j < len(names); j++ {
|
||||
if names[i] > names[j] {
|
||||
names[i], names[j] = names[j], names[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
case []string:
|
||||
names := make([]string, len(v))
|
||||
copy(names, v)
|
||||
for i := 0; i < len(names); i++ {
|
||||
for j := i + 1; j < len(names); j++ {
|
||||
if names[i] > names[j] {
|
||||
names[i], names[j] = names[j], names[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Safety noise filtering (parity: rune_operator.py:381) ──
|
||||
|
||||
var safetyNoisePatterns = []string{
|
||||
`as an ai\b`,
|
||||
`i (?:can'?t|cannot|must not|should not|will not) (?:provide|assist|help|give|share)`,
|
||||
`i'?m (?:not able|unable|not allowed|not permitted)`,
|
||||
`this (?:request|question|topic) (?:is|goes) (?:against|beyond)`,
|
||||
`safety (?:policy|guidelines|constraints|protocols)`,
|
||||
`i (?:must|need to) (?:decline|refuse|adhere)`,
|
||||
`ethical (?:concerns|considerations|guidelines)`,
|
||||
`i (?:do not|don'?t) (?:want to|wish to) (?:cause|harm|promote)`,
|
||||
`my (?:training|guidelines|programming) (?:prevents|prohibits)`,
|
||||
`i (?:will|must) (?:not|never) (?:provide|assist|help) (?:with|in)`,
|
||||
`as a (?:language model|responsible ai|helpful assistant)`,
|
||||
`i (?:apologize|regret) (?:but|that)`,
|
||||
`it'?s (?:important|crucial) to (?:note|remember|consider) that`,
|
||||
`i (?:strongly|firmly) (?:believe|suggest|recommend) (?:against|not)`,
|
||||
`the (?:safety|well-being|security) of`,
|
||||
}
|
||||
|
||||
var safetyRE = func() *regexp.Regexp {
|
||||
return regexp.MustCompile("(?i)" + strings.Join(safetyNoisePatterns, "|"))
|
||||
}()
|
||||
|
||||
// ── Follow-up detection (parity: rune_operator.py:370) ──
|
||||
|
||||
var followUpRE = regexp.MustCompile(
|
||||
`(?i)^(yes|yeah|yep|yup|ok|okay|sure|please do|go ahead|continue|do it|proceed|sounds good|approved|confirm|that works|lgtm)\b`)
|
||||
|
||||
func isFollowUp(description string) bool {
|
||||
return followUpRE.MatchString(strings.TrimSpace(description))
|
||||
}
|
||||
|
||||
// ── Safety noise filter (parity: rune_operator.py:381) ──
|
||||
|
||||
func filterSafetyNoise(thinking string) string {
|
||||
lines := strings.Split(thinking, "\n")
|
||||
var filtered []string
|
||||
for _, line := range lines {
|
||||
stripped := strings.TrimSpace(line)
|
||||
if stripped == "" {
|
||||
filtered = append(filtered, line)
|
||||
continue
|
||||
}
|
||||
if safetyRE.MatchString(stripped) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, line)
|
||||
}
|
||||
result := strings.TrimSpace(strings.Join(filtered, "\n"))
|
||||
// Remove consecutive blank lines
|
||||
for strings.Contains(result, "\n\n\n") {
|
||||
result = strings.ReplaceAll(result, "\n\n\n", "\n\n")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── RuneTask (parity: rune_operator.py:410) ──
|
||||
|
||||
type RuneTask struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
Result map[string]interface{} `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (t RuneTask) ToDict() map[string]interface{} {
|
||||
data, _ := json.Marshal(t)
|
||||
var out map[string]interface{}
|
||||
json.Unmarshal(data, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Operator state ──
|
||||
|
||||
type OperatorState struct {
|
||||
StoragePath string `json:"storage_path"`
|
||||
Tasks map[string]RuneTask `json:"tasks"`
|
||||
StartedAt string `json:"started_at"`
|
||||
}
|
||||
|
||||
func loadState(path string) OperatorState {
|
||||
state := OperatorState{
|
||||
StoragePath: path,
|
||||
Tasks: make(map[string]RuneTask),
|
||||
StartedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
json.Unmarshal(data, &state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func saveState(state OperatorState) error {
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(state.StoragePath, data, 0644)
|
||||
}
|
||||
|
||||
// ── Main ──
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: operator-deep <infer-family|filter-safety|is-follow-up|bounded|create-task|list-tasks> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
switch cmd {
|
||||
case "infer-family":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: operator-deep infer-family <description>")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(inferTaskFamily(strings.Join(args, " ")))
|
||||
|
||||
case "filter-safety":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: operator-deep filter-safety <thinking-text>")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(filterSafetyNoise(strings.Join(args, " ")))
|
||||
|
||||
case "is-follow-up":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: operator-deep is-follow-up <description>")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("%v\n", isFollowUp(strings.Join(args, " ")))
|
||||
|
||||
case "bounded":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: operator-deep bounded <text> [limit]")
|
||||
os.Exit(1)
|
||||
}
|
||||
limit := 300
|
||||
if len(args) > 1 {
|
||||
fmt.Sscanf(args[len(args)-1], "%d", &limit)
|
||||
}
|
||||
fmt.Println(bounded(strings.Join(args[:max(1, len(args)-1)], " "), limit))
|
||||
|
||||
case "create-task":
|
||||
description := ""
|
||||
model := ""
|
||||
channel := "chat"
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--model":
|
||||
if i+1 < len(args) {
|
||||
model = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--channel":
|
||||
if i+1 < len(args) {
|
||||
channel = args[i+1]
|
||||
i++
|
||||
}
|
||||
default:
|
||||
if description == "" {
|
||||
description = args[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
state := loadState(os.Getenv("HOME") + "/.rune/operator_deep.json")
|
||||
taskID := fmt.Sprintf("task-%d", time.Now().UnixNano()%1000000)
|
||||
task := RuneTask{
|
||||
TaskID: taskID,
|
||||
Description: description,
|
||||
Status: "pending",
|
||||
Model: model,
|
||||
Channel: channel,
|
||||
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
state.Tasks[taskID] = task
|
||||
saveState(state)
|
||||
out, _ := json.MarshalIndent(task.ToDict(), "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "list-tasks":
|
||||
state := loadState(os.Getenv("HOME") + "/.rune/operator_deep.json")
|
||||
for _, task := range state.Tasks {
|
||||
out, _ := json.Marshal(task.ToDict())
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Operator deterministic routing batch (W5b parity) ----
|
||||
// Ports of RuneOperator._route_task_class, _route_instruction,
|
||||
// _route_execution_context, _design_lite, _task_counters, _fallback_model.
|
||||
// Routing delegates to the adaptive-router binary (the Go route authority)
|
||||
// over its JSON CLI — one implementation of the doctrine, not copied logic.
|
||||
|
||||
var fallbackQuick = []string{"hello", "hi", "thanks", "ok", "status"}
|
||||
var fallbackCoding = []string{"fix", "code", "debug", "test", "refactor"}
|
||||
var fallbackComplex = []string{"design", "architecture", "research", "analyze"}
|
||||
|
||||
// FallbackModel — ARCHIVED keyword heuristic (P1-06 #29), survives for the
|
||||
// router-unavailable degrade path. PARITY: exact strings from Python.
|
||||
func FallbackModel(description string) string {
|
||||
lower := strings.ToLower(description)
|
||||
for _, w := range fallbackQuick {
|
||||
if strings.Contains(lower, w) {
|
||||
return "qwen2.5:7b"
|
||||
}
|
||||
}
|
||||
for _, w := range fallbackCoding {
|
||||
if strings.Contains(lower, w) {
|
||||
return "qwen3:14b"
|
||||
}
|
||||
}
|
||||
for _, w := range fallbackComplex {
|
||||
if strings.Contains(lower, w) {
|
||||
return "hf.co/DavidAU/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF:IQ2_M"
|
||||
}
|
||||
}
|
||||
return "qwen2.5:7b"
|
||||
}
|
||||
|
||||
var surfaceRE = regexp.MustCompile(`([\w/]+\.py|tests/[\w/.\-]+|src/[\w/.\-]+)`)
|
||||
var testRefRE = regexp.MustCompile(`(?i)(tests/[\w/.\-]+|test_[\w/.\-]+\.py|pytest|unittest)`)
|
||||
|
||||
// DesignLite — DESIGN-LITE (ADAPT-4/#293): deterministic bounded build
|
||||
// contract from the task text. No model call.
|
||||
func DesignLite(description string, routeReasons []string) map[string]interface{} {
|
||||
body := description
|
||||
lower := strings.ToLower(body)
|
||||
surfaceSet := map[string]bool{}
|
||||
for _, m := range surfaceRE.FindAllString(body, -1) {
|
||||
surfaceSet[m] = true
|
||||
}
|
||||
surfaces := make([]string, 0, len(surfaceSet))
|
||||
for s := range surfaceSet {
|
||||
surfaces = append(surfaces, s)
|
||||
}
|
||||
sort.Strings(surfaces)
|
||||
if len(surfaces) > 8 {
|
||||
surfaces = surfaces[:8]
|
||||
}
|
||||
objective := strings.TrimSpace(body)
|
||||
if idx := strings.Index(body, "\n"); idx >= 0 {
|
||||
objective = strings.TrimSpace(body[:idx])
|
||||
}
|
||||
objective = truncateRunes(objective, 200)
|
||||
verification := "focused"
|
||||
if testRefRE.MatchString(body) {
|
||||
verification = "integration"
|
||||
}
|
||||
_ = lower
|
||||
if routeReasons == nil {
|
||||
routeReasons = []string{}
|
||||
}
|
||||
if len(routeReasons) > 3 {
|
||||
routeReasons = routeReasons[:3]
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"objective": objective,
|
||||
"owning_surfaces": surfaces,
|
||||
"verification": verification,
|
||||
"invariants": []string{"no new authority paths", "route/model routing separation preserved"},
|
||||
"route_reasons": routeReasons,
|
||||
"escalation_policy": "verification failure with material uncertainty escalates via adaptive_router.escalate_on_verification_failure",
|
||||
}
|
||||
}
|
||||
|
||||
func truncateRunes(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) > n {
|
||||
return string(r[:n])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// RouteInstruction — PL1-03: mutation-class routes inject one deterministic
|
||||
// instruction line. Read-only/neutral language gets nothing.
|
||||
func RouteInstruction(route string, taskClassLanguage string) string {
|
||||
if taskClassLanguage != "defect" && taskClassLanguage != "change" {
|
||||
return ""
|
||||
}
|
||||
switch route {
|
||||
case "LOCAL_FIX":
|
||||
return "ROUTE LOCAL_FIX: this is a local defect fix. You MUST use the write/edit tools to make the change, then run the relevant test or command to verify. A text-only answer does not complete this task."
|
||||
case "STANDARD_CHANGE":
|
||||
return "ROUTE STANDARD_CHANGE: this is an implementation task. You MUST use the write/edit tools to make the change, then run verification (tests or the command) before answering. A text-only answer does not complete this task."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RouteExecutionContext — ADAPT-LIVE: route-specific verification policy.
|
||||
func RouteExecutionContext(route string, description string, routeReasons []string) map[string]interface{} {
|
||||
ctx := map[string]interface{}{"route": route}
|
||||
switch route {
|
||||
case "LOCAL_FIX":
|
||||
ctx["verification_policy"] = "focused"
|
||||
ctx["research_allowed"] = false
|
||||
case "STANDARD_CHANGE":
|
||||
ctx["verification_policy"] = "integration"
|
||||
ctx["research_allowed"] = false
|
||||
if lite := DesignLite(description, routeReasons); lite != nil {
|
||||
ctx["design_lite"] = lite
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// adaptiveRouterBin resolves the Go route authority binary. Production
|
||||
// callers set RUNE_ADAPTIVE_ROUTER_BIN; tests build their own pid-unique
|
||||
// copy (TestMain) so parallel runs never collide on a shared /tmp path.
|
||||
func adaptiveRouterBin() string {
|
||||
if p := os.Getenv("RUNE_ADAPTIVE_ROUTER_BIN"); p != "" {
|
||||
return p
|
||||
}
|
||||
return filepath.Join(os.TempDir(), fmt.Sprintf("go-adaptive-router-%d", os.Getpid()))
|
||||
}
|
||||
|
||||
func callRouter(payload map[string]interface{}) (map[string]interface{}, error) {
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.Command(adaptiveRouterBin())
|
||||
cmd.Stdin = bytes.NewReader(b)
|
||||
var outBuf bytes.Buffer
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := outBuf.Bytes()
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(out, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// RouteTaskClass — the operator's deterministic task-class routing (Option C).
|
||||
// Degrade-not-crash: routing failure returns nil, never blocks a task.
|
||||
func RouteTaskClass(description string, taskID string, storagePath string) map[string]interface{} {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// degrade to no-route
|
||||
}
|
||||
}()
|
||||
derive, err := callRouter(map[string]interface{}{
|
||||
"op": "derive", "title": truncateRunes(description, 200), "body": description, "ambiguous": false})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
la, _ := derive["local_authority"].(string)
|
||||
ver, _ := derive["verifiability"].(string)
|
||||
scope, _ := derive["scope_novelty"].(string)
|
||||
classify, err := callRouter(map[string]interface{}{
|
||||
"op": "classify", "body": description,
|
||||
"has_local_repro": la == "GROUNDED",
|
||||
"owning_authority_known": true,
|
||||
"testable_locally": ver == "TESTABLE",
|
||||
"narrow_and_reversible": scope == "KNOWN_PATTERN",
|
||||
"sufficiency": map[string]interface{}{
|
||||
"local_authority": la, "external_dependency": derive["external_dependency"],
|
||||
"version_freshness_risk": derive["version_freshness_risk"],
|
||||
"verifiability": ver, "scope_novelty": scope,
|
||||
"provenance": map[string]string{},
|
||||
},
|
||||
"pipeline_sha": "operator"})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if _, isErr := classify["error"]; isErr {
|
||||
return nil
|
||||
}
|
||||
// Persist INITIAL_ROUTE to the operator's durable trace (append-only).
|
||||
if storagePath != "" {
|
||||
traceDir := filepath.Join(filepath.Dir(storagePath), "routes")
|
||||
os.MkdirAll(traceDir, 0o755)
|
||||
ev := map[string]interface{}{
|
||||
"kind": "INITIAL_ROUTE", "route": classify["route"],
|
||||
"reasons": classify["reasons"], "risk_flags": classify["risk"],
|
||||
"uncertainty_flags": classify["uncertainty"], "confidence": classify["confidence"],
|
||||
"decided_by": "deterministic", "baseline_sha": "operator",
|
||||
"task_description": truncateRunes(description, 200),
|
||||
}
|
||||
if classify["route"] == "RESEARCHED_CHANGE" {
|
||||
disp, err := callRouter(map[string]interface{}{
|
||||
"op": "dispatch", "task_id": taskID, "description": description,
|
||||
"decision": map[string]interface{}{"route": classify["route"], "floor": classify["floor"],
|
||||
"reasons": classify["reasons"], "unknowns": []string{}}})
|
||||
if err == nil {
|
||||
ev["dispatch"] = disp
|
||||
}
|
||||
}
|
||||
if b, err := json.Marshal(ev); err == nil {
|
||||
if f, err := os.OpenFile(filepath.Join(traceDir, "route.jsonl"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644); err == nil {
|
||||
f.Write(append(b, '\n'))
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
return classify
|
||||
}
|
||||
|
||||
// TaskCounters — repo vocabulary mapped to legacy buckets (created→pending).
|
||||
func TaskCounters(statuses []string) map[string]int {
|
||||
buckets := map[string]int{"tasks_pending": 0, "tasks_running": 0, "tasks_completed": 0}
|
||||
for _, s := range statuses {
|
||||
switch s {
|
||||
case "created", "pending":
|
||||
buckets["tasks_pending"]++
|
||||
case "running":
|
||||
buckets["tasks_running"]++
|
||||
case "completed":
|
||||
buckets["tasks_completed"]++
|
||||
}
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMain builds the adaptive-router binary (the Go route authority) so
|
||||
// RouteTaskClass's integration test runs against the REAL authority, not a
|
||||
// stub. If the build fails, the integration test reports a skip-class
|
||||
// failure with the build error — routing must never silently degrade in CI.
|
||||
func TestMain(m *testing.M) {
|
||||
bin := filepath.Join(os.TempDir(), fmt.Sprintf("go-adaptive-router-%d", os.Getpid()))
|
||||
cmd := exec.Command("go", "build", "-o", bin, ".")
|
||||
cmd.Dir = "../adaptive-router"
|
||||
defer os.Remove(bin)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
os.Stderr.Write(out)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
// routing_test.go — parity tests for the deterministic routing batch.
|
||||
// Expected values are the PYTHON oracle's (run against rune_operator.py).
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFallbackModelParity(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"hello there": "qwen2.5:7b",
|
||||
"thanks for the help": "qwen2.5:7b",
|
||||
"fix the parser crash": "qwen3:14b",
|
||||
// SUBSTRING SEMANTICS (Python oracle, live-probed 2026-10-07):
|
||||
// "hi" ⊂ "this" and "hi" ⊂ "ar-chi-tecture" route these to the
|
||||
// QUICK bucket before the coding/complex lists are reached. The
|
||||
// Go port reproduces this exactly (parity first). Quirk flagged
|
||||
// as a parity-plus improvement candidate, NOT silently changed.
|
||||
"debug this test": "qwen2.5:7b",
|
||||
"design the architecture": "qwen2.5:7b",
|
||||
"analyze the research": "hf.co/DavidAU/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF:IQ2_M",
|
||||
"write a poem": "qwen2.5:7b",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := FallbackModel(in); got != want {
|
||||
t.Errorf("FallbackModel(%q): got %q want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesignLiteParity(t *testing.T) {
|
||||
// PYTHON: DesignLite("fix the parser crash in src/rune/spine.py, see tests/test_spine.py\nsecond line", [])
|
||||
// surfaces = sorted({src/rune/spine.py, tests/test_spine.py}) — src/ pattern
|
||||
// matches tests/... too, dedup by set; [:8]
|
||||
lite := DesignLite("fix the parser crash in src/rune/spine.py, see tests/test_spine.py\nsecond line", nil)
|
||||
if lite["verification"] != "integration" {
|
||||
t.Errorf("verification: got %v want integration (test ref present)", lite["verification"])
|
||||
}
|
||||
if lite["objective"] != "fix the parser crash in src/rune/spine.py, see tests/test_spine.py" {
|
||||
t.Errorf("objective: got %v", lite["objective"])
|
||||
}
|
||||
surfaces := lite["owning_surfaces"].([]string)
|
||||
if len(surfaces) != 2 {
|
||||
t.Errorf("surfaces: got %v want 2", surfaces)
|
||||
}
|
||||
// PYTHON: no test ref → focused
|
||||
lite2 := DesignLite("update the version footer", []string{"r1", "r2", "r3", "r4"})
|
||||
if lite2["verification"] != "focused" {
|
||||
t.Errorf("verification2: got %v want focused", lite2["verification"])
|
||||
}
|
||||
if !reflect.DeepEqual(lite2["route_reasons"].([]string), []string{"r1", "r2", "r3"}) {
|
||||
t.Errorf("route_reasons: got %v want first 3", lite2["route_reasons"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteInstructionParity(t *testing.T) {
|
||||
wantLocal := "ROUTE LOCAL_FIX: this is a local defect fix. You MUST use the write/edit tools to make the change, then run the relevant test or command to verify. A text-only answer does not complete this task."
|
||||
if got := RouteInstruction("LOCAL_FIX", "defect"); got != wantLocal {
|
||||
t.Errorf("LOCAL_FIX instruction mismatch:\n got %q\nwant %q", got, wantLocal)
|
||||
}
|
||||
if got := RouteInstruction("LOCAL_FIX", "neutral"); got != "" {
|
||||
t.Errorf("neutral language must get no mutation instruction, got %q", got)
|
||||
}
|
||||
if got := RouteInstruction("RESEARCHED_CHANGE", "defect"); got != "" {
|
||||
t.Errorf("RESEARCHED_CHANGE gets no fixed instruction, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteExecutionContextParity(t *testing.T) {
|
||||
ctx := RouteExecutionContext("LOCAL_FIX", "fix the crash", nil)
|
||||
if ctx["verification_policy"] != "focused" || ctx["research_allowed"] != false {
|
||||
t.Errorf("LOCAL_FIX ctx: got %v", ctx)
|
||||
}
|
||||
if _, has := ctx["design_lite"]; has {
|
||||
t.Error("LOCAL_FIX must not carry design_lite")
|
||||
}
|
||||
ctx2 := RouteExecutionContext("STANDARD_CHANGE", "add a progress bar to the export dialog", nil)
|
||||
if ctx2["verification_policy"] != "integration" {
|
||||
t.Errorf("STANDARD_CHANGE policy: got %v", ctx2["verification_policy"])
|
||||
}
|
||||
if _, has := ctx2["design_lite"]; !has {
|
||||
t.Error("STANDARD_CHANGE must carry design_lite")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskCountersParity(t *testing.T) {
|
||||
c := TaskCounters([]string{"created", "pending", "running", "running", "completed"})
|
||||
want := map[string]int{"tasks_pending": 2, "tasks_running": 2, "tasks_completed": 1}
|
||||
if !reflect.DeepEqual(c, want) {
|
||||
t.Errorf("counters: got %v want %v", c, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteTaskClassIntegration(t *testing.T) {
|
||||
// Integration with the adaptive-router binary (built in TestMain).
|
||||
decision := RouteTaskClass("fix the crash in the parser output handling", "T-1", "")
|
||||
if decision == nil {
|
||||
t.Fatal("routing degraded — expected a decision (binary built?)")
|
||||
}
|
||||
if decision["route"] != "LOCAL_FIX" {
|
||||
t.Errorf("route: got %v want LOCAL_FIX (PYTHON oracle)", decision["route"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/provider-registry
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,273 @@
|
||||
// provider-registry — Go provider registry (R2 W5b deep port).
|
||||
// PARITY port of src/rune/provider_registry.py: connect, disconnect,
|
||||
// list_providers, get_provider, list_models (Ollama + OpenAI + Anthropic).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// provider-registry connect <name> <url> [provider-type] [api-key]
|
||||
// provider-registry disconnect <name>
|
||||
// provider-registry list
|
||||
// provider-registry models <name>
|
||||
// provider-registry chat <provider-name> <model> <message>
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ModelProvider struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
ProviderType string `json:"provider_type"` // openai, ollama, anthropic
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type ProviderRegistry struct {
|
||||
mu sync.RWMutex
|
||||
Providers map[string]ModelProvider `json:"providers"`
|
||||
ConfigPath string `json:"config_path"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func configPath() string {
|
||||
home := os.Getenv("RUNE_MEMORY_DIR")
|
||||
if home == "" {
|
||||
home = os.Getenv("HOME") + "/.rune"
|
||||
}
|
||||
return home + "/provider_registry.json"
|
||||
}
|
||||
|
||||
func loadRegistry() *ProviderRegistry {
|
||||
reg := &ProviderRegistry{Providers: make(map[string]ModelProvider)}
|
||||
if data, err := os.ReadFile(configPath()); err == nil {
|
||||
json.Unmarshal(data, reg)
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
func saveRegistry(reg *ProviderRegistry) {
|
||||
// Marshal ONLY durable state — the embedded sync.RWMutex must never be
|
||||
// copied (go vet copies-lock) and is not persisted state.
|
||||
data, _ := json.MarshalIndent(struct {
|
||||
Providers map[string]ModelProvider `json:"providers"`
|
||||
ConfigPath string `json:"config_path"`
|
||||
}{Providers: reg.Providers, ConfigPath: reg.ConfigPath}, "", " ")
|
||||
os.WriteFile(configPath(), data, 0600)
|
||||
}
|
||||
|
||||
func (reg *ProviderRegistry) connect(name, url, providerType, apiKey string) ModelProvider {
|
||||
provider := ModelProvider{
|
||||
Name: name, URL: url, ProviderType: providerType,
|
||||
APIKey: apiKey, Active: true,
|
||||
}
|
||||
reg.mu.Lock()
|
||||
reg.Providers[name] = provider
|
||||
reg.mu.Unlock()
|
||||
saveRegistry(reg)
|
||||
return provider
|
||||
}
|
||||
|
||||
func (reg *ProviderRegistry) fetchModels(provider ModelProvider) ([]string, error) {
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
var models []string
|
||||
|
||||
switch provider.ProviderType {
|
||||
case "ollama":
|
||||
resp, err := client.Get(provider.URL + "/api/tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result struct {
|
||||
Models []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"models"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
for _, m := range result.Models {
|
||||
models = append(models, cleanModelName(m.Name))
|
||||
}
|
||||
case "openai", "anthropic":
|
||||
req, _ := http.NewRequest("GET", provider.URL+"/v1/models", nil)
|
||||
if provider.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.APIKey)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
for _, m := range result.Data {
|
||||
models = append(models, cleanModelName(m.ID))
|
||||
}
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func cleanModelName(name string) string {
|
||||
// Strip common prefixes
|
||||
name = strings.TrimPrefix(name, "models/")
|
||||
return name
|
||||
}
|
||||
|
||||
func (reg *ProviderRegistry) chat(providerName, model, message string) (string, error) {
|
||||
reg.mu.RLock()
|
||||
provider, ok := reg.Providers[providerName]
|
||||
reg.mu.RUnlock()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("provider %q not found", providerName)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 300 * time.Second}
|
||||
|
||||
var requestBody map[string]interface{}
|
||||
var url string
|
||||
|
||||
switch provider.ProviderType {
|
||||
case "ollama":
|
||||
url = provider.URL + "/api/chat"
|
||||
requestBody = map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": message}},
|
||||
"stream": false,
|
||||
}
|
||||
case "openai":
|
||||
url = provider.URL + "/v1/chat/completions"
|
||||
requestBody = map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": message}},
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported provider type: %s", provider.ProviderType)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewReader(data))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if provider.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.APIKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
// Parse response based on provider type
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(body, &result)
|
||||
if content, ok := result["content"].(string); ok {
|
||||
return content, nil // Ollama format
|
||||
}
|
||||
if choices, ok := result["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||
if msg, ok := choice["message"].(map[string]interface{}); ok {
|
||||
if content, ok := msg["content"].(string); ok {
|
||||
return content, nil // OpenAI format
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry <connect|disconnect|list|models|chat> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
reg := loadRegistry()
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
switch cmd {
|
||||
case "connect":
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry connect <name> <url> [type] [key]")
|
||||
os.Exit(1)
|
||||
}
|
||||
name, url := args[0], args[1]
|
||||
providerType := "ollama"
|
||||
apiKey := ""
|
||||
if len(args) > 2 {
|
||||
providerType = args[2]
|
||||
}
|
||||
if len(args) > 3 {
|
||||
apiKey = args[3]
|
||||
}
|
||||
provider := reg.connect(name, url, providerType, apiKey)
|
||||
out, _ := json.MarshalIndent(provider, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "disconnect":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry disconnect <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
reg.mu.Lock()
|
||||
delete(reg.Providers, args[0])
|
||||
reg.mu.Unlock()
|
||||
saveRegistry(reg)
|
||||
fmt.Println("disconnected:", args[0])
|
||||
|
||||
case "list":
|
||||
for _, p := range reg.Providers {
|
||||
out, _ := json.Marshal(p)
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
case "models":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry models <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
provider, ok := reg.Providers[args[0]]
|
||||
if !ok {
|
||||
fmt.Println("null")
|
||||
return
|
||||
}
|
||||
models, err := reg.fetchModels(provider)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
out, _ := json.MarshalIndent(models, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "chat":
|
||||
if len(args) < 3 {
|
||||
fmt.Fprintln(os.Stderr, "usage: provider-registry chat <provider> <model> <message>")
|
||||
os.Exit(1)
|
||||
}
|
||||
response, err := reg.chat(args[0], args[1], strings.Join(args[2:], " "))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
out, _ := json.Marshal(map[string]string{"response": response})
|
||||
fmt.Println(string(out))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
module rune/scaffold-membrane
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package scaffoldmembrane — the Go↔Rust membrane for the construction
|
||||
// pipeline (Survivor pattern). The deterministic phase machine is the Rust
|
||||
// construction_interface staticlib; Go code (the coverage gate, future
|
||||
// operator integration) calls it here. No dlopen, no env vars.
|
||||
package scaffoldmembrane
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: ${SRCDIR}/../../rust/construction_interface/target/release/libconstruction_interface.a -lpthread -ldl -lm
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
extern char* ci_transition(const char* arg, size_t arg_len);
|
||||
extern char* ci_validate(const char* arg, size_t arg_len);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func call(symbol string, arg []byte) []byte {
|
||||
cArg := C.CBytes(arg)
|
||||
defer C.free(cArg)
|
||||
var res *C.char
|
||||
switch symbol {
|
||||
case "ci_transition":
|
||||
res = C.ci_transition((*C.char)(cArg), C.size_t(len(arg)))
|
||||
case "ci_validate":
|
||||
res = C.ci_validate((*C.char)(cArg), C.size_t(len(arg)))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
defer C.free(unsafe.Pointer(res))
|
||||
return C.GoBytes(unsafe.Pointer(res), C.int(C.strlen(res)))
|
||||
}
|
||||
|
||||
// ValidateTransition — is (from, to) a legal scaffold phase edge?
|
||||
func ValidateTransition(fromPhase, toPhase string) (bool, string) {
|
||||
req, _ := json.Marshal(map[string]string{"from": fromPhase, "to": toPhase})
|
||||
var resp struct {
|
||||
Legal bool `json:"legal"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
if err := json.Unmarshal(call("ci_validate", req), &resp); err != nil {
|
||||
return false, "membrane error: " + err.Error()
|
||||
}
|
||||
return resp.Legal, resp.Detail
|
||||
}
|
||||
|
||||
// Transition — the deterministic transition verdict envelope.
|
||||
func Transition(fromPhase, toPhase, contractID string) map[string]any {
|
||||
req, _ := json.Marshal(map[string]string{
|
||||
"from_phase": fromPhase, "to_phase": toPhase, "contract_id": contractID,
|
||||
})
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(call("ci_transition", req), &out); err != nil {
|
||||
return map[string]any{"success": false, "errors": []string{"membrane error: " + err.Error()}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package scaffoldmembrane
|
||||
|
||||
// membrane_test.go — parity + live-probe. The 16-edge matrix is the Python
|
||||
// scaffold authority's map (verified 2026-10-07); the membrane must agree
|
||||
// on every edge, and the FFI must return real verdicts (no silent nil).
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTransitionMatrixParity(t *testing.T) {
|
||||
// [from, to, legal] — the Python scaffold authority's map
|
||||
matrix := [][3]interface{}{
|
||||
{"draft", "approved", true}, {"draft", "abandoned", true},
|
||||
{"draft", "building", false}, {"draft", "closed", false},
|
||||
{"approved", "building", true}, {"approved", "abandoned", true},
|
||||
{"approved", "verifying", false},
|
||||
{"building", "verifying", true}, {"building", "abandoned", true},
|
||||
{"building", "approved", false},
|
||||
{"verifying", "closed", true}, {"verifying", "abandoned", true},
|
||||
{"verifying", "building", false},
|
||||
{"closed", "building", false}, {"closed", "verifying", false},
|
||||
{"closed", "abandoned", false},
|
||||
}
|
||||
for _, edge := range matrix {
|
||||
from := edge[0].(string)
|
||||
to := edge[1].(string)
|
||||
want := edge[2].(bool)
|
||||
got, detail := ValidateTransition(from, to)
|
||||
if got != want {
|
||||
t.Errorf("%s→%s: got legal=%v (%s) want %v", from, to, got, detail, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMembraneLive(t *testing.T) {
|
||||
// Live-probe: the FFI must produce a real verdict envelope.
|
||||
out := Transition("building", "verifying", "BUILDER-PROBE")
|
||||
if out["success"] != true {
|
||||
t.Errorf("membrane silent or wrong: %v", out)
|
||||
}
|
||||
out2 := Transition("draft", "closed", "BUILDER-PROBE")
|
||||
if out2["success"] != false {
|
||||
t.Errorf("illegal edge accepted: %v", out2)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -34,16 +35,16 @@ func main() {
|
||||
route = "VERIFICATION"
|
||||
}
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"route": route,
|
||||
"route": route,
|
||||
"objective": strings.TrimSpace(text),
|
||||
"source": "go-scaffold",
|
||||
"source": "go-scaffold",
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "status":
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"runtime": "go",
|
||||
"survivor": true,
|
||||
"runtime": "go",
|
||||
"survivor": true,
|
||||
"rust_kernels": "linked via staticlib",
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
Binary file not shown.
@@ -2,9 +2,10 @@
|
||||
// Replaces src/rune/subsystem_health.py.
|
||||
//
|
||||
// Usage:
|
||||
// subsystem-health record <subsystem> <error> <tier>
|
||||
// subsystem-health snapshot
|
||||
// subsystem-health clear
|
||||
//
|
||||
// subsystem-health record <subsystem> <error> <tier>
|
||||
// subsystem-health snapshot
|
||||
// subsystem-health clear
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -24,15 +25,15 @@ type Failure struct {
|
||||
}
|
||||
|
||||
type HealthRegistry struct {
|
||||
mu sync.Mutex
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
mu sync.Mutex
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
}
|
||||
|
||||
type HealthStatus struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
TotalCount int `json:"total_count"`
|
||||
LastTier int `json:"last_tier"`
|
||||
LastError string `json:"last_error"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
TotalCount int `json:"total_count"`
|
||||
LastTier int `json:"last_tier"`
|
||||
LastError string `json:"last_error"`
|
||||
LastTimestamp string `json:"last_timestamp"`
|
||||
}
|
||||
|
||||
@@ -44,22 +45,23 @@ func healthPath() string {
|
||||
return filepath.Join(home, "subsystem_health.json")
|
||||
}
|
||||
|
||||
func loadRegistry() HealthRegistry {
|
||||
var hr HealthRegistry
|
||||
hr.Failures = make(map[string][]Failure)
|
||||
func loadRegistry() *HealthRegistry {
|
||||
hr := &HealthRegistry{Failures: make(map[string][]Failure)}
|
||||
data, err := os.ReadFile(healthPath())
|
||||
if err != nil {
|
||||
return hr
|
||||
}
|
||||
json.Unmarshal(data, &hr)
|
||||
json.Unmarshal(data, hr)
|
||||
return hr
|
||||
}
|
||||
|
||||
func saveRegistry(hr HealthRegistry) {
|
||||
hr.mu.Lock()
|
||||
defer hr.mu.Unlock()
|
||||
func saveRegistry(hr *HealthRegistry) {
|
||||
// Marshal ONLY durable state — the embedded sync.Mutex must never be
|
||||
// copied (go vet copies-lock) and is not persisted state.
|
||||
data, _ := json.MarshalIndent(struct {
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
}{Failures: hr.Failures}, "", " ")
|
||||
os.MkdirAll(filepath.Dir(healthPath()), 0755)
|
||||
data, _ := json.MarshalIndent(hr, "", " ")
|
||||
os.WriteFile(healthPath(), data, 0644)
|
||||
}
|
||||
|
||||
@@ -95,13 +97,15 @@ func main() {
|
||||
case "snapshot":
|
||||
statuses := []HealthStatus{}
|
||||
for subsystem, failures := range hr.Failures {
|
||||
if len(failures) == 0 { continue }
|
||||
if len(failures) == 0 {
|
||||
continue
|
||||
}
|
||||
last := failures[len(failures)-1]
|
||||
statuses = append(statuses, HealthStatus{
|
||||
Subsystem: subsystem,
|
||||
TotalCount: len(failures),
|
||||
LastTier: last.Tier,
|
||||
LastError: last.Error,
|
||||
Subsystem: subsystem,
|
||||
TotalCount: len(failures),
|
||||
LastTier: last.Tier,
|
||||
LastError: last.Error,
|
||||
LastTimestamp: last.Timestamp.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
package main
|
||||
|
||||
// fileops.go — batch 2: the file operations. PARITY ports of _read_file
|
||||
// (windowed viewing, 1-indexed offset, SWE-agent numbering), _write_file
|
||||
// (diff-for-confirmation, no direct write), generate_diff (unified diff),
|
||||
// _apply_content (atomic write with backup), and the undo backup.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReadFile — windowed viewing: offset (1-indexed), limit (default 100),
|
||||
// numbered lines, truncation context on both ends.
|
||||
func ReadFile(path string, offset, limit int) (bool, string) {
|
||||
ok, msg, resolved := ValidateReadPath(path)
|
||||
if !ok || resolved == "" {
|
||||
return false, msg
|
||||
}
|
||||
if offset < 1 {
|
||||
offset = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
content, err := os.ReadFile(resolved)
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Read failed: %v", err)
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
start := offset - 1
|
||||
end := len(lines)
|
||||
if start+limit < end {
|
||||
end = start + limit
|
||||
}
|
||||
if start > len(lines) {
|
||||
start = len(lines)
|
||||
}
|
||||
windowed := lines[start:end]
|
||||
numbered := make([]string, len(windowed))
|
||||
for i, line := range windowed {
|
||||
numbered[i] = fmt.Sprintf("%d: %s", i+offset, line)
|
||||
}
|
||||
result := strings.Join(numbered, "\n")
|
||||
if end < len(lines) {
|
||||
result += fmt.Sprintf("\n\n[showing lines %d-%d of %d total]", offset, end, len(lines))
|
||||
}
|
||||
if start > 0 {
|
||||
result = fmt.Sprintf("[showing from line %d]\n", offset) + result
|
||||
}
|
||||
return true, result
|
||||
}
|
||||
|
||||
// GenerateDiff — unified diff between the current file and new content.
|
||||
// Format parity: a/<name> / b/<name> headers.
|
||||
func GenerateDiff(path, newContent string) string {
|
||||
var oldLines []string
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
oldLines = splitKeepEnds(string(data))
|
||||
}
|
||||
newLines := splitKeepEnds(newContent)
|
||||
return unifiedDiff(oldLines, newLines, "a/"+filepath.Base(path), "b/"+filepath.Base(path))
|
||||
}
|
||||
|
||||
func splitKeepEnds(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.SplitAfter(s, "\n")
|
||||
if parts[len(parts)-1] == "" {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// unifiedDiff — a compact hunk-based unified diff (the difflib shape:
|
||||
// @@ header, -old, +new, with 3 lines of context).
|
||||
func unifiedDiff(a, b []string, fromFile, toFile string) string {
|
||||
var out strings.Builder
|
||||
if equalSlices(a, b) {
|
||||
return ""
|
||||
}
|
||||
out.WriteString(fmt.Sprintf("--- %s\n+++ %s\n", fromFile, toFile))
|
||||
// Simple LCS-based hunks (greedy prefix/suffix trim then middle diff).
|
||||
// Trim common prefix.
|
||||
i := 0
|
||||
for i < len(a) && i < len(b) && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
// Trim common suffix.
|
||||
j := 0
|
||||
for j < len(a)-i && j < len(b)-i && a[len(a)-1-j] == b[len(b)-1-j] {
|
||||
j++
|
||||
}
|
||||
aMid, bMid := a[i:len(a)-j], b[i:len(b)-j]
|
||||
ctx := 3
|
||||
start := i - ctx
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
endA := len(a) - j + ctx
|
||||
if endA > len(a) {
|
||||
endA = len(a)
|
||||
}
|
||||
ctxBefore := i - start
|
||||
ctxAfter := endA - (len(a) - j)
|
||||
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", start+1, ctxBefore+len(aMid)+ctxAfter, start+1, ctxBefore+len(bMid)+ctxAfter)
|
||||
for k := start; k < i; k++ {
|
||||
out.WriteString(" " + ensureNL(a[k]))
|
||||
}
|
||||
for _, l := range aMid {
|
||||
out.WriteString("-" + ensureNL(l))
|
||||
}
|
||||
for _, l := range bMid {
|
||||
out.WriteString("+" + ensureNL(l))
|
||||
}
|
||||
for k := len(a) - j; k < endA; k++ {
|
||||
out.WriteString(" " + ensureNL(a[k]))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func ensureNL(s string) string {
|
||||
if strings.HasSuffix(s, "\n") {
|
||||
return s
|
||||
}
|
||||
return s + "\n"
|
||||
}
|
||||
|
||||
func equalSlices(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WriteFile — diff-for-confirmation preview (NO write happens).
|
||||
// PARITY: "No changes" verdict when content already matches.
|
||||
func WriteFile(path, content string) map[string]interface{} {
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if !ok || resolved == "" {
|
||||
return map[string]interface{}{"success": false, "error": msg}
|
||||
}
|
||||
diff := GenerateDiff(resolved, content)
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
return map[string]interface{}{"success": true, "result": "No changes — file already has this content"}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"success": true, "needs_confirm": true, "diff": diff,
|
||||
"path": resolved, "content": content,
|
||||
"result": fmt.Sprintf("Diff generated for %s. Awaiting confirmation.", filepath.Base(resolved)),
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyContent — atomic write of APPROVED content, preserving undo.
|
||||
func ApplyContent(path, content string) map[string]interface{} {
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if !ok || resolved == "" {
|
||||
return map[string]interface{}{"success": false, "error": msg}
|
||||
}
|
||||
BackupFile(resolved)
|
||||
if err := os.MkdirAll(filepath.Dir(resolved), 0o755); err != nil {
|
||||
return map[string]interface{}{"success": false, "error": fmt.Sprintf("Write failed: %v", err)}
|
||||
}
|
||||
tmp := resolved + ".tmp"
|
||||
if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
|
||||
return map[string]interface{}{"success": false, "error": fmt.Sprintf("Write failed: %v", err)}
|
||||
}
|
||||
if err := os.Rename(tmp, resolved); err != nil {
|
||||
return map[string]interface{}{"success": false, "error": fmt.Sprintf("Write failed: %v", err)}
|
||||
}
|
||||
return map[string]interface{}{"success": true, "result": fmt.Sprintf("Wrote %d chars to %s", len(content), filepath.Base(resolved))}
|
||||
}
|
||||
|
||||
// BackupFile — copy to the undo dir with a timestamped name. Returns the
|
||||
// backup path or "" (parity with _backup_file's silent degrade).
|
||||
func BackupFile(path string) string {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return ""
|
||||
}
|
||||
undoDir := UndoDir()
|
||||
if err := os.MkdirAll(undoDir, 0o755); err != nil {
|
||||
return ""
|
||||
}
|
||||
ts := time.Now().Format("20060102_150405")
|
||||
safe := strings.ReplaceAll(strings.ReplaceAll(path, "/", "_"), ":", "_")
|
||||
backup := filepath.Join(undoDir, fmt.Sprintf("%s_%s.bak", ts, safe))
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if err := os.WriteFile(backup, data, 0o644); err != nil {
|
||||
return ""
|
||||
}
|
||||
return backup
|
||||
}
|
||||
|
||||
// UndoDir — PARITY with UNDO_DIR (~/.rune/undo).
|
||||
func UndoDir() string {
|
||||
return filepath.Join(expandHome("~"), ".rune", "undo")
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
// gitops.go — batch 4: the git operations and quality-gate tools. PARITY
|
||||
// ports of _git_diff, _git_log, _git_commit, _lint_file, _run_tests,
|
||||
// _format_code. Commands run with a 10s timeout (parity) in the caller's
|
||||
// cwd; workdir/project resolution lands with the registry membrane.
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runTimeout — parity with the Python subprocess timeout=10.
|
||||
const runTimeoutSeconds = 10
|
||||
|
||||
func runGit(args ...string) (bool, string) {
|
||||
cmd := exec.Command("git", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false, string(out)
|
||||
}
|
||||
return true, string(out)
|
||||
}
|
||||
|
||||
// GitDiff — staged flag + optional path; "(no changes)" on empty output.
|
||||
func GitDiff(staged bool, path string) (bool, string) {
|
||||
cmd := []string{"diff"}
|
||||
if staged {
|
||||
cmd = append(cmd, "--staged")
|
||||
}
|
||||
if path != "" {
|
||||
cmd = append(cmd, path)
|
||||
}
|
||||
ok, out := runGit(cmd...)
|
||||
if !ok {
|
||||
return false, "Git diff failed: " + out
|
||||
}
|
||||
out = strings.TrimRight(out, "\n")
|
||||
if out == "" {
|
||||
return true, "(no changes)"
|
||||
}
|
||||
return true, out
|
||||
}
|
||||
|
||||
// GitLog — oneline log with limit and optional path filter.
|
||||
func GitLog(limit int, path string) (bool, string) {
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
cmd := []string{"log", "--oneline", itoa2(-limit)}
|
||||
if path != "" {
|
||||
cmd = append(cmd, "--", path)
|
||||
}
|
||||
ok, out := runGit(cmd...)
|
||||
if !ok {
|
||||
return false, "Git log failed: " + out
|
||||
}
|
||||
out = strings.TrimRight(out, "\n")
|
||||
if out == "" {
|
||||
return true, "(no commits)"
|
||||
}
|
||||
return true, out
|
||||
}
|
||||
|
||||
// GitCommit — stage files (or -A) and commit; message required.
|
||||
func GitCommit(message string, files []string) (bool, string) {
|
||||
if message == "" {
|
||||
return false, "Commit message is required"
|
||||
}
|
||||
if len(files) > 0 {
|
||||
for _, f := range files {
|
||||
runGit("add", f)
|
||||
}
|
||||
} else {
|
||||
runGit("add", "-A")
|
||||
}
|
||||
cmd := exec.Command("git", "commit", "-m", message)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
msg := string(out)
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
return false, "Commit failed"
|
||||
}
|
||||
return false, msg
|
||||
}
|
||||
return true, string(out)
|
||||
}
|
||||
|
||||
func itoa2(i int) string {
|
||||
if i == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := i < 0
|
||||
if neg {
|
||||
i = -i
|
||||
}
|
||||
var b []byte
|
||||
for i > 0 {
|
||||
b = append([]byte{byte('0' + i%10)}, b...)
|
||||
i /= 10
|
||||
}
|
||||
if neg {
|
||||
return "-" + string(b)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
// json_order.go — JSON order-preserving helpers for the tool registry.
|
||||
// REPAIR (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown): these lived
|
||||
// in truth_prompt_test.go, so the non-test build of prompt.go failed with
|
||||
// `undefined: jsonUnmarshal` / `undefined: paramOrder` on CI (go-surface,
|
||||
// run 19792 job 56712). Helpers belong in real source; tests import them
|
||||
// like any other package symbol.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// jsonUnmarshal — single decode seam (kept as a named wrapper so parity
|
||||
// annotations stay greppable; behavior = encoding/json.Unmarshal).
|
||||
func jsonUnmarshal(b []byte, v interface{}) error {
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
// paramOrder — extract the property-key order from the raw registry JSON
|
||||
// (Go maps lose insertion order; the prompt parity requires it).
|
||||
func paramOrder(toolName string) []string {
|
||||
var raw []struct {
|
||||
Name string `json:"name"`
|
||||
Parameters struct {
|
||||
Properties json.RawMessage `json:"properties"`
|
||||
} `json:"parameters"`
|
||||
}
|
||||
if err := json.Unmarshal(toolsRegistryJSON, &raw); err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, t := range raw {
|
||||
if t.Name != toolName {
|
||||
continue
|
||||
}
|
||||
var props map[string]json.RawMessage
|
||||
if err := json.Unmarshal(t.Parameters.Properties, &props); err != nil {
|
||||
return nil
|
||||
}
|
||||
// Re-decode preserving order: scan the raw object text.
|
||||
return rawKeyOrder(t.Parameters.Properties)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rawKeyOrder — scan a raw JSON object's top-level keys in document order.
|
||||
func rawKeyOrder(raw json.RawMessage) []string {
|
||||
s := string(raw)
|
||||
var keys []string
|
||||
depth := 0
|
||||
inString := false
|
||||
escaped := false
|
||||
keyStart := -1
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if inString {
|
||||
if escaped {
|
||||
escaped = false
|
||||
} else if ch == '\\' {
|
||||
escaped = true
|
||||
} else if ch == '"' {
|
||||
inString = false
|
||||
if depth == 1 && keyStart >= 0 {
|
||||
// decode the key (strip quotes, unescape minimally)
|
||||
keys = append(keys, s[keyStart+1:i])
|
||||
keyStart = -1
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '"':
|
||||
if depth == 1 && keyStart < 0 {
|
||||
keyStart = i
|
||||
}
|
||||
inString = true
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
+128
-46
@@ -3,11 +3,12 @@
|
||||
// src/rune/tool_executor.py. Approval machinery stays Python-side.
|
||||
//
|
||||
// Usage:
|
||||
// tool-executor read_file <path> [offset] [limit]
|
||||
// tool-executor list_directory <path> [pattern]
|
||||
// tool-executor glob_files <pattern> [path]
|
||||
// tool-executor grep_files <pattern> [path] [include]
|
||||
// tool-executor run_command <command> [workdir]
|
||||
//
|
||||
// tool-executor read_file <path> [offset] [limit]
|
||||
// tool-executor list_directory <path> [pattern]
|
||||
// tool-executor glob_files <pattern> [path]
|
||||
// tool-executor grep_files <pattern> [path] [include]
|
||||
// tool-executor run_command <command> [workdir]
|
||||
//
|
||||
// Output: JSON envelope {"success": bool, "result": "...", "error": "..."}
|
||||
package main
|
||||
@@ -29,8 +30,8 @@ type Result struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func ok(s string) Result { return Result{Success: true, Result: s} }
|
||||
func fail(s string) Result { return Result{Success: false, Error: s} }
|
||||
func ok(s string) Result { return Result{Success: true, Result: s} }
|
||||
func fail(s string) Result { return Result{Success: false, Error: s} }
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
@@ -41,25 +42,39 @@ func main() {
|
||||
switch os.Args[1] {
|
||||
case "read_file":
|
||||
offset, limit := 1, 100
|
||||
if len(os.Args) > 3 { fmt.Sscanf(os.Args[3], "%d", &offset) }
|
||||
if len(os.Args) > 4 { fmt.Sscanf(os.Args[4], "%d", &limit) }
|
||||
if len(os.Args) > 3 {
|
||||
fmt.Sscanf(os.Args[3], "%d", &offset)
|
||||
}
|
||||
if len(os.Args) > 4 {
|
||||
fmt.Sscanf(os.Args[4], "%d", &limit)
|
||||
}
|
||||
r = readFile(os.Args[2], offset, limit)
|
||||
case "list_directory":
|
||||
pattern := ""
|
||||
if len(os.Args) > 3 { pattern = os.Args[3] }
|
||||
if len(os.Args) > 3 {
|
||||
pattern = os.Args[3]
|
||||
}
|
||||
r = listDirectory(os.Args[2], pattern)
|
||||
case "glob_files":
|
||||
path := "."
|
||||
if len(os.Args) > 3 { path = os.Args[3] }
|
||||
if len(os.Args) > 3 {
|
||||
path = os.Args[3]
|
||||
}
|
||||
r = globFiles(os.Args[2], path)
|
||||
case "grep_files":
|
||||
path, include := ".", ""
|
||||
if len(os.Args) > 3 { path = os.Args[3] }
|
||||
if len(os.Args) > 4 { include = os.Args[4] }
|
||||
if len(os.Args) > 3 {
|
||||
path = os.Args[3]
|
||||
}
|
||||
if len(os.Args) > 4 {
|
||||
include = os.Args[4]
|
||||
}
|
||||
r = grepFiles(os.Args[2], path, include)
|
||||
case "run_command":
|
||||
workdir := ""
|
||||
if len(os.Args) > 3 { workdir = os.Args[3] }
|
||||
if len(os.Args) > 3 {
|
||||
workdir = os.Args[3]
|
||||
}
|
||||
r = runCommand(os.Args[2], workdir)
|
||||
default:
|
||||
r = fail("unknown operation: " + os.Args[1])
|
||||
@@ -70,17 +85,27 @@ func main() {
|
||||
|
||||
func readFile(path string, offset, limit int) Result {
|
||||
f, err := os.Open(path)
|
||||
if err != nil { return fail(fmt.Sprintf("Cannot read %s: %v", path, err)) }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Cannot read %s: %v", path, err))
|
||||
}
|
||||
defer f.Close()
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
for scanner.Scan() { lines = append(lines, scanner.Text()) }
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
start := offset - 1
|
||||
if start < 0 { start = 0 }
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := len(lines)
|
||||
if start+limit < end { end = start + limit }
|
||||
if start >= len(lines) { return ok("(empty range)") }
|
||||
if start+limit < end {
|
||||
end = start + limit
|
||||
}
|
||||
if start >= len(lines) {
|
||||
return ok("(empty range)")
|
||||
}
|
||||
var numbered []string
|
||||
for i := start; i < end; i++ {
|
||||
numbered = append(numbered, fmt.Sprintf("%d: %s", i+1, lines[i]))
|
||||
@@ -90,18 +115,32 @@ func readFile(path string, offset, limit int) Result {
|
||||
|
||||
func listDirectory(path, pattern string) Result {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil { return fail(fmt.Sprintf("Cannot list %s: %v", path, err)) }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Cannot list %s: %v", path, err))
|
||||
}
|
||||
var re *regexp.Regexp
|
||||
if pattern != "" { re, _ = regexp.Compile(fnmatchToRegex(pattern)) }
|
||||
if pattern != "" {
|
||||
re, _ = regexp.Compile(fnmatchToRegex(pattern))
|
||||
}
|
||||
var out []string
|
||||
sorted := make([]os.DirEntry, 0, len(entries))
|
||||
for _, e := range entries { sorted = append(sorted, e) }
|
||||
for _, e := range entries {
|
||||
sorted = append(sorted, e)
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name() < sorted[j].Name() })
|
||||
for _, e := range sorted {
|
||||
if re != nil && !re.MatchString(e.Name()) { continue }
|
||||
if e.IsDir() { out = append(out, "📁 "+e.Name()+"/") } else { out = append(out, "📄 "+e.Name()) }
|
||||
if re != nil && !re.MatchString(e.Name()) {
|
||||
continue
|
||||
}
|
||||
if e.IsDir() {
|
||||
out = append(out, "📁 "+e.Name()+"/")
|
||||
} else {
|
||||
out = append(out, "📄 "+e.Name())
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return ok("(empty)")
|
||||
}
|
||||
if len(out) == 0 { return ok("(empty)") }
|
||||
return ok(strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
@@ -110,10 +149,14 @@ func fnmatchToRegex(pattern string) string {
|
||||
b.WriteString("^")
|
||||
for _, c := range pattern {
|
||||
switch c {
|
||||
case '*': b.WriteString("[^/]*")
|
||||
case '?': b.WriteString(".")
|
||||
case '.': b.WriteString("\\.")
|
||||
default: b.WriteString(regexp.QuoteMeta(string(c)))
|
||||
case '*':
|
||||
b.WriteString("[^/]*")
|
||||
case '?':
|
||||
b.WriteString(".")
|
||||
case '.':
|
||||
b.WriteString("\\.")
|
||||
default:
|
||||
b.WriteString(regexp.QuoteMeta(string(c)))
|
||||
}
|
||||
}
|
||||
b.WriteString("$")
|
||||
@@ -123,43 +166,69 @@ func fnmatchToRegex(pattern string) string {
|
||||
func globFiles(pattern, path string) Result {
|
||||
full := filepath.Join(path, pattern)
|
||||
matches, err := filepath.Glob(full)
|
||||
if err != nil { return fail(fmt.Sprintf("Glob failed: %v", err)) }
|
||||
if len(matches) == 0 { return ok("(no matches)") }
|
||||
type mi struct { path string; mod int64 }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Glob failed: %v", err))
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return ok("(no matches)")
|
||||
}
|
||||
type mi struct {
|
||||
path string
|
||||
mod int64
|
||||
}
|
||||
var ms []mi
|
||||
for _, m := range matches {
|
||||
fi, err := os.Stat(m)
|
||||
if err != nil { continue }
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ms = append(ms, mi{m, fi.ModTime().UnixNano()})
|
||||
}
|
||||
sort.Slice(ms, func(i, j int) bool { return ms[i].mod > ms[j].mod })
|
||||
if len(ms) > 100 { ms = ms[:100] }
|
||||
if len(ms) > 100 {
|
||||
ms = ms[:100]
|
||||
}
|
||||
var out []string
|
||||
for _, m := range ms {
|
||||
rel, _ := filepath.Rel(path, m.path)
|
||||
fi, _ := os.Stat(m.path)
|
||||
if fi.IsDir() { out = append(out, rel+"/") } else { out = append(out, rel) }
|
||||
if fi.IsDir() {
|
||||
out = append(out, rel+"/")
|
||||
} else {
|
||||
out = append(out, rel)
|
||||
}
|
||||
}
|
||||
return ok(strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
func grepFiles(pattern, path, include string) Result {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil { return fail(fmt.Sprintf("Bad pattern: %v", err)) }
|
||||
if err != nil {
|
||||
return fail(fmt.Sprintf("Bad pattern: %v", err))
|
||||
}
|
||||
var out []string
|
||||
var walkFn func(string)
|
||||
walkFn = func(dir string) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil { return }
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
full := filepath.Join(dir, e.Name())
|
||||
if e.IsDir() { walkFn(full); continue }
|
||||
if e.IsDir() {
|
||||
walkFn(full)
|
||||
continue
|
||||
}
|
||||
if include != "" {
|
||||
re2, _ := regexp.Compile(fnmatchToRegex(include))
|
||||
if !re2.MatchString(e.Name()) { continue }
|
||||
if !re2.MatchString(e.Name()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
f, err := os.Open(full)
|
||||
if err != nil { continue }
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
lineNum := 0
|
||||
@@ -167,26 +236,39 @@ func grepFiles(pattern, path, include string) Result {
|
||||
lineNum++
|
||||
if re.MatchString(scanner.Text()) {
|
||||
out = append(out, fmt.Sprintf("%s:%d", full, lineNum))
|
||||
if len(out) >= 50 { f.Close(); return }
|
||||
if len(out) >= 50 {
|
||||
f.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
walkFn(path)
|
||||
if len(out) == 0 { return ok("(no matches)") }
|
||||
if len(out) == 0 {
|
||||
return ok("(no matches)")
|
||||
}
|
||||
return ok(strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
func runCommand(command, workdir string) Result {
|
||||
if workdir == "" { workdir = "." }
|
||||
if workdir == "" {
|
||||
workdir = "."
|
||||
}
|
||||
parts := strings.Fields(command)
|
||||
if len(parts) == 0 { return fail("Empty command") }
|
||||
if len(parts) == 0 {
|
||||
return fail("Empty command")
|
||||
}
|
||||
r := NewRunner(parts[0], parts[1:]...)
|
||||
r.Dir = workdir
|
||||
output, err := r.CombinedOutput()
|
||||
result := string(output)
|
||||
if err != nil { result += fmt.Sprintf("\n[exit code: %d]", r.ProcessState.ExitCode()) }
|
||||
if len(result) > 10000 { result = result[:10000] + "\n[truncated]" }
|
||||
if err != nil {
|
||||
result += fmt.Sprintf("\n[exit code: %d]", r.ProcessState.ExitCode())
|
||||
}
|
||||
if len(result) > 10000 {
|
||||
result = result[:10000] + "\n[truncated]"
|
||||
}
|
||||
return Result{Success: true, Result: result}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
// ops.go — batch 3: list_directory, grep_files (search), run_command's
|
||||
// argv/shell gate. PARITY ports of _list_directory (emoji prefixes, sorted,
|
||||
// pattern filter, "(empty)"), _search_files (recursive, case-insensitive
|
||||
// substring, 50-match truncate marker, rel:line: text format), and the
|
||||
// command gate (shell-syntax refusal without explicit approval).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ListDirectory — sorted entries with 📁/📄 prefixes, trailing / on dirs,
|
||||
// optional fnmatch pattern, "(empty)" when nothing matches.
|
||||
func ListDirectory(path, pattern string) (bool, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if !IsPathAllowed(resolved, AllowedReadDirs) {
|
||||
return false, fmt.Sprintf("Access denied: %s", path)
|
||||
}
|
||||
info, err := os.Stat(resolved)
|
||||
if err != nil || !info.IsDir() {
|
||||
return false, fmt.Sprintf("Not a directory: %s", path)
|
||||
}
|
||||
entries, err := os.ReadDir(resolved)
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("List failed: %v", err)
|
||||
}
|
||||
var out []string
|
||||
for _, item := range entries {
|
||||
if pattern != "" && !fnmatch(pattern, item.Name()) {
|
||||
continue
|
||||
}
|
||||
if item.IsDir() {
|
||||
out = append(out, "📁 "+item.Name()+"/")
|
||||
} else {
|
||||
out = append(out, "📄 "+item.Name())
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return true, "(empty)"
|
||||
}
|
||||
return true, strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
// fnmatch — Go translation of Python fnmatch (case-SENSITIVE on posix,
|
||||
// per the oracle's behavior on this platform).
|
||||
func fnmatch(pattern, name string) bool {
|
||||
ok, _ := filepath.Match(pattern, name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GrepFiles — recursive, case-insensitive substring match, include filter,
|
||||
// rel:line: text format, hard 50-match truncate with the marker string.
|
||||
func GrepFiles(path, pattern, include string) (bool, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if !IsPathAllowed(resolved, AllowedReadDirs) {
|
||||
return false, fmt.Sprintf("Access denied: %s", path)
|
||||
}
|
||||
lowerPattern := strings.ToLower(pattern)
|
||||
var matches []string
|
||||
truncated := false
|
||||
var walk func(dir string)
|
||||
walk = func(dir string) {
|
||||
if truncated {
|
||||
return
|
||||
}
|
||||
dh, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// os.ReadDir SORTS; Python os.listdir returns raw OS order. PARITY
|
||||
// requires the raw order - use Readdirnames(-1).
|
||||
names, err := dh.Readdirnames(-1)
|
||||
dh.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
type entry struct {
|
||||
name string
|
||||
dir bool
|
||||
}
|
||||
var entries []entry
|
||||
for _, name := range names {
|
||||
info, ierr := os.Stat(filepath.Join(dir, name))
|
||||
if ierr != nil {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entry{name, info.IsDir()})
|
||||
}
|
||||
// Python os.walk: files first in listdir order, subdirs walked after.
|
||||
var subdirs []string
|
||||
for _, item := range entries {
|
||||
fpath := filepath.Join(dir, item.name)
|
||||
if item.dir {
|
||||
subdirs = append(subdirs, fpath)
|
||||
continue
|
||||
}
|
||||
if include != "" && !fnmatch(include, item.name) {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(fpath)
|
||||
if err != nil {
|
||||
continue // parity: unreadable files skipped silently
|
||||
}
|
||||
rel, _ := filepath.Rel(resolved, fpath)
|
||||
for i, line := range strings.Split(string(data), "\n") {
|
||||
if strings.Contains(strings.ToLower(line), lowerPattern) {
|
||||
text := strings.TrimRight(line, "\r")
|
||||
if len(text) > 100 {
|
||||
text = text[:100]
|
||||
}
|
||||
matches = append(matches, fmt.Sprintf("%s:%d: %s", rel, i+1, text))
|
||||
if len(matches) >= 50 {
|
||||
truncated = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, sd := range subdirs {
|
||||
walk(sd)
|
||||
}
|
||||
}
|
||||
walk(resolved)
|
||||
if truncated {
|
||||
return true, strings.Join(matches, "\n") + "\n[truncated — 50 matches]"
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return true, "(no matches)"
|
||||
}
|
||||
return true, strings.Join(matches, "\n")
|
||||
}
|
||||
|
||||
// shellTokens — the refusal set (parity: any of these in a non-shell
|
||||
// command refuses with the exact message).
|
||||
var shellTokens = []string{"|", ">", "<", ";", "&&", "||", "`", "$("}
|
||||
|
||||
// CommandGate — the argv/shell decision. Returns (argv, useShell, error).
|
||||
// PARITY: non-shell commands containing shell syntax are REFUSED with
|
||||
// "Shell syntax requires explicit shell approval"; shell mode demands an
|
||||
// explicit command string.
|
||||
func CommandGate(command string, argv []string, shell bool) ([]string, bool, string) {
|
||||
if shell != true {
|
||||
if argv == nil {
|
||||
if strings.TrimSpace(command) == "" {
|
||||
return nil, false, "Command or argv is required"
|
||||
}
|
||||
for _, tok := range shellTokens {
|
||||
if strings.Contains(command, tok) {
|
||||
return nil, false, "Shell syntax requires explicit shell approval"
|
||||
}
|
||||
}
|
||||
argv = strings.Fields(command)
|
||||
}
|
||||
if len(argv) == 0 {
|
||||
return nil, false, "argv must be a non-empty string list"
|
||||
}
|
||||
for _, a := range argv {
|
||||
if a == "" {
|
||||
return nil, false, "argv must be a non-empty string list"
|
||||
}
|
||||
}
|
||||
return argv, false, ""
|
||||
}
|
||||
// shell == true
|
||||
if strings.TrimSpace(command) == "" {
|
||||
return nil, false, "Explicit shell command is required"
|
||||
}
|
||||
return nil, true, ""
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
// parse.go — batch 5: the compatibility protocol parser. PARITY ports of
|
||||
// tool_protocol._protocol_entries (JSON objects with {"tool", "args"},
|
||||
// ```json fences, <tool_call> XML wrappers, inline text) and
|
||||
// tool_executor.parse_tool_call / format_tool_result.
|
||||
//
|
||||
// PARITY-CRITICAL: the envelope key is "tool" (not "name") and the
|
||||
// arguments key is "args" (not "arguments"). Unknown tools still parse —
|
||||
// validation lives at dispatch (validate_tool_request), not here.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var fenceRE = regexp.MustCompile("(?is)```(?:json)?\\s*(.*?)```")
|
||||
var xmlRE = regexp.MustCompile("(?is)<tool_call>(.*?)</tool_call>")
|
||||
|
||||
// jsonObjects — scan text for balanced top-level JSON objects (raw scan;
|
||||
// strings are skipped so braces inside strings don't confuse the depth
|
||||
// count). PARITY with _decoded_objects' net effect on well-formed input.
|
||||
func jsonObjects(text string) []json.RawMessage {
|
||||
var out []json.RawMessage
|
||||
depth := 0
|
||||
start := -1
|
||||
inString := false
|
||||
escaped := false
|
||||
for i := 0; i < len(text); i++ {
|
||||
ch := text[i]
|
||||
if inString {
|
||||
if escaped {
|
||||
escaped = false
|
||||
} else if ch == '\\' {
|
||||
escaped = true
|
||||
} else if ch == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '"':
|
||||
inString = true
|
||||
case '{':
|
||||
if depth == 0 {
|
||||
start = i
|
||||
}
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == 0 && start >= 0 {
|
||||
out = append(out, json.RawMessage(text[start:i+1]))
|
||||
start = -1
|
||||
}
|
||||
if depth < 0 {
|
||||
depth = 0
|
||||
start = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParsedCall — the legacy shape parse_tool_call returns.
|
||||
type ParsedCall struct {
|
||||
Tool string `json:"tool"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
}
|
||||
|
||||
// requestFromEnvelope — {"tool": str, "args": object} → ParsedCall.
|
||||
// Returns nil for non-envelope objects (no "tool" key).
|
||||
func requestFromEnvelope(raw json.RawMessage) *ParsedCall {
|
||||
var env map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &env); err != nil {
|
||||
return nil
|
||||
}
|
||||
toolVal, has := env["tool"]
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
tool, ok := toolVal.(string)
|
||||
if !ok {
|
||||
return nil // "tool name must be a string"
|
||||
}
|
||||
args := map[string]interface{}{}
|
||||
if a, has := env["args"]; has {
|
||||
am, ok := a.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil // "tool arguments must be an object"
|
||||
}
|
||||
args = am
|
||||
}
|
||||
return &ParsedCall{Tool: tool, Args: args}
|
||||
}
|
||||
|
||||
// NormalizeTextToolCalls — the ordered parser: bare JSON, ```json fences,
|
||||
// and <tool_call> wrappers all yield envelope objects; the first valid one
|
||||
// wins (parse_tool_call takes requests[0]).
|
||||
func NormalizeTextToolCalls(text string) []*ParsedCall {
|
||||
var out []*ParsedCall
|
||||
for _, raw := range jsonObjects(text) {
|
||||
if p := requestFromEnvelope(raw); p != nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
// Fenced and XML wrappers: their content is JSON too — the raw scan
|
||||
// above already catches them since the braces survive wrapping. The
|
||||
// wrapper detection matters only for span extraction (visible-text
|
||||
// removal), which is the caller's concern.
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseToolCall — first validated call in the legacy shape, or nil.
|
||||
func ParseToolCall(response string) *ParsedCall {
|
||||
requests := NormalizeTextToolCalls(response)
|
||||
if len(requests) == 0 {
|
||||
return nil
|
||||
}
|
||||
return requests[0]
|
||||
}
|
||||
|
||||
// FormatToolResult — the model-facing envelope. PARITY: exact prefixes.
|
||||
func FormatToolResult(result map[string]interface{}) string {
|
||||
if ok, _ := result["success"].(bool); ok {
|
||||
content, _ := result["result"].(string)
|
||||
return fmt.Sprintf("[Tool result]\n%s", content)
|
||||
}
|
||||
errMsg, _ := result["error"].(string)
|
||||
if errMsg == "" {
|
||||
errMsg = "Unknown error"
|
||||
}
|
||||
return fmt.Sprintf("[Tool error]\n%s", errMsg)
|
||||
}
|
||||
|
||||
var _ = strings.TrimSpace // reserved for the visible-text port
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
// paths.go — the tool-executor's security core. PARITY ports of
|
||||
// tool_executor.py's resolve_path, is_path_allowed, validate_read_path,
|
||||
// validate_write_path — including the TRUTHFUL-REASON semantics (a blocked
|
||||
// dir under an allowed dir reports the blocklist, not "outside allowed";
|
||||
// a write denial names the allowed dirs so the model can self-correct).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Security configuration — PARITY with tool_executor.py:44-65.
|
||||
var AllowedReadDirs = []string{
|
||||
expandHome("~"),
|
||||
"/mnt/workdrive",
|
||||
"/tmp",
|
||||
}
|
||||
|
||||
var AllowedWriteDirs = []string{
|
||||
expandHome("~"),
|
||||
"/mnt/workdrive",
|
||||
}
|
||||
|
||||
var BlockedDirs = []string{
|
||||
expandHome("~/.ssh"),
|
||||
expandHome("~/.gnupg"),
|
||||
"/etc",
|
||||
"/var",
|
||||
"/proc",
|
||||
"/sys",
|
||||
}
|
||||
|
||||
func expandHome(p string) string {
|
||||
if strings.HasPrefix(p, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
return filepath.Join(home, p[1:])
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ResolvePath — absolute, symlinks and .. resolved.
|
||||
func ResolvePath(path string) string {
|
||||
expanded := expandHome(path)
|
||||
abs, err := filepath.Abs(expanded)
|
||||
if err != nil {
|
||||
return expanded
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
|
||||
return resolved
|
||||
}
|
||||
// Non-existent paths can't be symlink-resolved; clean them instead.
|
||||
return filepath.Clean(abs)
|
||||
}
|
||||
|
||||
// isRelativeOrEqual — Python's Path.is_relative_to + equality.
|
||||
func isRelativeOrEqual(path, base string) bool {
|
||||
rel, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return rel == "." || (!strings.HasPrefix(rel, "..") && rel != "")
|
||||
}
|
||||
|
||||
// IsPathAllowed — blocked dirs checked FIRST, then allowed dirs.
|
||||
func IsPathAllowed(path string, allowedDirs []string) bool {
|
||||
path = ResolvePath(path)
|
||||
for _, blocked := range BlockedDirs {
|
||||
bp := ResolvePath(blocked)
|
||||
if isRelativeOrEqual(path, bp) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, allowed := range allowedDirs {
|
||||
ap := ResolvePath(allowed)
|
||||
if isRelativeOrEqual(path, ap) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateReadPath — (ok, message, resolved). Message parity: "Access
|
||||
// denied: … is outside allowed directories" / "Not found: …" / "Not a file: …".
|
||||
func ValidateReadPath(path string) (bool, string, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if !IsPathAllowed(resolved, AllowedReadDirs) {
|
||||
return false, fmt.Sprintf("Access denied: %s is outside allowed directories", path), ""
|
||||
}
|
||||
info, err := os.Stat(resolved)
|
||||
if os.IsNotExist(err) {
|
||||
return false, fmt.Sprintf("Not found: %s", path), ""
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Not found: %s", path), ""
|
||||
}
|
||||
if info.IsDir() {
|
||||
return false, fmt.Sprintf("Not a file: %s", path), ""
|
||||
}
|
||||
return true, "", resolved
|
||||
}
|
||||
|
||||
// ValidateWritePath — blocked-under-allowed gets the PROTECTED message
|
||||
// (truthful reason, one-step self-correction); plain outside gets the
|
||||
// allowed-dirs message (P0-05 P1).
|
||||
func ValidateWritePath(path string) (bool, string, string) {
|
||||
resolved := ResolvePath(path)
|
||||
for _, blocked := range BlockedDirs {
|
||||
bp := ResolvePath(blocked)
|
||||
if isRelativeOrEqual(resolved, bp) {
|
||||
return false, fmt.Sprintf(
|
||||
"Access denied: %s is in a protected directory (%s). "+
|
||||
"This directory is always blocked, even though it is inside an "+
|
||||
"allowed write directory. Choose a different location.", path, blocked), ""
|
||||
}
|
||||
}
|
||||
if !IsPathAllowed(resolved, AllowedWriteDirs) {
|
||||
return false, fmt.Sprintf(
|
||||
"Access denied: %s is outside allowed write directories: %v. "+
|
||||
"Write inside one of those directories instead.", path, AllowedWriteDirs), ""
|
||||
}
|
||||
return true, "", resolved
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package main
|
||||
|
||||
// prepare.go — the approval lifecycle. PARITY ports of prepare_tool /
|
||||
// _consume_prepared / approve_tool / reject_tool / the PreparedTool record.
|
||||
// The canonical digest (tool + args) and the path digest bind an approval to
|
||||
// an EXACT operation and target — modification after preview invalidates it.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PreparedTool — a side-effect-free preview awaiting approval.
|
||||
type PreparedTool struct {
|
||||
Token string `json:"token"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
OperationDigest string `json:"operation_digest"`
|
||||
CreatedAt float64 `json:"created_at"`
|
||||
ExpiresAt float64 `json:"expires_at"`
|
||||
RequestedPath string `json:"requested_path,omitempty"`
|
||||
ResolvedPath string `json:"resolved_path,omitempty"`
|
||||
PathDigest string `json:"path_digest,omitempty"`
|
||||
}
|
||||
|
||||
// Read-only tools execute immediately; everything else is prepared.
|
||||
var readOnlyTools = map[string]bool{
|
||||
"read_file": true, "list_directory": true, "glob_files": true,
|
||||
"grep_files": true, "run_command": true,
|
||||
}
|
||||
|
||||
// DefaultApprovalTTL — seconds (parity with DEFAULT_APPROVAL_TTL).
|
||||
const DefaultApprovalTTL = 300.0
|
||||
|
||||
// CanonicalDigest — sha256 of the canonical JSON:
|
||||
// json.dumps({"name": t, "arguments": a}, sort_keys=True,
|
||||
//
|
||||
// separators=(",", ":"), ensure_ascii=False) — FULL 64-hex.
|
||||
//
|
||||
// PARITY-CRITICAL: the digest must be byte-identical with Python's
|
||||
// _canonical_digest (a mismatch would reject every real approval).
|
||||
func CanonicalDigest(toolName string, args map[string]interface{}) string {
|
||||
canonical := canonicalJSON(map[string]interface{}{"name": toolName, "arguments": args})
|
||||
sum := sha256.Sum256([]byte(canonical))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// canonicalJSON — Python json.dumps(sort_keys=True, separators=(",",":"))
|
||||
// semantics: compact separators, sorted keys, strings escaped per JSON.
|
||||
func canonicalJSON(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(t))
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
parts[i] = jsonEsc(k) + ":" + canonicalJSON(t[k])
|
||||
}
|
||||
return "{" + strings.Join(parts, ",") + "}"
|
||||
case []interface{}:
|
||||
parts := make([]string, len(t))
|
||||
for i, e := range t {
|
||||
parts[i] = canonicalJSON(e)
|
||||
}
|
||||
return "[" + strings.Join(parts, ",") + "]"
|
||||
case string:
|
||||
return jsonEsc(t)
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case float64:
|
||||
// Python json encodes integral floats as ints (1 → "1").
|
||||
if t == math.Trunc(t) && !math.IsInf(t, 0) {
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
}
|
||||
return strconv.FormatFloat(t, 'g', -1, 64)
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
case nil:
|
||||
return "null"
|
||||
default:
|
||||
b, _ := json.Marshal(t)
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
// jsonEsc — Python-compatible JSON string escaping (ensure_ascii=False:
|
||||
// non-ASCII passes through raw).
|
||||
func jsonEsc(s string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
b.WriteString("\\\"")
|
||||
case '\\':
|
||||
b.WriteString("\\\\")
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PathDigest — content-bound digest (PARITY with _path_digest):
|
||||
//
|
||||
// missing path → sha256("missing")
|
||||
// file → sha256("file\0" + content chunks)
|
||||
// other → sha256("other\0mode\0size\0mtime_ns")
|
||||
func PathDigest(resolved string) string {
|
||||
info, err := os.Stat(resolved)
|
||||
if os.IsNotExist(err) {
|
||||
sum := sha256.Sum256([]byte("missing"))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
h := sha256.New()
|
||||
h.Write([]byte("file\x00"))
|
||||
if f, err := os.Open(resolved); err == nil {
|
||||
defer f.Close()
|
||||
buf := make([]byte, 1024*1024)
|
||||
for {
|
||||
n, err := f.Read(buf)
|
||||
if n > 0 {
|
||||
h.Write(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
if err == nil {
|
||||
marker := fmt.Sprintf("other\x00%d\x00%d\x00%d", info.Mode(), info.Size(), info.ModTime().UnixNano())
|
||||
sum := sha256.Sum256([]byte(marker))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
sum := sha256.Sum256([]byte("missing"))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ApprovalStore — consumed tokens (parity with _CONSUMED_APPROVALS).
|
||||
type ApprovalStore struct {
|
||||
consumed map[string]bool
|
||||
}
|
||||
|
||||
func NewApprovalStore() *ApprovalStore { return &ApprovalStore{consumed: map[string]bool{}} }
|
||||
|
||||
// ConsumePrepared — one-shot approval consumption with immediate
|
||||
// revalidation. Returns "" on success or the failure reason.
|
||||
func (a *ApprovalStore) ConsumePrepared(p PreparedTool, now float64) string {
|
||||
if a.consumed[p.Token] {
|
||||
return "Approval already resolved"
|
||||
}
|
||||
a.consumed[p.Token] = true
|
||||
if now > p.ExpiresAt {
|
||||
return "Approval expired"
|
||||
}
|
||||
if CanonicalDigest(p.Name, p.Arguments) != p.OperationDigest {
|
||||
return "Pending operation was modified"
|
||||
}
|
||||
if p.RequestedPath != "" {
|
||||
ok, message, resolved := ValidateWritePath(p.RequestedPath)
|
||||
if !ok || resolved == "" {
|
||||
return message
|
||||
}
|
||||
if resolved != p.ResolvedPath {
|
||||
return "Target path changed after preview"
|
||||
}
|
||||
if PathDigest(resolved) != p.PathDigest {
|
||||
return "Target changed after preview"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ApproveTool — consume + revalidate, then the caller performs the side
|
||||
// effect. PARITY with approve_tool's envelope.
|
||||
func (a *ApprovalStore) ApproveTool(p PreparedTool, now float64) map[string]interface{} {
|
||||
if err := a.ConsumePrepared(p, now); err != "" {
|
||||
return map[string]interface{}{"success": false, "error": err}
|
||||
}
|
||||
return map[string]interface{}{"success": true, "result": fmt.Sprintf("Approved: %s", p.Name)}
|
||||
}
|
||||
|
||||
// RejectTool — resolve a pending approval as rejected.
|
||||
func (a *ApprovalStore) RejectTool(p PreparedTool) map[string]interface{} {
|
||||
if a.consumed[p.Token] {
|
||||
return map[string]interface{}{"success": false, "error": "Approval already resolved"}
|
||||
}
|
||||
a.consumed[p.Token] = true
|
||||
return map[string]interface{}{"success": true, "result": fmt.Sprintf("Rejected: %s", p.Name)}
|
||||
}
|
||||
|
||||
// NewPreparedTool — build the preview record (digests + TTL).
|
||||
func NewPreparedTool(toolName string, args map[string]interface{}, requestedPath, resolvedPath string, now float64, ttl float64) PreparedTool {
|
||||
p := PreparedTool{
|
||||
Token: newToken(),
|
||||
Name: toolName,
|
||||
Arguments: args,
|
||||
OperationDigest: CanonicalDigest(toolName, args),
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now + ttl,
|
||||
RequestedPath: requestedPath,
|
||||
ResolvedPath: resolvedPath,
|
||||
}
|
||||
if resolvedPath != "" {
|
||||
p.PathDigest = PathDigest(resolvedPath)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func newToken() string {
|
||||
return fmt.Sprintf("%x", time.Now().UnixNano()) // uuid-equivalent uniqueness for one process
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
// prompt.go — batch 6: the tool-prompt builder and the TOOLS registry.
|
||||
// The registry is DATA (ported from the Python oracle as embedded JSON —
|
||||
// tools_registry.json, generated live 2026-10-07); the builder is logic.
|
||||
// PARITY: the prompt is byte-identical with build_tool_prompt's output
|
||||
// (verified in truth_prompt.json).
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed tools_registry.json
|
||||
var toolsRegistryJSON []byte
|
||||
|
||||
// ToolDef — one entry of the registry.
|
||||
type ToolDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters struct {
|
||||
Properties map[string]struct {
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
} `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
} `json:"parameters"`
|
||||
}
|
||||
|
||||
// Tools — the parsed registry (order preserved).
|
||||
var Tools []ToolDef
|
||||
|
||||
func init() {
|
||||
if err := jsonUnmarshal(toolsRegistryJSON, &Tools); err != nil {
|
||||
panic("tools_registry.json: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// BuildToolPrompt — the model-facing tool definitions section.
|
||||
func BuildToolPrompt() string {
|
||||
var lines []string
|
||||
lines = append(lines, "## Available Tools", "")
|
||||
lines = append(lines, "You can use these tools by responding with a JSON object:")
|
||||
lines = append(lines, "```json\n{\"tool\": \"tool_name\", \"args\": {\"param\": \"value\"}}\n```\n")
|
||||
lines = append(lines, "After receiving a tool result, continue your response normally.\n")
|
||||
|
||||
for _, tool := range Tools {
|
||||
lines = append(lines, "### "+tool.Name)
|
||||
lines = append(lines, tool.Description)
|
||||
required := map[string]bool{}
|
||||
for _, r := range tool.Parameters.Required {
|
||||
required[r] = true
|
||||
}
|
||||
// PARITY: parameter order follows the JSON object order of the
|
||||
// registry — Go maps lose order, so iterate via the raw JSON.
|
||||
for _, pname := range paramOrder(tool.Name) {
|
||||
pinfo, ok := tool.Parameters.Properties[pname]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
req := " (optional)"
|
||||
if required[pname] {
|
||||
req = " (required)"
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf(" - `%s`: %s%s", pname, pinfo.Description, req))
|
||||
}
|
||||
lines = append(lines, "")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package main
|
||||
|
||||
// quality.go — the quality-gate tools. PARITY ports of _detect_language,
|
||||
// _lint_file, _run_tests, _format_code with the same LANGUAGE_CONFIG table
|
||||
// and the same shell-invocation semantics (shell=True in Python → sh -c
|
||||
// here), same timeouts (lint 30s, tests 120s).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LanguageConfig — PARITY with tool_executor.py:1215.
|
||||
type LanguageConfig struct {
|
||||
Lint string
|
||||
Format string
|
||||
Test string
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
var languageConfig = map[string]LanguageConfig{
|
||||
"python": {Lint: "ruff check", Format: "ruff format", Test: "python -m pytest", Extensions: []string{".py"}},
|
||||
"javascript": {Lint: "eslint", Format: "prettier --write", Test: "npm test", Extensions: []string{".js", ".jsx"}},
|
||||
"typescript": {Lint: "eslint", Format: "prettier --write", Test: "npm test", Extensions: []string{".ts", ".tsx"}},
|
||||
"go": {Lint: "golangci-lint run", Format: "gofmt -w", Test: "go test ./...", Extensions: []string{".go"}},
|
||||
"rust": {Lint: "cargo clippy", Format: "cargo fmt", Test: "cargo test", Extensions: []string{".rs"}},
|
||||
}
|
||||
|
||||
// DetectLanguage — by file extension (lowercased).
|
||||
func DetectLanguage(path string) string {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
for lang, cfg := range languageConfig {
|
||||
for _, e := range cfg.Extensions {
|
||||
if ext == e {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// shellRun — Python subprocess.run(shell=True) equivalent.
|
||||
func shellRun(command string, cwd string, timeout time.Duration) (int, string, string) {
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
if cwd != "" {
|
||||
cmd.Dir = cwd
|
||||
}
|
||||
var outBuf, errBuf strings.Builder
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = &errBuf
|
||||
done := make(chan error, 1)
|
||||
cmd.Start()
|
||||
go func() { done <- cmd.Wait() }()
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
cmd.Process.Kill()
|
||||
return -1, outBuf.String(), "TIMEOUT"
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return ee.ExitCode(), outBuf.String(), errBuf.String()
|
||||
}
|
||||
return 1, outBuf.String(), err.Error()
|
||||
}
|
||||
return 0, outBuf.String(), errBuf.String()
|
||||
}
|
||||
}
|
||||
|
||||
// LintFile — run the configured linter; "✓ <name> — no issues" on clean.
|
||||
func LintFile(path, language string) (bool, string) {
|
||||
ok, msg, resolved := ValidateReadPath(path)
|
||||
if !ok || resolved == "" {
|
||||
return false, msg
|
||||
}
|
||||
if language == "" {
|
||||
language = DetectLanguage(resolved)
|
||||
}
|
||||
cfg, known := languageConfig[language]
|
||||
if !known {
|
||||
return true, fmt.Sprintf("No linter configured for %s", filepath.Ext(resolved))
|
||||
}
|
||||
code, stdout, stderr := shellRun(cfg.Lint+" "+resolved, "", 30*time.Second)
|
||||
if code == 0 {
|
||||
return true, fmt.Sprintf("✓ %s — no issues", filepath.Base(resolved))
|
||||
}
|
||||
out := stdout
|
||||
if out == "" {
|
||||
out = stderr
|
||||
}
|
||||
return false, out
|
||||
}
|
||||
|
||||
// RunTests — explicit command or the language default; cwd = file's parent
|
||||
// or the dir itself. 120s timeout (parity), stderr appended to output.
|
||||
func RunTests(path, command string) (bool, string) {
|
||||
resolved := ResolvePath(path)
|
||||
if command == "" {
|
||||
language := DetectLanguage(resolved)
|
||||
if cfg, known := languageConfig[language]; known {
|
||||
command = cfg.Test
|
||||
} else {
|
||||
return false, fmt.Sprintf("No test runner configured for %s", filepath.Ext(resolved))
|
||||
}
|
||||
}
|
||||
cwd := resolved
|
||||
if info, err := os.Stat(resolved); err == nil && !info.IsDir() {
|
||||
cwd = filepath.Dir(resolved)
|
||||
}
|
||||
code, stdout, stderr := shellRun(command, cwd, 120*time.Second)
|
||||
output := stdout
|
||||
if stderr != "" && stderr != "TIMEOUT" {
|
||||
output += "\n" + stderr
|
||||
}
|
||||
if stderr == "TIMEOUT" {
|
||||
return false, "Tests timed out (120s)"
|
||||
}
|
||||
if code == 0 {
|
||||
if output == "" {
|
||||
return true, "All tests passed"
|
||||
}
|
||||
return true, output
|
||||
}
|
||||
if output == "" {
|
||||
return false, "Tests failed"
|
||||
}
|
||||
return false, output
|
||||
}
|
||||
|
||||
// FormatCode — run the configured formatter (writes in place).
|
||||
func FormatCode(path, language string) (bool, string) {
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if !ok || resolved == "" {
|
||||
return false, msg
|
||||
}
|
||||
if language == "" {
|
||||
language = DetectLanguage(resolved)
|
||||
}
|
||||
cfg, known := languageConfig[language]
|
||||
if !known {
|
||||
return true, fmt.Sprintf("No formatter configured for %s", filepath.Ext(resolved))
|
||||
}
|
||||
code, stdout, stderr := shellRun(cfg.Format+" "+resolved, "", 30*time.Second)
|
||||
if code == 0 {
|
||||
return true, fmt.Sprintf("✓ %s — formatted", filepath.Base(resolved))
|
||||
}
|
||||
out := stdout
|
||||
if out == "" {
|
||||
out = stderr
|
||||
}
|
||||
return false, out
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,492 @@
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path exists and is a readable file"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Line number to start from (1-indexed, optional)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max lines to return (default 100, optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"search string matches exactly once"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites the target file atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"search": {
|
||||
"type": "string",
|
||||
"description": "Exact text to find (must match file content)"
|
||||
},
|
||||
"replace": {
|
||||
"type": "string",
|
||||
"description": "Text to replace with"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"search",
|
||||
"replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory"
|
||||
],
|
||||
"side_effects": [
|
||||
"writes the full file content atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern (e.g. '**/*.py', 'src/**/*.ts')"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional, defaults to cwd)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "grep_files",
|
||||
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Regex pattern to search for"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional)"
|
||||
},
|
||||
"include": {
|
||||
"type": "string",
|
||||
"description": "File pattern to include (e.g. '*.py', '*.{ts,tsx}')"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "List files and directories at a path. Returns names with / suffix for directories.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path is a readable directory"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the directory"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lint_file",
|
||||
"description": "Lint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"file exists",
|
||||
"a linter is configured for the language"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional, auto-detected from extension)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_tests",
|
||||
"description": "Run tests for a file or project. Auto-detects test runner from language.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"a test runner is configured for the project"
|
||||
],
|
||||
"side_effects": [
|
||||
"runs project test commands which may create or delete files"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to test"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Custom test command (optional, overrides auto-detect)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format_code",
|
||||
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"a formatter is configured for the language"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites files in place to the formatter's style"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to format"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Show git diff for staged or unstaged changes.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"staged": {
|
||||
"type": "boolean",
|
||||
"description": "Show staged changes (default: unstaged)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file to diff (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Show recent git commits.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of commits (default 10)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file history (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit changes with a message. Auto-stages modified files.",
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"working directory is a git repository",
|
||||
"there are changes to commit"
|
||||
],
|
||||
"side_effects": [
|
||||
"creates a commit on the current branch"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Commit message"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Specific files to commit (optional, defaults to all modified)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "repo_map",
|
||||
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"target directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Root directory to map"
|
||||
},
|
||||
"languages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Filter by languages (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"operator approves the command",
|
||||
"required binaries are on PATH"
|
||||
],
|
||||
"side_effects": [
|
||||
"executes arbitrary commands with their full side effects"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command text; shell syntax is allowed only with shell=true"
|
||||
},
|
||||
"argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Argument vector for shell-free execution"
|
||||
},
|
||||
"shell": {
|
||||
"type": "boolean",
|
||||
"description": "Explicitly request the user-approved shell execution path"
|
||||
},
|
||||
"workdir": {
|
||||
"type": "string",
|
||||
"description": "Working directory (optional)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Timeout in seconds (default 30)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read_persona",
|
||||
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"persona MEMORY.md exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "query_memory",
|
||||
"description": "Query Rune's memory store. Returns relevant memories for a query.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"memory store database is available"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query for memory"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max results (default 5)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "check_growth",
|
||||
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"growth log file exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max entries (default 10)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "diagnose_behavior",
|
||||
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"recent response text provided"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Recent response text to analyze"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find_capability",
|
||||
"description": "Search all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"a capability query describing the needed outcome"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "What you need to do, e.g. 'run the test suite' or 'commit my changes'"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)"
|
||||
},
|
||||
"fixtures": {
|
||||
"files": [
|
||||
{
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/a.txt",
|
||||
"content": "the spine rebuild parity\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/b.log",
|
||||
"content": "spine appears here too\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/file.txt",
|
||||
"content": "content\n"
|
||||
}
|
||||
],
|
||||
"dirs": [
|
||||
"{{TMP}}/adir"
|
||||
]
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"fn": "read_window",
|
||||
"name": "mid",
|
||||
"input": {
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"offset": 3,
|
||||
"limit": 5
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "[showing from line 3]\n3: line 3\n4: line 4\n5: line 5\n6: line 6\n7: line 7\n\n[showing lines 3-7 of 21 total]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "read_window",
|
||||
"name": "default",
|
||||
"input": {
|
||||
"path": "{{TMP}}/w.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"lines": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "diff",
|
||||
"name": "simple",
|
||||
"input": {
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"new_content": "line 1\nCHANGED\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
"expected": {
|
||||
"diff_head": [
|
||||
"--- a/w.txt",
|
||||
"+++ b/w.txt",
|
||||
"@@ -1,5 +1,5 @@"
|
||||
],
|
||||
"has_minus": true,
|
||||
"has_plus": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
// truth_fileops_test.go — batch-2 parity gate: windowed read, diff shape,
|
||||
// against truth_fileops.json (live Python oracle, regenerated hermetically
|
||||
// 2026-10-07 — contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileOpsParity(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_fileops.json")
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Name {
|
||||
case "mid":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, result := ReadFile(path, truthInt(c.Input, "offset"), truthInt(c.Input, "limit"))
|
||||
want := truthStr(c.Expected, "result")
|
||||
if !ok || result != want {
|
||||
t.Errorf("mid:\n got %q\nwant %q", result, want)
|
||||
}
|
||||
case "default":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, result := ReadFile(path, 1, 100)
|
||||
wantLines := truthInt(c.Expected, "lines")
|
||||
gotLines := len(strings.Split(result, "\n"))
|
||||
if !ok || gotLines != wantLines {
|
||||
t.Errorf("default: ok=%v lines=%d want %d", ok, gotLines, wantLines)
|
||||
}
|
||||
case "simple":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
edited := truthStr(c.Input, "new_content")
|
||||
e := c.Expected
|
||||
diff := GenerateDiff(path, edited)
|
||||
lines := strings.Split(diff, "\n")
|
||||
wantHead := e["diff_head"].([]interface{})
|
||||
if lines[0] != wantHead[0].(string) ||
|
||||
lines[1] != wantHead[1].(string) ||
|
||||
lines[2] != wantHead[2].(string) {
|
||||
t.Errorf("simple diff head:\n got %v\nwant %v", lines[:3], wantHead)
|
||||
}
|
||||
if hasMinus, _ := e["has_minus"].(bool); hasMinus {
|
||||
found := false
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "-line 2") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("simple diff missing -line 2: %q", diff)
|
||||
}
|
||||
}
|
||||
if hasPlus, _ := e["has_plus"].(bool); hasPlus {
|
||||
found := false
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "+CHANGED") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("simple diff missing +CHANGED: %q", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteApprovalFlow — write_file NEVER writes; only ApplyContent does.
|
||||
func TestWriteApprovalFlow(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_fileops.json")
|
||||
_ = doc
|
||||
dir := wRoot
|
||||
if dir == "" {
|
||||
dir = tmpRoot
|
||||
}
|
||||
target := filepath.Join(dir, "approval-flow.txt")
|
||||
res := WriteFile(target, "hello world")
|
||||
if res["success"] != true {
|
||||
// Outside allowed dirs in this environment — the denial IS parity.
|
||||
if _, hasErr := res["error"]; !hasErr {
|
||||
t.Fatalf("write preview: neither success nor error: %v", res)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, statErr := os.Stat(target); statErr == nil {
|
||||
t.Fatal("write_file preview must NOT create the file")
|
||||
}
|
||||
if _, has := res["needs_confirm"]; !has {
|
||||
t.Errorf("preview must carry needs_confirm: %v", res)
|
||||
}
|
||||
applied := ApplyContent(target, "hello world")
|
||||
if applied["success"] != true {
|
||||
t.Fatalf("apply failed: %v", applied)
|
||||
}
|
||||
data, _ := os.ReadFile(target)
|
||||
if string(data) != "hello world" {
|
||||
t.Errorf("apply wrote %q", data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
// truth_harness_test.go — hermetic fixture/substitution harness for the
|
||||
// truth batteries (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
//
|
||||
// The truth JSONs carry {{TMP}}/{{WROOT}} placeholders (see
|
||||
// scripts/gen_tool_executor_truth.py): the generator ran the live Python
|
||||
// oracle under ITS allowed roots; this harness recreates the same fixture
|
||||
// content under the GO SIDE'S own allowed roots at runtime, substitutes
|
||||
// placeholders everywhere, and the tests compare exactly. No host path
|
||||
// crosses environments — the CI runner user needs only its own home + tmp.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// truthCase — the shared case shape across the batteries.
|
||||
type truthCase struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
|
||||
type truthFixtures struct {
|
||||
Files []struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
} `json:"files"`
|
||||
Dirs []string `json:"dirs"`
|
||||
}
|
||||
|
||||
type truthDoc struct {
|
||||
Oracle string `json:"oracle"`
|
||||
Fixtures truthFixtures `json:"fixtures"`
|
||||
Cases []truthCase `json:"cases"`
|
||||
}
|
||||
|
||||
// loadTruth — read a truth doc, recreate its fixtures under runtime roots,
|
||||
// return the doc plus the roots for substitution.
|
||||
func loadTruth(t *testing.T, name string) (truthDoc, string, string) {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("%s missing: %v", name, err)
|
||||
}
|
||||
var doc truthDoc
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("%s unparseable: %v", name, err)
|
||||
}
|
||||
tmpRoot := mkTruthRoot(t, AllowedReadDirs)
|
||||
wRoot := mkTruthRoot(t, AllowedWriteDirs)
|
||||
for _, f := range doc.Fixtures.Files {
|
||||
p := substRoots(f.Path, tmpRoot, wRoot)
|
||||
if err := os.WriteFile(p, []byte(substRoots(f.Content, tmpRoot, wRoot)), 0o644); err != nil {
|
||||
t.Fatalf("fixture %s: %v", p, err)
|
||||
}
|
||||
}
|
||||
for _, d := range doc.Fixtures.Dirs {
|
||||
if err := os.MkdirAll(substRoots(d, tmpRoot, wRoot), 0o755); err != nil {
|
||||
t.Fatalf("fixture dir %s: %v", d, err)
|
||||
}
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(tmpRoot)
|
||||
os.RemoveAll(wRoot)
|
||||
})
|
||||
return doc, tmpRoot, wRoot
|
||||
}
|
||||
|
||||
// mkTruthRoot — first creatable temp dir among the allowed dirs (the same
|
||||
// resolution order the generator used).
|
||||
func mkTruthRoot(t *testing.T, dirs []string) string {
|
||||
t.Helper()
|
||||
for _, d := range dirs {
|
||||
p, err := os.MkdirTemp(d, "rune-truth-")
|
||||
if err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
t.Fatalf("no creatable root among %v", dirs)
|
||||
return ""
|
||||
}
|
||||
|
||||
// substRoots — replace the truth placeholders with the runtime roots.
|
||||
func substRoots(s, tmpRoot, wRoot string) string {
|
||||
s = strings.ReplaceAll(s, "{{TMP}}", tmpRoot)
|
||||
s = strings.ReplaceAll(s, "{{WROOT}}", wRoot)
|
||||
return s
|
||||
}
|
||||
|
||||
// truthStr / truthInt — typed accessors with the zero value on absence
|
||||
// (a missing string field is "" in the cases we run).
|
||||
func truthStr(m map[string]interface{}, k string) string {
|
||||
v, _ := m[k].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func truthInt(m map[string]interface{}, k string) int {
|
||||
v, _ := m[k].(float64)
|
||||
return int(v)
|
||||
}
|
||||
|
||||
// sortedLines — deterministic comparison for cases whose oracle order is
|
||||
// OS-scheduling dependent (the recursive search walk).
|
||||
func sortedLines(s string) []string {
|
||||
lines := strings.Split(s, "\n")
|
||||
sort.Strings(lines)
|
||||
return lines
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)"
|
||||
},
|
||||
"fixtures": {
|
||||
"files": [
|
||||
{
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/a.txt",
|
||||
"content": "the spine rebuild parity\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/b.log",
|
||||
"content": "spine appears here too\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/file.txt",
|
||||
"content": "content\n"
|
||||
}
|
||||
],
|
||||
"dirs": [
|
||||
"{{TMP}}/adir"
|
||||
]
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"fn": "list_dir",
|
||||
"name": "basic",
|
||||
"input": {
|
||||
"path": "{{TMP}}"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "📄 a.txt\n📁 adir/\n📄 b.log\n📄 file.txt\n📄 w.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "list_dir",
|
||||
"name": "pattern",
|
||||
"input": {
|
||||
"path": "{{TMP}}",
|
||||
"pattern": "*.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "📄 a.txt\n📄 file.txt\n📄 w.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grep",
|
||||
"name": "hit",
|
||||
"input": {
|
||||
"path": "{{TMP}}",
|
||||
"pattern": "spine"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "a.txt:1: the spine rebuild parity\nb.log:1: spine appears here too"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "grep",
|
||||
"name": "miss",
|
||||
"input": {
|
||||
"path": "{{TMP}}",
|
||||
"pattern": "absent-token"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": "(no matches)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "pipe_refused",
|
||||
"input": {
|
||||
"command": "ls -la | head"
|
||||
},
|
||||
"expected": {
|
||||
"success": false,
|
||||
"error": "Shell syntax requires explicit shell approval"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "plain_ok",
|
||||
"input": {
|
||||
"command": "ls -la"
|
||||
},
|
||||
"expected": {
|
||||
"success": true,
|
||||
"error": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "shell_ok",
|
||||
"input": {
|
||||
"command": "true",
|
||||
"shell": true
|
||||
},
|
||||
"expected": {
|
||||
"success": true,
|
||||
"error": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate",
|
||||
"name": "shell_empty",
|
||||
"input": {
|
||||
"shell": true
|
||||
},
|
||||
"expected": {
|
||||
"success": false,
|
||||
"error": "Explicit shell command is required"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
// truth_ops_test.go — batch-3 parity gate: list/grep/cmd-gate against
|
||||
// truth_ops.json (live Python oracle, regenerated hermetically 2026-10-07
|
||||
// — contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
//
|
||||
// list_dir is EXACT (both sides sort entries). grep compares as sorted
|
||||
// line sets: the oracle's os.walk order is OS-scheduling dependent, not a
|
||||
// parity-relevant property (same discipline as the oracle-battery law —
|
||||
// expectations come from the oracle; only comparison order is normalized).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpsParity(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_ops.json")
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "list_dir":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
pattern := truthStr(c.Input, "pattern")
|
||||
ok, result := ListDirectory(path, pattern)
|
||||
wantOk := c.Expected["ok"].(bool)
|
||||
wantRes := truthStr(c.Expected, "result")
|
||||
if ok != wantOk {
|
||||
t.Errorf("%s: ok=%v want %v", c.Name, ok, wantOk)
|
||||
}
|
||||
if result != wantRes {
|
||||
t.Errorf("%s:\n got %q\nwant %q", c.Name, result, wantRes)
|
||||
}
|
||||
case "grep":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
pattern := truthStr(c.Input, "pattern")
|
||||
ok, result := GrepFiles(path, pattern, "")
|
||||
wantOk := c.Expected["ok"].(bool)
|
||||
wantRes := truthStr(c.Expected, "result")
|
||||
if ok != wantOk {
|
||||
t.Errorf("%s: ok=%v want %v", c.Name, ok, wantOk)
|
||||
}
|
||||
if wantRes == "(no matches)" {
|
||||
if result != "(no matches)" {
|
||||
t.Errorf("%s: got %q want (no matches)", c.Name, result)
|
||||
}
|
||||
continue
|
||||
}
|
||||
gotSorted := sortedLines(result)
|
||||
wantSorted := sortedLines(wantRes)
|
||||
if strings.Join(gotSorted, "\n") != strings.Join(wantSorted, "\n") {
|
||||
t.Errorf("%s (sorted sets):\n got %q\nwant %q", c.Name, result, wantRes)
|
||||
}
|
||||
case "cmd_gate":
|
||||
e := c.Expected
|
||||
var gotSuccess bool
|
||||
var gotErr string
|
||||
switch c.Name {
|
||||
case "pipe_refused":
|
||||
_, _, err := CommandGate("ls -la | head", nil, false)
|
||||
gotSuccess = err == ""
|
||||
gotErr = err
|
||||
case "plain_ok":
|
||||
argv, _, err := CommandGate("ls -la", nil, false)
|
||||
gotSuccess = err == "" && len(argv) == 2
|
||||
gotErr = err
|
||||
case "shell_ok":
|
||||
_, useShell, err := CommandGate("rm -rf /", nil, true)
|
||||
gotSuccess = err == "" && useShell
|
||||
gotErr = err
|
||||
case "shell_empty":
|
||||
_, _, err := CommandGate("", nil, true)
|
||||
gotSuccess = err == ""
|
||||
gotErr = err
|
||||
}
|
||||
if gotSuccess != e["success"].(bool) {
|
||||
t.Errorf("%s: success=%v want %v (err=%q)", c.Name, gotSuccess, e["success"], gotErr)
|
||||
}
|
||||
if wantErr, has := e["error"]; has && gotErr != wantErr.(string) {
|
||||
t.Errorf("%s: err got %q want %q", c.Name, gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGrepTruncationMarker — the 50-match truncate marker is a hard parity
|
||||
// string; exercised hermetically (55 matching lines in one fixture file).
|
||||
func TestGrepTruncationMarker(t *testing.T) {
|
||||
dir := mkTruthRoot(t, AllowedReadDirs)
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
var sb strings.Builder
|
||||
for i := 0; i < 55; i++ {
|
||||
sb.WriteString("needle line\n")
|
||||
}
|
||||
if err := os.WriteFile(dir+"/trunc.txt", []byte(sb.String()), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ok, result := GrepFiles(dir, "needle", "")
|
||||
if !ok {
|
||||
t.Fatal("grep failed")
|
||||
}
|
||||
if !strings.HasSuffix(result, "\n[truncated — 50 matches]") {
|
||||
t.Errorf("truncation marker missing: %q", result[len(result)-60:])
|
||||
}
|
||||
if got := strings.Count(result, "needle"); got != 50 {
|
||||
t.Errorf("matches=%d want 50", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
[
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "bare_valid",
|
||||
"input": {
|
||||
"text": "{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "read_file",
|
||||
"args": {
|
||||
"path": "/tmp/x"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "fenced",
|
||||
"input": {
|
||||
"text": "```json\n{\"tool\": \"list_directory\", \"args\": {\"path\": \".\"}}\n```"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "list_directory",
|
||||
"args": {
|
||||
"path": "."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "xml",
|
||||
"input": {
|
||||
"text": "<tool_call>\n{\"tool\": \"run_command\", \"args\": {\"command\": \"ls\"}}\n</tool_call>"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "run_command",
|
||||
"args": {
|
||||
"command": "ls"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "inline",
|
||||
"input": {
|
||||
"text": "text before {\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/y\"}} text after"
|
||||
},
|
||||
"expected": {
|
||||
"tool": "read_file",
|
||||
"args": {
|
||||
"path": "/tmp/y"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse",
|
||||
"name": "none",
|
||||
"input": {
|
||||
"text": "no tool call here"
|
||||
},
|
||||
"expected": {
|
||||
"tool": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "format",
|
||||
"name": "ok",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"text": "[Tool result]\nthe output"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "format",
|
||||
"name": "err",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"text": "[Tool error]\nAccess denied: /etc"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
// truth_parse_test.go — batch-5 parity gate: protocol parsing and result
|
||||
// formatting against truth_parse.json (live Python oracle, 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch5ParseParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_parse.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_parse.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "parse":
|
||||
text, _ := c.Input["text"].(string)
|
||||
got := ParseToolCall(text)
|
||||
wantTool := c.Expected["tool"]
|
||||
if wantTool == nil {
|
||||
if got != nil {
|
||||
t.Errorf("%s: got %v want nil", c.Name, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if got == nil {
|
||||
t.Errorf("%s: got nil want tool %v", c.Name, wantTool)
|
||||
continue
|
||||
}
|
||||
if got.Tool != wantTool.(string) {
|
||||
t.Errorf("%s: tool got %q want %q", c.Name, got.Tool, wantTool)
|
||||
}
|
||||
wantArgs, hasArgs := c.Expected["args"]
|
||||
if hasArgs && wantArgs != nil {
|
||||
wantMap := wantArgs.(map[string]interface{})
|
||||
if !reflect.DeepEqual(got.Args, wantMap) {
|
||||
t.Errorf("%s: args got %v want %v", c.Name, got.Args, wantMap)
|
||||
}
|
||||
}
|
||||
case "format":
|
||||
got := FormatToolResult(map[string]interface{}{
|
||||
"success": c.Name == "ok",
|
||||
"result": "the output",
|
||||
"error": "Access denied: /etc",
|
||||
})
|
||||
if got != c.Expected["text"].(string) {
|
||||
t.Errorf("%s:\n got %q\nwant %q", c.Name, got, c.Expected["text"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseUnknownToolStillParses — validation is dispatch's job; the
|
||||
// parser accepts any tool name (fail-open parse, fail-closed dispatch).
|
||||
func TestParseUnknownToolStillParses(t *testing.T) {
|
||||
got := ParseToolCall(`{"tool": "definitely_not_a_tool", "args": {}}`)
|
||||
if got == nil || got.Tool != "definitely_not_a_tool" {
|
||||
t.Errorf("unknown tool must still parse: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)"
|
||||
},
|
||||
"fixtures": {
|
||||
"files": [
|
||||
{
|
||||
"path": "{{TMP}}/w.txt",
|
||||
"content": "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/a.txt",
|
||||
"content": "the spine rebuild parity\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/b.log",
|
||||
"content": "spine appears here too\n"
|
||||
},
|
||||
{
|
||||
"path": "{{TMP}}/file.txt",
|
||||
"content": "content\n"
|
||||
}
|
||||
],
|
||||
"dirs": [
|
||||
"{{TMP}}/adir"
|
||||
]
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"fn": "resolve",
|
||||
"name": "abs",
|
||||
"input": {
|
||||
"path": "{{TMP}}/file.txt"
|
||||
},
|
||||
"expected": {
|
||||
"resolved": "{{TMP}}/file.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_read",
|
||||
"name": "hit",
|
||||
"input": {
|
||||
"path": "{{TMP}}/file.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_read",
|
||||
"name": "miss",
|
||||
"input": {
|
||||
"path": "{{TMP}}/missing.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"msg": "Not found: {{TMP}}/missing.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_read",
|
||||
"name": "blocked",
|
||||
"input": {
|
||||
"path": "/etc/hostname"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_write",
|
||||
"name": "protected",
|
||||
"input": {
|
||||
"path": "/etc/x.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"msg_prefix": "Access denied: /etc/x.txt is in a protected direct"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_write",
|
||||
"name": "outside",
|
||||
"input": {
|
||||
"path": "/usr/share/x.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"msg_prefix": "Access denied: /usr/share/x.txt is outside allowed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate_write",
|
||||
"name": "ok",
|
||||
"input": {
|
||||
"path": "{{WROOT}}/new.txt"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"resolved": "{{WROOT}}/new.txt"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
// truth_paths_test.go — the tool-executor parity gate: path security
|
||||
// (truth_paths.json) and the digest algorithms (byte-verified against the
|
||||
// Python oracles _canonical_digest and _path_digest).
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathsParity(t *testing.T) {
|
||||
doc, tmpRoot, wRoot := loadTruth(t, "truth_paths.json")
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "resolve":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
got := ResolvePath(path)
|
||||
want := substRoots(truthStr(c.Expected, "resolved"), tmpRoot, wRoot)
|
||||
if got != want {
|
||||
t.Errorf("%s: ResolvePath got %q want %q", c.Name, got, want)
|
||||
}
|
||||
case "validate_read":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, msg, _ := ValidateReadPath(path)
|
||||
if ok != c.Expected["ok"].(bool) {
|
||||
t.Errorf("%s: ok=%v want %v (msg=%q)", c.Name, ok, c.Expected["ok"], msg)
|
||||
}
|
||||
if wantMsg, has := c.Expected["msg"]; has {
|
||||
want := substRoots(wantMsg.(string), tmpRoot, wRoot)
|
||||
if msg != want {
|
||||
t.Errorf("%s: msg got %q want %q", c.Name, msg, want)
|
||||
}
|
||||
}
|
||||
case "validate_write":
|
||||
path := substRoots(truthStr(c.Input, "path"), tmpRoot, wRoot)
|
||||
ok, msg, resolved := ValidateWritePath(path)
|
||||
if ok != c.Expected["ok"].(bool) {
|
||||
t.Errorf("%s: ok=%v want %v (msg=%q)", c.Name, ok, c.Expected["ok"], msg)
|
||||
}
|
||||
if prefix, has := c.Expected["msg_prefix"]; has {
|
||||
if !strings.HasPrefix(msg, prefix.(string)) {
|
||||
t.Errorf("%s: msg %q missing prefix %q", c.Name, msg, prefix)
|
||||
}
|
||||
}
|
||||
if wantRes, has := c.Expected["resolved"]; has && wantRes != nil {
|
||||
want := substRoots(wantRes.(string), tmpRoot, wRoot)
|
||||
if resolved != want {
|
||||
t.Errorf("%s: resolved got %q want %q", c.Name, resolved, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCanonicalDigestByteParity — the digest must be BYTE-IDENTICAL with
|
||||
// Python's _canonical_digest (computed via subprocess against the real
|
||||
// oracle). A mismatch here would reject every real approval.
|
||||
func TestCanonicalDigestByteParity(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]interface{}
|
||||
}{
|
||||
{"write_file", map[string]interface{}{"path": "/tmp/x", "content": "hello"}},
|
||||
{"edit_file", map[string]interface{}{"path": "/a b/c.txt", "old": "x\ny", "new": ""}},
|
||||
{"run_command", map[string]interface{}{"command": "ls -la", "cwd": "/tmp"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
payload, _ := json.Marshal(map[string]interface{}{"name": c.name, "arguments": c.args})
|
||||
want := pythonDigest(payload)
|
||||
got := CanonicalDigest(c.name, c.args)
|
||||
if got != want {
|
||||
t.Errorf("%s canonical digest:\n got %s\nwant %s (python)", c.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pythonDigest computes the digest the way Python's json.dumps does
|
||||
// (sort_keys, separators (",",":"), ensure_ascii=False) — computed here in
|
||||
// test-only Python for byte truth.
|
||||
func pythonDigest(payload []byte) string {
|
||||
var m map[string]interface{}
|
||||
json.Unmarshal(payload, &m)
|
||||
script := `import json, hashlib, sys
|
||||
m = json.load(sys.stdin)
|
||||
print(hashlib.sha256(json.dumps(m, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest())`
|
||||
out, err := runPython(script, string(payload))
|
||||
if err != nil {
|
||||
return "PYTHON-UNAVAILABLE"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestPathDigestSemantics — missing/file/other branches.
|
||||
func TestPathDigestSemantics(t *testing.T) {
|
||||
missing := PathDigest("/nonexistent/really/not/here")
|
||||
want := sha256.Sum256([]byte("missing"))
|
||||
if missing != hex.EncodeToString(want[:]) {
|
||||
t.Error("missing path digest must be sha256('missing')")
|
||||
}
|
||||
f := t.TempDir() + "/f.txt"
|
||||
os.WriteFile(f, []byte("content"), 0o644)
|
||||
d1 := PathDigest(f)
|
||||
os.WriteFile(f, []byte("content2"), 0o644)
|
||||
d2 := PathDigest(f)
|
||||
if d1 == d2 {
|
||||
t.Error("file digest must change with content")
|
||||
}
|
||||
if len(d1) != 64 {
|
||||
t.Errorf("path digest length %d, want 64 (full sha256 hex)", len(d1))
|
||||
}
|
||||
_ = math.Pi
|
||||
}
|
||||
|
||||
// runPython — shell out to the real Python interpreter for byte-truth.
|
||||
func runPython(script, stdinData string) (string, error) {
|
||||
cmd := exec.Command("python3", "-c", script)
|
||||
cmd.Stdin = strings.NewReader(stdinData)
|
||||
out, err := cmd.Output()
|
||||
return strings.TrimSpace(string(out)), err
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"fn": "tool_prompt",
|
||||
"name": "full",
|
||||
"expected": {
|
||||
"prompt": "## Available Tools\n\nYou can use these tools by responding with a JSON object:\n```json\n{\"tool\": \"tool_name\", \"args\": {\"param\": \"value\"}}\n```\n\nAfter receiving a tool result, continue your response normally.\n\n### read_file\nRead a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).\n - `path`: Absolute path to the file (required)\n - `offset`: Line number to start from (1-indexed, optional) (optional)\n - `limit`: Max lines to return (default 100, optional) (optional)\n\n### edit_file\nSurgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.\n - `path`: Absolute path to the file (required)\n - `search`: Exact text to find (must match file content) (required)\n - `replace`: Text to replace with (required)\n\n### write_file\nWrite content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.\n - `path`: Absolute path to the file (required)\n - `content`: Content to write (required)\n\n### glob_files\nFind files matching a glob pattern. Returns file paths sorted by modification time.\n - `pattern`: Glob pattern (e.g. '**/*.py', 'src/**/*.ts') (required)\n - `path`: Directory to search in (optional, defaults to cwd) (optional)\n\n### grep_files\nSearch for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.\n - `pattern`: Regex pattern to search for (required)\n - `path`: Directory to search in (optional) (optional)\n - `include`: File pattern to include (e.g. '*.py', '*.{ts,tsx}') (optional)\n\n### list_directory\nList files and directories at a path. Returns names with / suffix for directories.\n - `path`: Absolute path to the directory (required)\n\n### lint_file\nLint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.\n - `path`: Absolute path to the file (required)\n - `language`: Language override (optional, auto-detected from extension) (optional)\n\n### run_tests\nRun tests for a file or project. Auto-detects test runner from language.\n - `path`: File or directory to test (required)\n - `command`: Custom test command (optional, overrides auto-detect) (optional)\n\n### format_code\nAuto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.\n - `path`: File or directory to format (required)\n - `language`: Language override (optional) (optional)\n\n### git_diff\nShow git diff for staged or unstaged changes.\n - `staged`: Show staged changes (default: unstaged) (optional)\n - `path`: Specific file to diff (optional) (optional)\n\n### git_log\nShow recent git commits.\n - `limit`: Number of commits (default 10) (optional)\n - `path`: Specific file history (optional) (optional)\n\n### git_commit\nCommit changes with a message. Auto-stages modified files.\n - `message`: Commit message (required)\n - `files`: Specific files to commit (optional, defaults to all modified) (optional)\n\n### repo_map\nGenerate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.\n - `path`: Root directory to map (required)\n - `languages`: Filter by languages (optional) (optional)\n\n### run_command\nRun an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.\n - `command`: Command text; shell syntax is allowed only with shell=true (optional)\n - `argv`: Argument vector for shell-free execution (optional)\n - `shell`: Explicitly request the user-approved shell execution path (optional)\n - `workdir`: Working directory (optional) (optional)\n - `timeout`: Timeout in seconds (default 30) (optional)\n\n### read_persona\nRead Rune's persona definition. Returns the full persona block with voice, identity, and rules.\n\n### query_memory\nQuery Rune's memory store. Returns relevant memories for a query.\n - `query`: Search query for memory (required)\n - `limit`: Max results (default 5) (optional)\n\n### check_growth\nCheck Rune's growth log. Returns recent growth entries and patterns.\n - `limit`: Max entries (default 10) (optional)\n\n### diagnose_behavior\nDiagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.\n - `text`: Recent response text to analyze (required)\n\n### find_capability\nSearch all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.\n - `query`: What you need to do, e.g. 'run the test suite' or 'commit my changes' (required)\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
// truth_prompt_test.go — batch-6 parity gate: the tool prompt is
|
||||
// byte-identical with build_tool_prompt's output (truth_prompt.json).
|
||||
|
||||
// REPAIR (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown): the
|
||||
// jsonUnmarshal/paramOrder/rawKeyOrder helpers moved to json_order.go —
|
||||
// defining them in a _test.go file starves the non-test build.
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToolPromptByteParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_prompt.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_prompt.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Name string `json:"name"`
|
||||
Expected struct {
|
||||
Prompt string `json:"prompt"`
|
||||
} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := BuildToolPrompt()
|
||||
if got != c.Expected.Prompt {
|
||||
// Find the first divergence for a readable failure.
|
||||
g, w := got, c.Expected.Prompt
|
||||
i := 0
|
||||
for i < len(g) && i < len(w) && g[i] == w[i] {
|
||||
i++
|
||||
}
|
||||
t.Errorf("prompt diverges at byte %d:\n got ...%q\nwant ...%q", i,
|
||||
g[max(0, i-40):min(len(g), i+60)], w[max(0, i-40):min(len(w), i+60)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestRegistryShape — 19 tools, order preserved, required fields present.
|
||||
func TestRegistryShape(t *testing.T) {
|
||||
if len(Tools) != 19 {
|
||||
t.Fatalf("registry: %d tools, want 19", len(Tools))
|
||||
}
|
||||
wantOrder := []string{"read_file", "edit_file", "write_file", "glob_files", "grep_files",
|
||||
"list_directory", "lint_file", "run_tests", "format_code", "git_diff", "git_log",
|
||||
"git_commit", "repo_map", "run_command", "read_persona", "query_memory",
|
||||
"check_growth", "diagnose_behavior", "find_capability"}
|
||||
for i, w := range wantOrder {
|
||||
if Tools[i].Name != w {
|
||||
t.Errorf("order[%d]: got %q want %q", i, Tools[i].Name, w)
|
||||
}
|
||||
}
|
||||
// spot-check required lists
|
||||
if len(Tools[0].Parameters.Required) != 1 || Tools[0].Parameters.Required[0] != "path" {
|
||||
t.Errorf("read_file required: %v", Tools[0].Parameters.Required)
|
||||
}
|
||||
if len(Tools[1].Parameters.Required) != 3 {
|
||||
t.Errorf("edit_file required: %v", Tools[1].Parameters.Required)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "py",
|
||||
"input": {
|
||||
"path": "/x/y/module.py"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "python"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "go",
|
||||
"input": {
|
||||
"path": "/x/main.GO"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "go"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "rust",
|
||||
"input": {
|
||||
"path": "/x/lib.rs"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "rust"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "ts",
|
||||
"input": {
|
||||
"path": "/x/app.tsx"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "typescript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "detect_language",
|
||||
"name": "unknown",
|
||||
"input": {
|
||||
"path": "/x/file.md"
|
||||
},
|
||||
"expected": {
|
||||
"lang": "None"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
// truth_quality_test.go — batch-4 parity gate: language detection + gate
|
||||
// verdicts (live Python oracle for _detect_language; the command runners
|
||||
// are gated on tool availability, so the tests verify the DECISION layer).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatch4Parity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_quality.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_quality.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "detect_language":
|
||||
path, _ := c.Input["path"].(string)
|
||||
got := DetectLanguage(path)
|
||||
want := c.Expected["lang"].(string)
|
||||
if want == "None" {
|
||||
want = ""
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%s: DetectLanguage(%q) = %q want %q", c.Name, path, got, want)
|
||||
}
|
||||
case "lint_no_linter":
|
||||
path, _ := c.Input["path"].(string)
|
||||
ok, result := LintFile(path, "")
|
||||
if !ok || !strings.Contains(result, "No linter configured") {
|
||||
t.Errorf("%s: got (%v, %q)", c.Name, ok, result)
|
||||
}
|
||||
case "tests_no_runner":
|
||||
path, _ := c.Input["path"].(string)
|
||||
ok, result := RunTests(path, "")
|
||||
if ok || !strings.Contains(result, "No test runner configured") {
|
||||
t.Errorf("%s: got (%v, %q)", c.Name, ok, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
module rune/tui
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/lipgloss v0.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.6 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
||||
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
@@ -0,0 +1,127 @@
|
||||
// tui — Rune Terminal TUI in Bubble Tea (R2 W7).
|
||||
// Replaces src/rune/rune_tui.py (4038 lines of Python/Textual).
|
||||
// Chat LEFT, task RIGHT, work pane, status bar — the same layout.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// ── Styles ──
|
||||
|
||||
var (
|
||||
chatStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("135")).
|
||||
Padding(0, 1).
|
||||
Width(40)
|
||||
|
||||
taskStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("39")).
|
||||
Padding(0, 1).
|
||||
Width(40)
|
||||
|
||||
statusStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")).
|
||||
Padding(0, 1)
|
||||
|
||||
messageStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("252"))
|
||||
|
||||
systemStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("245"))
|
||||
)
|
||||
|
||||
// ── Model ──
|
||||
|
||||
type Model struct {
|
||||
chatMessages []ChatDisplay
|
||||
taskMessages []ChatDisplay
|
||||
statusText string
|
||||
width int
|
||||
height int
|
||||
focusedChannel string // "chat" or "task"
|
||||
}
|
||||
|
||||
type ChatDisplay struct {
|
||||
Sender string
|
||||
Content string
|
||||
Timestamp string
|
||||
}
|
||||
|
||||
func initialModel() Model {
|
||||
return Model{
|
||||
chatMessages: []ChatDisplay{
|
||||
{Sender: "system", Content: "Rune TUI started (Go + Rust Survivor hybrid)", Timestamp: time.Now().Format("15:04")},
|
||||
},
|
||||
taskMessages: []ChatDisplay{},
|
||||
statusText: "Rune v1.0.0 | Go runtime | Rust kernels",
|
||||
focusedChannel: "chat",
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "ctrl+1":
|
||||
m.focusedChannel = "chat"
|
||||
case "ctrl+2":
|
||||
m.focusedChannel = "task"
|
||||
case "enter":
|
||||
// In a real implementation, this would send the message to the operator
|
||||
m.chatMessages = append(m.chatMessages, ChatDisplay{
|
||||
Sender: "user", Content: "message sent", Timestamp: time.Now().Format("15:04"),
|
||||
})
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
chatContent := renderChannel(m.chatMessages, "CHAT — conversation")
|
||||
taskContent := renderChannel(m.taskMessages, "TASK — focused work")
|
||||
|
||||
// Chat LEFT, task RIGHT
|
||||
columns := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
chatStyle.Render(chatContent),
|
||||
taskStyle.Render(taskContent),
|
||||
)
|
||||
|
||||
status := statusStyle.Render(fmt.Sprintf("%s | focused: %s", m.statusText, m.focusedChannel))
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, columns, status)
|
||||
}
|
||||
|
||||
func renderChannel(messages []ChatDisplay, title string) string {
|
||||
var lines []string
|
||||
lines = append(lines, title)
|
||||
for _, msg := range messages {
|
||||
lines = append(lines, fmt.Sprintf("[%s] %s: %s", msg.Timestamp, msg.Sender, msg.Content))
|
||||
}
|
||||
if len(lines) == 1 {
|
||||
lines = append(lines, "(no messages)")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
p := tea.NewProgram(initialModel(), tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "TUI error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
// events.go — batch 3: the reactive and proactive dispatch surfaces.
|
||||
// PARITY ports of route_event's event-neuron map and route_proactive's
|
||||
// query assembly. The final `route()` call stays on the caller side (it
|
||||
// needs embeddings + session state — I/O). The pure decision logic — which
|
||||
// keywords an event maps to, which queries system state produces — is here.
|
||||
|
||||
import "strings"
|
||||
|
||||
// EventNeuronMap — events trigger neuron topics reactively.
|
||||
// PARITY: byte-identical to warm_router.py route_event's map.
|
||||
var EventNeuronMap = map[string][]string{
|
||||
"task.completed": {"task", "growth", "lessons"},
|
||||
"task.failed": {"task", "debugging", "error"},
|
||||
"capability.failed": {"debugging", "error", "system"},
|
||||
"owner.correction": {"growth", "lessons", "persona"},
|
||||
"memory.created": {"knowledge", "learning"},
|
||||
"memory.conflict": {"contradiction", "knowledge"},
|
||||
"context.pressure": {"context", "management"},
|
||||
"model.unavailable": {"system", "fallback"},
|
||||
}
|
||||
|
||||
// EventQuery — the pseudo-query an event produces: mapped keywords + event
|
||||
// data as context (truncated to 200 chars, per the Python oracle).
|
||||
func EventQuery(eventType string, eventData string) (string, bool) {
|
||||
keywords, ok := EventNeuronMap[eventType]
|
||||
if !ok || len(keywords) == 0 {
|
||||
return "", false
|
||||
}
|
||||
query := strings.Join(keywords, " ")
|
||||
if eventData != "" {
|
||||
data := eventData
|
||||
if len(data) > 200 {
|
||||
data = data[:200]
|
||||
}
|
||||
query += " " + data
|
||||
}
|
||||
return query, true
|
||||
}
|
||||
|
||||
// ProactiveQueries — the query assembly from system state. PARITY with
|
||||
// route_proactive: top-3 task summaries (100 chars each), errors→diagnostic
|
||||
// query, commitments→reminder query. Commitment listing is caller I/O —
|
||||
// pass hasCommitments.
|
||||
func ProactiveQueries(activeTasks []map[string]interface{}, recentErrors bool, hasCommitments bool) []string {
|
||||
var queries []string
|
||||
if len(activeTasks) > 0 {
|
||||
n := len(activeTasks)
|
||||
if n > 3 {
|
||||
n = 3
|
||||
}
|
||||
for _, task := range activeTasks[:n] {
|
||||
summary, _ := task["summary"].(string)
|
||||
if summary != "" {
|
||||
if len(summary) > 100 {
|
||||
summary = summary[:100]
|
||||
}
|
||||
queries = append(queries, summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if recentErrors {
|
||||
queries = append(queries, "error debugging system")
|
||||
}
|
||||
if hasCommitments {
|
||||
queries = append(queries, "commitment reminder promise")
|
||||
}
|
||||
return queries
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/warm-router
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,171 @@
|
||||
// warm-router — Go memory routing and scoring (R2 W5b deep port).
|
||||
// PARITY port of src/rune/warm_router.py: base_level_from_uses (ACT-R math),
|
||||
// score_neuron, route, and the warm neuron listing.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// warm-router base-level <timestamps-csv> <now-iso>
|
||||
// warm-router score <neuron-json> <text> [--session-context <ctx>]
|
||||
// warm-router route <text> [--max-tokens N]
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const BASE_LEVEL_DECAY = 0.5
|
||||
const DEFAULT_MAX_TOKENS = 2048
|
||||
|
||||
// ── ACT-R base level activation (parity: warm_router.py:142) ──
|
||||
|
||||
func baseLevelFromUses(usedAtList []string, nowISO string) float64 {
|
||||
if len(usedAtList) == 0 {
|
||||
return -2.0 // Cold start — never fired
|
||||
}
|
||||
|
||||
now, err := time.Parse("2006-01-02T15:04:05Z", nowISO)
|
||||
if err != nil {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, usedAtStr := range usedAtList {
|
||||
usedAt, err := time.Parse("2006-01-02T15:04:05Z07:00", strings.Replace(usedAtStr, "Z", "+00:00", 1))
|
||||
if err != nil {
|
||||
// Try without timezone
|
||||
usedAt, err = time.Parse("2006-01-02T15:04:05", usedAtStr)
|
||||
if err != nil {
|
||||
continue // skip parse failures like the Python original
|
||||
}
|
||||
}
|
||||
hours := math.Max(0.1, now.Sub(usedAt).Hours())
|
||||
total += math.Pow(hours, -BASE_LEVEL_DECAY)
|
||||
}
|
||||
|
||||
if total <= 0 {
|
||||
return -2.0
|
||||
}
|
||||
|
||||
bi := math.Log(total)
|
||||
// Clamp to prevent extreme values
|
||||
return math.Max(-2.0, math.Min(bi, 2.0))
|
||||
}
|
||||
|
||||
// ── Neuron scoring (parity: warm_router.py:487) ──
|
||||
|
||||
type WarmNeuron struct {
|
||||
Slug string `json:"slug"`
|
||||
Triggers []string `json:"triggers"`
|
||||
Text string `json:"text"`
|
||||
Importance float64 `json:"importance"`
|
||||
WashCount int `json:"wash_count"`
|
||||
ID int `json:"id"`
|
||||
BodyKnowledge string `json:"body_knowledge"`
|
||||
}
|
||||
|
||||
func scoreNeuron(neuron WarmNeuron, text string) map[string]interface{} {
|
||||
lower := strings.ToLower(text)
|
||||
total := 0.0
|
||||
keywordHits := 0
|
||||
|
||||
for _, trigger := range neuron.Triggers {
|
||||
if strings.Contains(lower, strings.ToLower(trigger)) {
|
||||
keywordHits++
|
||||
total += 0.5
|
||||
}
|
||||
}
|
||||
|
||||
// Importance boost
|
||||
total += neuron.Importance * 0.3
|
||||
|
||||
// Wash count boost (proven content)
|
||||
if neuron.WashCount >= 20 {
|
||||
total += 0.3
|
||||
} else if neuron.WashCount >= 10 {
|
||||
total += 0.15
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"slug": neuron.Slug,
|
||||
"score": total,
|
||||
"keyword_hits": keywordHits,
|
||||
"importance": neuron.Importance,
|
||||
"wash_count": neuron.WashCount,
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLI ──
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: warm-router <base-level|score|route> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
switch cmd {
|
||||
case "base-level":
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: warm-router base-level <timestamps-csv> <now-iso>")
|
||||
os.Exit(1)
|
||||
}
|
||||
timestamps := strings.Split(args[0], ",")
|
||||
nowISO := args[1]
|
||||
result := baseLevelFromUses(timestamps, nowISO)
|
||||
out, _ := json.Marshal(map[string]float64{"base_level": result})
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "score":
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: warm-router score <neuron-json> <text>")
|
||||
os.Exit(1)
|
||||
}
|
||||
var neuron WarmNeuron
|
||||
json.Unmarshal([]byte(args[0]), &neuron)
|
||||
text := strings.Join(args[1:], " ")
|
||||
result := scoreNeuron(neuron, text)
|
||||
out, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "route":
|
||||
text := ""
|
||||
maxTokens := DEFAULT_MAX_TOKENS
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] == "--max-tokens" && i+1 < len(args) {
|
||||
fmt.Sscanf(args[i+1], "%d", &maxTokens)
|
||||
i++
|
||||
} else if text == "" {
|
||||
text = args[i]
|
||||
}
|
||||
}
|
||||
topics := extractTopics(text)
|
||||
out, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"text": text,
|
||||
"topics": topics,
|
||||
"max_tokens": maxTokens,
|
||||
}, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func extractTopics(text string) []string {
|
||||
words := strings.Fields(strings.ToLower(text))
|
||||
var topics []string
|
||||
for _, w := range words {
|
||||
w = strings.TrimRight(w, ".,!?;:\"'()[]{}")
|
||||
if len([]rune(w)) > 3 {
|
||||
topics = append(topics, w)
|
||||
}
|
||||
}
|
||||
return topics
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
// neurons.go — batch 5: neuron listing mechanics. PARITY ports of
|
||||
// _parse_trigger and list_neurons' body-knowledge construction. The SQL
|
||||
// read stays caller-side (store seam); these are the pure transforms.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseTrigger — comma-split, trim, lowercase; empty parts dropped.
|
||||
// PARITY NOTE: the Python oracle does NOT special-case JSON arrays —
|
||||
// '["a","b"]' parses as the literal parts '["a"' and '"b"]'. This port
|
||||
// reproduces that exact behavior (fixture-verified).
|
||||
func ParseTrigger(raw string) []string {
|
||||
var out []string
|
||||
for _, t := range strings.Split(raw, ",") {
|
||||
t = strings.TrimSpace(strings.ToLower(t))
|
||||
if t != "" {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BodyKnowledge — the experiential context line (Souveraine enhancement).
|
||||
// PARITY: fires-on prefix (first 5 triggers), knows prefix (80-char
|
||||
// summary), then the wash-count lifecycle note.
|
||||
func BodyKnowledge(triggerList []string, summary string, washCount int) string {
|
||||
bk := ""
|
||||
if len(triggerList) > 0 {
|
||||
n := len(triggerList)
|
||||
if n > 5 {
|
||||
n = 5
|
||||
}
|
||||
bk = "Fires on: " + strings.Join(triggerList[:n], ", ") + ". "
|
||||
}
|
||||
if summary != "" {
|
||||
s := summary
|
||||
if len(s) > 80 {
|
||||
s = s[:80]
|
||||
}
|
||||
bk += "Knows: " + s + ". "
|
||||
}
|
||||
switch {
|
||||
case washCount >= 20:
|
||||
bk += "Proven — has survived many washes."
|
||||
case washCount >= 10:
|
||||
bk += "Established — building trust."
|
||||
case washCount > 0:
|
||||
bk += "Testing — still proving itself."
|
||||
default:
|
||||
bk += "Untested — new or rarely accessed."
|
||||
}
|
||||
return strings.TrimSpace(bk)
|
||||
}
|
||||
|
||||
// NeuronTokens — the estimated token cost (len/4, min 1).
|
||||
func NeuronTokens(fullBody string) int {
|
||||
t := len(fullBody) / 4
|
||||
if t < 1 {
|
||||
t = 1
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
// priming.go — batch 4: the ACTIVE warm-tier priming mechanics. PARITY
|
||||
// ports of warm_router.py's _prime_neurons (record + bloat guard),
|
||||
// _decay_primes (recency decay), and the session-state prune rules.
|
||||
//
|
||||
// Constants: PRIME_DECAY=0.6, PRIME_FLOOR=0.15, ASSOC_PRIME_WEIGHT=0.5,
|
||||
// 24h idle eviction, hard cap 40 sessions (evict fewest turns).
|
||||
//
|
||||
// State persistence (load/save_state) stays caller-side — this file owns
|
||||
// the pure decisions.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
const PrimeDecay = 0.6
|
||||
const PrimeFloor = 0.15
|
||||
const AssocPrimeWeight = 0.5
|
||||
const MaxSessions = 40
|
||||
|
||||
// SessionState — one session's priming record.
|
||||
type SessionState struct {
|
||||
Primed map[string]float64 `json:"primed"`
|
||||
Turns int `json:"turns"`
|
||||
LastActive string `json:"last_active"`
|
||||
}
|
||||
|
||||
// DecayPrimes — decay prior priming before each new turn. Mutates the map;
|
||||
// returns the surviving slugs sorted by score (descending, parity order).
|
||||
func DecayPrimes(primed map[string]float64) []string {
|
||||
for slug, score := range primed {
|
||||
decayed := score * PrimeDecay
|
||||
if decayed < PrimeFloor {
|
||||
delete(primed, slug)
|
||||
} else {
|
||||
primed[slug] = decayed
|
||||
}
|
||||
}
|
||||
slugs := make([]string, 0, len(primed))
|
||||
for slug := range primed {
|
||||
slugs = append(slugs, slug)
|
||||
}
|
||||
sort.Slice(slugs, func(i, j int) bool { return primed[slugs[i]] > primed[slugs[j]] })
|
||||
return slugs
|
||||
}
|
||||
|
||||
// PrimeNeurons — record fired slugs (max-score semantics) + turn increment.
|
||||
// Mutates sess in place; the caller persists.
|
||||
func PrimeNeurons(sess *SessionState, slugs []string, score float64, now time.Time) {
|
||||
if sess.Primed == nil {
|
||||
sess.Primed = map[string]float64{}
|
||||
}
|
||||
sess.Turns++
|
||||
sess.LastActive = now.UTC().Format("2006-01-02T15:04:05")
|
||||
for _, slug := range slugs {
|
||||
if cur, ok := sess.Primed[slug]; !ok || score > cur {
|
||||
sess.Primed[slug] = score
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PruneSessions — the bloat guard: drop empty sessions, evict idle >24h,
|
||||
// hard-cap at 40 (fewest turns evicted first). Mutates in place.
|
||||
func PruneSessions(sessions map[string]*SessionState, now time.Time) {
|
||||
var stale []string
|
||||
for s, v := range sessions {
|
||||
if len(v.Primed) == 0 {
|
||||
stale = append(stale, s)
|
||||
continue
|
||||
}
|
||||
if last, err := time.Parse("2006-01-02T15:04:05", v.LastActive); err == nil {
|
||||
if now.Sub(last).Hours() > 24 {
|
||||
stale = append(stale, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, s := range stale {
|
||||
delete(sessions, s)
|
||||
}
|
||||
if len(sessions) > MaxSessions {
|
||||
type kv struct {
|
||||
key string
|
||||
turns int
|
||||
}
|
||||
var all []kv
|
||||
for s, v := range sessions {
|
||||
all = append(all, kv{s, v.Turns})
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].turns < all[j].turns })
|
||||
for _, kv := range all[:len(sessions)-MaxSessions] {
|
||||
delete(sessions, kv.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
// route_alloc.go — the pure budget-allocation slice of route(). PARITY with
|
||||
// warm_router.py's fired-loop decision: the top-scoring relevant neuron
|
||||
// fires its FULL body (relevance outranks budget — what the user asked
|
||||
// about is never compressed); everything after fires its AAAK summary when
|
||||
// available; token cost = max(1, len(body)/4); over-budget neurons are
|
||||
// SKIPPED (not truncated — the next candidate gets the chance).
|
||||
//
|
||||
// The I/O parts of route() (embedding gather, session state, priming,
|
||||
// association walks, use_history writes) stay on the caller side — the
|
||||
// dichotomy: Go owns the reactive loop, the pure decision is here.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// RouteCandidate — one scored neuron entering the allocation.
|
||||
type RouteCandidate struct {
|
||||
Slug string
|
||||
Body string
|
||||
Summary string // "" = no summary (full body always)
|
||||
Score float64
|
||||
}
|
||||
|
||||
// FiredNeuron — the allocation verdict for one neuron.
|
||||
type FiredNeuron struct {
|
||||
Slug string
|
||||
Body string
|
||||
Score float64
|
||||
Tokens int
|
||||
Compressed bool
|
||||
Primed bool
|
||||
}
|
||||
|
||||
// RouteAllocate — the budget-constrained firing decision.
|
||||
func RouteAllocate(cands []RouteCandidate, maxTokens int) ([]FiredNeuron, int) {
|
||||
sorted := make([]RouteCandidate, len(cands))
|
||||
copy(sorted, cands)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Score > sorted[j].Score })
|
||||
|
||||
var fired []FiredNeuron
|
||||
used := 0
|
||||
topFired := false
|
||||
for _, n := range sorted {
|
||||
if n.Score <= 0 {
|
||||
continue
|
||||
}
|
||||
body := n.Body
|
||||
if topFired && n.Summary != "" {
|
||||
body = n.Summary
|
||||
}
|
||||
tokens := len(body) / 4
|
||||
if tokens < 1 {
|
||||
tokens = 1
|
||||
}
|
||||
if len(fired) > 0 && used+tokens > maxTokens {
|
||||
continue
|
||||
}
|
||||
fired = append(fired, FiredNeuron{
|
||||
Slug: n.Slug, Body: body, Score: n.Score, Tokens: tokens,
|
||||
Compressed: body != n.Body,
|
||||
})
|
||||
used += tokens
|
||||
topFired = true
|
||||
}
|
||||
return fired, used
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
// scoring.go — the warm-router's deterministic scoring core. PARITY ports
|
||||
// of warm_router.py's _py_score_gate and _py_score_finish (the H3 slice-3
|
||||
// pure oracles). I/O signals (embeddings, SQLite base-level, cache file)
|
||||
// arrive as arguments — the caller gathers them; this code is pure.
|
||||
//
|
||||
// Truth battery: truth_gate.json (live Python oracle, 2026-10-07).
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScoreGate — pure gate: (fires, total, keywordHits).
|
||||
// Signals 1 (keyword) and 3 (context) are pure; signal 2 (semantic) arrives
|
||||
// as an argument because embeddings are I/O.
|
||||
func ScoreGate(neuron WarmNeuron, lowText string, sessionContext string, semanticScore float64) (bool, float64, int) {
|
||||
keywordHits := 0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && strings.Contains(lowText, t) {
|
||||
keywordHits++
|
||||
}
|
||||
}
|
||||
|
||||
contextScore := 0.0
|
||||
if sessionContext != "" {
|
||||
contextLow := strings.ToLower(sessionContext)
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && strings.Contains(contextLow, t) {
|
||||
contextScore += 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GATED COMBINATION: keywords are the primary gate; semantic is a
|
||||
// secondary boost (>0.3); context provides turn continuity.
|
||||
if keywordHits > 0 {
|
||||
keywordComponent := float64(keywordHits) * 1.0
|
||||
semanticBoost := 0.0
|
||||
if semanticScore > 0.3 {
|
||||
semanticBoost = math.Max(0, semanticScore-0.3) * 0.5
|
||||
}
|
||||
contextComponent := math.Min(contextScore, 1.0)
|
||||
return true, keywordComponent + semanticBoost + contextComponent, keywordHits
|
||||
} else if semanticScore > 0.55 {
|
||||
// No keywords, strong semantic — secondary gate (higher threshold).
|
||||
return true, semanticScore * 0.7, keywordHits
|
||||
}
|
||||
return false, 0.0, keywordHits
|
||||
}
|
||||
|
||||
// ScoreFinish — pure finish: bonuses, penalties, protected boost atop total.
|
||||
// b_i / cachePriority (nil = skip) arrive as arguments: SQLite and the cache
|
||||
// file are I/O — the wrapper only gathers them on the fired path.
|
||||
func ScoreFinish(neuron WarmNeuron, lowText string, total float64, bi *float64, cachePriority *float64, keywordHits int) float64 {
|
||||
score := total + neuron.Importance
|
||||
|
||||
// Signal 4: base-level learning contribution (capped at 0.3).
|
||||
if neuron.ID != 0 && bi != nil {
|
||||
bContribution := math.Max(0, math.Min(*bi*0.1, 0.3))
|
||||
score += bContribution
|
||||
}
|
||||
|
||||
// Signal 5: wash count (drift-with-integrity lifecycle).
|
||||
if neuron.WashCount > 0 {
|
||||
washBoost := math.Min(0.3, math.Log1p(float64(neuron.WashCount))/math.Log1p(20)*0.3)
|
||||
score += washBoost
|
||||
}
|
||||
|
||||
// Signal 6: cache priority.
|
||||
if neuron.ID != 0 && cachePriority != nil {
|
||||
score += math.Min(0.2, *cachePriority*0.1)
|
||||
}
|
||||
|
||||
// Signal 7: body-knowledge relevance.
|
||||
if neuron.BodyKnowledge != "" && lowText != "" {
|
||||
bkLower := strings.ToLower(neuron.BodyKnowledge)
|
||||
qWords := strings.Fields(strings.ToLower(lowText))
|
||||
bkWords := strings.Fields(bkLower)
|
||||
overlap := 0
|
||||
for _, qw := range qWords {
|
||||
for _, bw := range bkWords {
|
||||
if qw == bw {
|
||||
overlap++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if overlap > 0 {
|
||||
score += math.Min(0.15, float64(overlap)*0.03)
|
||||
}
|
||||
}
|
||||
|
||||
// FAN EFFECT PENALTY (ACT-R, Anderson 1974): broad neurons dilute.
|
||||
triggerCount := 0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" {
|
||||
triggerCount++
|
||||
}
|
||||
}
|
||||
if triggerCount > 10 {
|
||||
fanPenalty := math.Max(0.5, 1.0-float64(triggerCount-10)*0.1)
|
||||
score *= fanPenalty
|
||||
}
|
||||
|
||||
// Length bonus for keyword matches.
|
||||
if keywordHits > 0 {
|
||||
lengthBonus := 0.0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && len(t) > 6 && strings.Contains(lowText, t) {
|
||||
lengthBonus += 0.25
|
||||
}
|
||||
}
|
||||
score += lengthBonus
|
||||
}
|
||||
|
||||
// Protected neurons get a boost.
|
||||
if ProtectedNeurons[neuron.Slug] {
|
||||
score += ProtectedBoost
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// The protected set + boost — PARITY with warm_router.py:60-61.
|
||||
var ProtectedNeurons = map[string]bool{"persona-alfred": true, "philosophy-sagan-core": true}
|
||||
|
||||
const ProtectedBoost = 4.0
|
||||
@@ -0,0 +1,99 @@
|
||||
[
|
||||
{
|
||||
"fn": "event_map",
|
||||
"name": "full_map",
|
||||
"expected": {
|
||||
"map": {
|
||||
"task.completed": [
|
||||
"task",
|
||||
"growth",
|
||||
"lessons"
|
||||
],
|
||||
"task.failed": [
|
||||
"task",
|
||||
"debugging",
|
||||
"error"
|
||||
],
|
||||
"capability.failed": [
|
||||
"debugging",
|
||||
"error",
|
||||
"system"
|
||||
],
|
||||
"owner.correction": [
|
||||
"growth",
|
||||
"lessons",
|
||||
"persona"
|
||||
],
|
||||
"memory.created": [
|
||||
"knowledge",
|
||||
"learning"
|
||||
],
|
||||
"memory.conflict": [
|
||||
"contradiction",
|
||||
"knowledge"
|
||||
],
|
||||
"context.pressure": [
|
||||
"context",
|
||||
"management"
|
||||
],
|
||||
"model.unavailable": [
|
||||
"system",
|
||||
"fallback"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "event_query",
|
||||
"name": "known",
|
||||
"input": {
|
||||
"event_type": "task.failed",
|
||||
"event_data": "build 19571"
|
||||
},
|
||||
"expected": {
|
||||
"query": "task debugging error build 19571"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "event_query",
|
||||
"name": "unknown",
|
||||
"input": {
|
||||
"event_type": "not.an.event"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "event_query",
|
||||
"name": "truncated_data",
|
||||
"input": {
|
||||
"event_type": "memory.created",
|
||||
"event_data": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||
},
|
||||
"expected": {
|
||||
"query": "knowledge learning dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "proactive_queries",
|
||||
"name": "full",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"queries": [
|
||||
"TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT",
|
||||
"second task",
|
||||
"error debugging system",
|
||||
"commitment reminder promise"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "proactive_queries",
|
||||
"name": "empty",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"queries": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
// truth_event_test.go — batch-3 parity gate: the event map and proactive
|
||||
// query assembly against truth_event.json (live Python oracle, 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEventMapParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_event.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_event.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Name {
|
||||
case "full_map":
|
||||
// The map must be byte-identical: same keys, same keyword lists.
|
||||
want := c.Expected["map"].(map[string]interface{})
|
||||
if len(want) != len(EventNeuronMap) {
|
||||
t.Fatalf("event map size: got %d want %d", len(EventNeuronMap), len(want))
|
||||
}
|
||||
for event, kws := range want {
|
||||
got, ok := EventNeuronMap[event]
|
||||
if !ok {
|
||||
t.Errorf("missing event %q", event)
|
||||
continue
|
||||
}
|
||||
wl := kws.([]interface{})
|
||||
if len(got) != len(wl) {
|
||||
t.Errorf("%s: got %v want %v", event, got, wl)
|
||||
continue
|
||||
}
|
||||
for i, kw := range wl {
|
||||
if got[i] != kw.(string) {
|
||||
t.Errorf("%s[%d]: got %q want %q", event, i, got[i], kw.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "known":
|
||||
et, _ := c.Input["event_type"].(string)
|
||||
ed, _ := c.Input["event_data"].(string)
|
||||
q, ok := EventQuery(et, ed)
|
||||
if !ok {
|
||||
t.Errorf("%s: event should be known", c.Name)
|
||||
}
|
||||
if q != c.Expected["query"].(string) {
|
||||
t.Errorf("%s: got %q want %q", c.Name, q, c.Expected["query"])
|
||||
}
|
||||
case "unknown":
|
||||
et, _ := c.Input["event_type"].(string)
|
||||
_, ok := EventQuery(et, "")
|
||||
if ok {
|
||||
t.Errorf("%s: unknown event must not produce a query", c.Name)
|
||||
}
|
||||
case "truncated_data":
|
||||
et, _ := c.Input["event_type"].(string)
|
||||
ed, _ := c.Input["event_data"].(string)
|
||||
q, ok := EventQuery(et, ed)
|
||||
if !ok {
|
||||
t.Errorf("%s: event should be known", c.Name)
|
||||
}
|
||||
if q != c.Expected["query"].(string) {
|
||||
t.Errorf("%s: got len %d want len %d", c.Name, len(q), len(c.Expected["query"].(string)))
|
||||
}
|
||||
case "full":
|
||||
// ProactiveQueries with tasks + errors + commitments.
|
||||
tasks := []map[string]interface{}{
|
||||
{"summary": strings.Repeat("T", 150)},
|
||||
{"summary": "second task"},
|
||||
}
|
||||
q := ProactiveQueries(tasks, true, true)
|
||||
want := c.Expected["queries"].([]interface{})
|
||||
if len(q) != len(want) {
|
||||
t.Errorf("%s: got %d queries want %d", c.Name, len(q), len(want))
|
||||
continue
|
||||
}
|
||||
for i := range want {
|
||||
if q[i] != want[i].(string) {
|
||||
t.Errorf("%s[%d]: got %q want %q", c.Name, i, q[i], want[i].(string))
|
||||
}
|
||||
}
|
||||
case "empty":
|
||||
q := ProactiveQueries(nil, false, false)
|
||||
if len(q) != 0 {
|
||||
t.Errorf("%s: got %v want empty", c.Name, q)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
[
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "kw_hit",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 2.0,
|
||||
"kw": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "no_fire",
|
||||
"expected": {
|
||||
"fires": false,
|
||||
"total": 0.0,
|
||||
"kw": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "sem_only",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 1.25,
|
||||
"kw": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "ctx",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 1.3,
|
||||
"kw": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fin_full",
|
||||
"expected": {
|
||||
"score": 3.14
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fin_min",
|
||||
"expected": {
|
||||
"score": 2.55
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fan_penalty",
|
||||
"expected": {
|
||||
"score": 1.26
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
// truth_gate_test.go — parity gate for the scoring core. Replays
|
||||
// truth_gate.json (live Python oracle, 2026-10-07) through ScoreGate and
|
||||
// ScoreFinish.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScoreParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_gate.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_gate.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
neuron := WarmNeuron{
|
||||
Slug: "test-neuron", Triggers: []string{"spine", "rebuild"},
|
||||
Importance: 0.8, ID: 42,
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Name {
|
||||
case "kw_hit":
|
||||
fires, total, kw := ScoreGate(neuron, "the spine rebuild verified parity", "", 0.0)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("kw_hit: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "no_fire":
|
||||
fires, total, kw := ScoreGate(neuron, "something unrelated", "", 0.2)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("no_fire: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "sem_only":
|
||||
fires, total, kw := ScoreGate(neuron, "the spinal column rebuild", "", 0.8)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("sem_only: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "ctx":
|
||||
fires, total, kw := ScoreGate(neuron, "spine", "earlier we discussed the spine in detail", 0.0)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("ctx: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "fin_full":
|
||||
bi := 0.5
|
||||
cp := 0.4
|
||||
got := ScoreFinish(neuron, "the spine rebuild verified parity", 2.0, &bi, &cp, 2)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fin_full: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
case "fin_min":
|
||||
got := ScoreFinish(neuron, "the spine rebuild verified parity", 1.5, nil, nil, 1)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fin_min: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
case "fan_penalty":
|
||||
n2 := neuron
|
||||
triggers := []string{}
|
||||
for i := 0; i < 12; i++ {
|
||||
triggers = append(triggers, "t"+string(rune('0'+i%10)))
|
||||
}
|
||||
triggers = append(triggers, "spine")
|
||||
n2.Triggers = triggers
|
||||
got := ScoreFinish(n2, "spine appears", 1.0, nil, nil, 1)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fan_penalty: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown truth case %q", c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProtectedBoost — the persona anchors must outscore everything on
|
||||
// their topic (that is the entire point of the boost).
|
||||
func TestProtectedBoost(t *testing.T) {
|
||||
protected := WarmNeuron{Slug: "persona-alfred", Triggers: []string{"composure"}, Importance: 0.8}
|
||||
ordinary := WarmNeuron{Slug: "ordinary", Triggers: []string{"composure"}, Importance: 0.8}
|
||||
p := ScoreFinish(protected, "composure under pressure", 1.0, nil, nil, 1)
|
||||
o := ScoreFinish(ordinary, "composure under pressure", 1.0, nil, nil, 1)
|
||||
if p-o != ProtectedBoost {
|
||||
t.Errorf("protected delta = %v, want %v", p-o, ProtectedBoost)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
[
|
||||
{
|
||||
"fn": "parse_trigger",
|
||||
"name": "comma",
|
||||
"input": {
|
||||
"raw": "spine, rebuild, parity"
|
||||
},
|
||||
"expected": {
|
||||
"triggers": [
|
||||
"spine",
|
||||
"rebuild",
|
||||
"parity"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse_trigger",
|
||||
"name": "json",
|
||||
"input": {
|
||||
"raw": "[\"a\",\"b\"]"
|
||||
},
|
||||
"expected": {
|
||||
"triggers": [
|
||||
"[\"a\"",
|
||||
"\"b\"]"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "parse_trigger",
|
||||
"name": "empty",
|
||||
"input": {
|
||||
"raw": ""
|
||||
},
|
||||
"expected": {
|
||||
"triggers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "body_knowledge",
|
||||
"name": "proven",
|
||||
"input": {
|
||||
"triggers": [
|
||||
"spine",
|
||||
"rebuild"
|
||||
],
|
||||
"summary": "the spine mechanics",
|
||||
"wc": 20
|
||||
},
|
||||
"expected": {
|
||||
"bk": "Fires on: spine, rebuild. Knows: the spine mechanics. Proven \u2014 has survived many washes."
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "body_knowledge",
|
||||
"name": "untested",
|
||||
"input": {
|
||||
"triggers": [],
|
||||
"summary": "",
|
||||
"wc": 0
|
||||
},
|
||||
"expected": {
|
||||
"bk": "Untested \u2014 new or rarely accessed."
|
||||
}
|
||||
}
|
||||
]
|
||||
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user