Compare commits
154
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84b8d6b215 | ||
|
|
7f5d57236a | ||
|
|
bda07c68ee | ||
|
|
c5be22be09 | ||
|
|
4d6fec3d17 | ||
|
|
15dbbc562a | ||
|
|
6eb5e06119 | ||
|
|
78e4750a77 | ||
|
|
c3948bae98 | ||
|
|
b8184a0f01 | ||
|
|
469b91f217 | ||
|
|
bb960b1656 | ||
|
|
38c76a0c29 | ||
|
|
3186566514 | ||
|
|
24d14ff8e1 | ||
|
|
6d0d455033 | ||
|
|
b0a682b61f | ||
|
|
ab18540d7e | ||
|
|
f489467e1b | ||
|
|
64172d8675 | ||
|
|
8e75414907 | ||
|
|
7a9d823792 | ||
|
|
98b5a73ed1 | ||
|
|
4a9d8001c1 | ||
|
|
538bbeb821 | ||
|
|
cc3b3d81eb | ||
|
|
eb01156d9f | ||
|
|
290d264f31 | ||
|
|
16c147dbee | ||
|
|
15cba0bb24 | ||
|
|
1283b0bf94 | ||
|
|
1ee35d76c9 | ||
|
|
e8daf78eaa | ||
|
|
19976c310c | ||
|
|
e2430a9154 | ||
|
|
de5445357e | ||
|
|
e94c8e27a8 | ||
|
|
0816c5997d | ||
|
|
b502a1d7e8 | ||
|
|
0cd587dfd3 | ||
|
|
8404cddb1d | ||
|
|
17ec4862e3 | ||
|
|
6b712a9b75 | ||
|
|
31f49671ec | ||
|
|
103a42103b | ||
|
|
615da6bd09 | ||
|
|
693a494884 | ||
|
|
ee653c6a4c | ||
|
|
171f3fb250 | ||
|
|
9731181a6b | ||
|
|
544c19719e | ||
|
|
d571658b5a | ||
|
|
50c997beab | ||
|
|
9e6215e605 | ||
|
|
6429c5bb49 | ||
|
|
ea34f2d5ab | ||
|
|
0eff96a335 | ||
|
|
de68444e50 | ||
|
|
d58e88da5c | ||
|
|
c730196dba | ||
|
|
c750513fab | ||
|
|
27c4bf8d78 | ||
|
|
5b8023ca2a | ||
|
|
57eaa8f383 | ||
|
|
567fd39937 | ||
|
|
817a97abd9 | ||
|
|
1e6cb4d1ba | ||
|
|
4e11aa4268 | ||
|
|
1703c25367 | ||
|
|
7cc47de6de | ||
|
|
e93315d156 | ||
|
|
be49fb32d1 | ||
|
|
c41ed97373 | ||
|
|
558401844e | ||
|
|
26e4929b10 | ||
|
|
1462262476 | ||
|
|
0bf47265c8 | ||
|
|
02a9dc4a31 | ||
|
|
0f3aeff58b | ||
|
|
ff2abcd502 | ||
|
|
b61af7a526 | ||
|
|
497d14a449 | ||
|
|
2e68b6b054 | ||
|
|
969e39f6a3 | ||
|
|
273267763e | ||
|
|
8973caae77 | ||
|
|
3450a1cb83 | ||
|
|
9cb93e6573 | ||
|
|
4b0d1d24d2 | ||
|
|
114c394741 | ||
|
|
7c68e41a63 | ||
|
|
2ac2d0aefd | ||
|
|
6f44a87894 | ||
|
|
485623dbc4 | ||
|
|
43e7f5f7d2 | ||
|
|
54d2db97b0 | ||
|
|
523a859656 | ||
|
|
9b765852b8 | ||
|
|
f1e63b703e | ||
|
|
6d40f94f18 | ||
|
|
7a869476be | ||
|
|
e5292cce7b | ||
|
|
396ae120dc | ||
|
|
01b57297b8 | ||
|
|
497db5a144 | ||
|
|
9425ccc1f4 | ||
|
|
3ed547f86f | ||
|
|
5bc837c232 | ||
|
|
08bc1a6b62 | ||
|
|
201ca05b56 | ||
|
|
5cd90f2104 | ||
|
|
c8bc454e33 | ||
|
|
b81edb2029 | ||
|
|
87a890dce3 | ||
|
|
a0821be8a5 | ||
|
|
cb137bd37a | ||
|
|
72afb1a55a | ||
|
|
c9095f5af5 | ||
|
|
b5422f0772 | ||
|
|
da36b99184 | ||
|
|
873d0025ad | ||
|
|
a9ce967557 | ||
|
|
eb3a2c681c | ||
|
|
1bc1cdeaf0 | ||
|
|
31565844d7 | ||
|
|
c08b211f0e | ||
|
|
46c5032208 | ||
|
|
92584daeef | ||
|
|
bb5e243c88 | ||
|
|
ac936a8ba9 | ||
|
|
e6c8c5444d | ||
|
|
11270cb06a | ||
|
|
f8a568714b | ||
|
|
c3f81e60ec | ||
|
|
efb99d4682 | ||
|
|
489718ab1b | ||
|
|
9db580ac6d | ||
|
|
80ed978fbf | ||
|
|
3f119e50d8 | ||
|
|
517379313b | ||
|
|
643f9aa9d1 | ||
|
|
61a5082633 | ||
|
|
9121bbd8b0 | ||
|
|
cba9045982 | ||
|
|
0c92f482a7 | ||
|
|
3120e8ae08 | ||
|
|
ea3c20e205 | ||
|
|
a93327d107 | ||
|
|
e08625f629 | ||
|
|
021fef2065 | ||
|
|
3788875410 | ||
|
|
d96b89bd4c | ||
|
|
fe45c6a8dd | ||
|
|
c7b2edd4fc |
No files matched your search
@@ -15,6 +15,9 @@
|
||||
# Canary files are omitted: src/rune/canary_*.py are standalone CI
|
||||
# verification scripts executed directly by the canary job, never imported by
|
||||
# the test suite — counting them would make the global floor a fiction.
|
||||
# ffi_query_memory_smoke.py is the same class: a standalone evidence script
|
||||
# driven directly (and by tests/test_ffi_query_memory_smoke.py when the cdylib
|
||||
# is built), never imported by the suite.
|
||||
|
||||
[run]
|
||||
source = src/rune
|
||||
@@ -22,6 +25,7 @@ omit =
|
||||
tests/*
|
||||
scripts/*
|
||||
src/rune/canary_*.py
|
||||
src/rune/ffi_query_memory_smoke.py
|
||||
|
||||
[report]
|
||||
fail_under = 29
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Fix a bug or issue
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what's broken and why it matters. -->
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after: "Doing X currently produces Y; it should produce Z." -->
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
@@ -29,10 +59,6 @@ assignees: conductor
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — DESCRIPTION GATE v4 format (plain summary + pipeline requirements)
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what this does and why it matters. Nick should
|
||||
understand the goal without decoding internals. -->
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete example of the intended behavior, in plain terms:
|
||||
"If X happens, the system will Y instead of Z." -->
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
@@ -37,7 +63,6 @@ DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
@@ -69,4 +94,4 @@ Hard invariants: derived cognition preserves evidentiary ancestry · epistemic c
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
backup:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
+104
-22
@@ -11,8 +11,8 @@ jobs:
|
||||
# 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'
|
||||
runs-on: rune-infra-pi
|
||||
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
|
||||
@@ -23,12 +23,13 @@ jobs:
|
||||
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) {
|
||||
if (has.length === 1) {
|
||||
// Prioritized — clear the flag if it was set.
|
||||
if (labels.includes('unprioritized')) {
|
||||
await github.rest.issues.removeLabel({
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, name: 'unprioritized',
|
||||
issue_number: issue.number,
|
||||
id: issue.labels.find(l => l.name === 'unprioritized').id,
|
||||
}).catch(e => { if (e.status !== 404) throw e; });
|
||||
}
|
||||
return;
|
||||
@@ -44,12 +45,26 @@ jobs:
|
||||
});
|
||||
|
||||
auto-label:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Analyze and label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// CLOSED-ISSUE GUARD (#407): labels are filing-time metadata.
|
||||
// A closed issue's lifecycle has ended; re-running label
|
||||
// inference on it re-adds stale type labels (bug = currently
|
||||
// incorrect behavior — a closed fixed issue must not carry
|
||||
// it) and makes any human label cleanup self-defeating,
|
||||
// because every PUT fires a 'labeled' event that re-triggers
|
||||
// this job. Post-close label curation belongs to humans and
|
||||
// the conductor, not this workflow.
|
||||
const issueState = context.payload.issue?.state;
|
||||
if (issueState === 'closed') {
|
||||
console.log(`Issue #${context.payload.issue?.number} is closed — auto-label skips (labels are curated post-close).`);
|
||||
return;
|
||||
}
|
||||
const action = context.payload.action;
|
||||
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();
|
||||
@@ -58,9 +73,44 @@ jobs:
|
||||
const managedLabels = new Set([
|
||||
'memory', 'governance', 'identity', 'growth',
|
||||
'perception', 'infrastructure', 'life', 'custodian',
|
||||
'priority: high', 'bug', 'feature',
|
||||
'bug', 'feature', 'research',
|
||||
'P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low',
|
||||
]);
|
||||
|
||||
// Type label: DECLARED over GUESSED. The template spine
|
||||
// (kind: bug|feature|research|epic|...) is authoritative.
|
||||
// A recognized kind maps to its label; ANY other declared kind
|
||||
// (e.g. epic) means NO type label and NO keyword guessing —
|
||||
// epics are containers, not bugs. Keyword heuristics apply
|
||||
// only to pre-template issues with no spine at all.
|
||||
let isBug = false, isFeature = false, isResearch = false;
|
||||
const kindMatch = body && body.match(/^kind:\s*([a-z_-]+)\s*$/m);
|
||||
if (kindMatch) {
|
||||
const kind = kindMatch[1];
|
||||
isBug = kind === 'bug';
|
||||
isFeature = kind === 'feature';
|
||||
isResearch = kind === 'research';
|
||||
} else if (action === 'opened') {
|
||||
// KEYWORD FALLBACK (#407): pre-template issues only, and
|
||||
// ONLY at filing time. Re-running keyword guesses on every
|
||||
// edit/label event overrides human curation of spine-less
|
||||
// issues (documented rule: keyword heuristics are the
|
||||
// pre-template fallback, not a standing authority).
|
||||
const textBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
isBug = textBug;
|
||||
isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
isResearch = !isBug && !isFeature && title.startsWith('[RESEARCH]');
|
||||
}
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
if (isResearch) {
|
||||
desiredLabels.add('research');
|
||||
}
|
||||
|
||||
// Umbrella labels
|
||||
if (text.includes('memory') || text.includes('store') || text.includes('recall') || text.includes('consolidat')) {
|
||||
desiredLabels.add('memory');
|
||||
@@ -87,19 +137,31 @@ jobs:
|
||||
desiredLabels.add('custodian');
|
||||
}
|
||||
|
||||
// Priority labels
|
||||
if (text.includes('urgent') || text.includes('critical') || text.includes('broken')) {
|
||||
desiredLabels.add('priority: high');
|
||||
}
|
||||
|
||||
// Type labels
|
||||
const isBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
const isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
// Priority labels (2026-10-05: PROGRAMMATIC P-ASSIGNMENT —
|
||||
// the gate demanded P0-P3 labels but nothing assigned them;
|
||||
// the dead 'priority: high' label is gone per the 2026-09-04
|
||||
// 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).
|
||||
const pMatch = title.match(/\bP([0-3])\b/);
|
||||
const priorityLabelNames = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
|
||||
if (pMatch) {
|
||||
desiredLabels.add(priorityLabelNames[Number(pMatch[1])]);
|
||||
} else if (text.includes('urgent') || text.includes('critical') ||
|
||||
text.includes('spine') || text.includes('blocks other work')) {
|
||||
desiredLabels.add(priorityLabelNames[0]); // P0 — critical
|
||||
} else if (text.includes('broken') || text.includes('fix') ||
|
||||
text.includes('repair') || text.includes('defect') ||
|
||||
text.includes('real defect') || text.includes('money')) {
|
||||
desiredLabels.add(priorityLabelNames[1]); // P1 — high
|
||||
} else if (text.includes('improve') || text.includes('consolidat') ||
|
||||
text.includes('refactor') || text.includes('kernel') ||
|
||||
text.includes('port')) {
|
||||
desiredLabels.add(priorityLabelNames[2]); // P2 — medium
|
||||
} else if (text.includes('benchmark') || text.includes('measurement') ||
|
||||
text.includes('deferred')) {
|
||||
desiredLabels.add(priorityLabelNames[3]); // P3 — low
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
||||
@@ -109,6 +171,7 @@ jobs:
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const currentLabels = new Set(current.data.map(label => label.name));
|
||||
const currentLabelIds = new Map(current.data.map(label => [label.name, label.id]));
|
||||
const staleManaged = [...currentLabels].filter(
|
||||
label => managedLabels.has(label) && !desiredLabels.has(label)
|
||||
);
|
||||
@@ -118,11 +181,14 @@ jobs:
|
||||
|
||||
for (const name of staleManaged) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
// Gitea's DELETE /labels/{id} requires the numeric label ID.
|
||||
// Octokit's GitHub-style removeLabel(name) returned 404 and
|
||||
// left stale priority labels behind (#2467: P0 AND P2).
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
name,
|
||||
id: currentLabelIds.get(name),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
@@ -140,5 +206,21 @@ 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.
|
||||
const reconciled = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
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}', {
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber, id: flag.id,
|
||||
});
|
||||
}
|
||||
console.log(`Removed managed labels: ${staleManaged.join(', ') || 'none'}`);
|
||||
console.log(`Added managed labels: ${missingDesired.join(', ') || 'none'}`);
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
update-gitea:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Check current Gitea version
|
||||
id: current
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions: write-all
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -131,8 +131,12 @@ jobs:
|
||||
"pyproject.toml": [(f'version = "{VERSION}"', f'version = "{NEW}"')],
|
||||
}
|
||||
|
||||
# Fetch current master content, apply replacements, VERIFY all five
|
||||
# surfaces agree on NEW before committing anything (TUI gate).
|
||||
# Fetch current master content, apply replacements, VERIFY all six
|
||||
# surface patterns (five files + the TUI footer) agree on NEW before
|
||||
# committing anything (TUI gate). The footer check was added after
|
||||
# the 0.89.0 half-bump: a stale RELEASE_VERSION made both replace
|
||||
# pairs no-op; the gate's RECOVER regex fixed the header only and
|
||||
# the footer silently stayed 0.88.4, failing the banner test.
|
||||
edited = {}
|
||||
for path, pairs in surfaces.items():
|
||||
status, cur = api(f"/contents/{path}?ref=master")
|
||||
@@ -151,6 +155,7 @@ jobs:
|
||||
checks = [
|
||||
("src/rune/__init__.py", rf'__version__ = "({VER})"'),
|
||||
("src/rune/rune_tui.py", rf"Version: v({VER})"),
|
||||
("src/rune/rune_tui.py", rf"Rune v({VER}) \|"),
|
||||
("src/rune/canary_gitea_automation.py", rf"'__version__ = \"({VER})\"'"),
|
||||
("pyproject.toml", rf'version = "({VER})"'),
|
||||
("README.md", rf"v({VER})"),
|
||||
|
||||
@@ -67,6 +67,20 @@ jobs:
|
||||
unit-tests:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
env:
|
||||
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
|
||||
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
|
||||
# The unit shim battery loads the cdylib (ep_loader); point it at the
|
||||
# runner-owned cdylib FILE from the crate-build step below (the override
|
||||
# expects a file path, not a directory).
|
||||
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
|
||||
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
|
||||
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
|
||||
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
|
||||
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
|
||||
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
|
||||
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
|
||||
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -102,6 +116,25 @@ jobs:
|
||||
fi
|
||||
python3 scripts/umbrella_impact.py check \
|
||||
--base "$(git merge-base origin/master HEAD)" --head HEAD ${BODY_FILE:+--body-file "$BODY_FILE"}
|
||||
- name: Build Rust evidence_provenance crate
|
||||
run: |
|
||||
# tests/test_evidence_provenance_shims.py drives the cdylib through
|
||||
# the shim layer. Toolchain law (see the canary job for the full
|
||||
# rationale): system rustc is broken on this host (LLVM_23.1
|
||||
# version-node symbol gap); use the healthy rustup toolchain, PATH
|
||||
# first so the rustup shim can't exec the broken system rustc.
|
||||
# Runner-owned /tmp target dir (the repo's target/ is nick's).
|
||||
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
|
||||
mkdir -p "$CARGO_TARGET_DIR"
|
||||
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
|
||||
- name: Test with coverage (global floor + changed-lines ratchet)
|
||||
run: |
|
||||
# #149 coverage ratchet — both floors live in .coveragerc; raise,
|
||||
@@ -113,12 +146,45 @@ jobs:
|
||||
.test-venv/bin/coverage xml
|
||||
.test-venv/bin/diff-cover coverage.xml --compare-branch origin/master --fail-under=70 --diff-range-notation ..
|
||||
|
||||
go-surface:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Verify Go toolchain version pin
|
||||
run: |
|
||||
# H0 pin: the Go surface builds against the pacman toolchain; the
|
||||
# pin file is the single source of truth so a distro upgrade that
|
||||
# changes behavior fails loud here, not downstream in the canary.
|
||||
WANT=$(cat go-version.txt)
|
||||
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: go vet + go test + go build
|
||||
run: |
|
||||
cd src/go/rune-entry
|
||||
go vet ./...
|
||||
go test ./...
|
||||
go build -o /tmp/rune-entry-ci .
|
||||
|
||||
canary:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
env:
|
||||
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
|
||||
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
|
||||
# The build emits to a runner-owned /tmp target dir (the repo's target/
|
||||
# is nick-owned); EP_RUST_LIB points ep_loader at the cdylib FILE there
|
||||
# (the override expects a file path, not a directory).
|
||||
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
|
||||
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
|
||||
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
|
||||
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
|
||||
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
|
||||
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
|
||||
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
|
||||
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -130,6 +196,34 @@ jobs:
|
||||
python3 -m venv .canary-venv
|
||||
.canary-venv/bin/pip install --disable-pip-version-check -r requirements-ci.txt
|
||||
.canary-venv/bin/pip install --disable-pip-version-check -e . --no-deps
|
||||
- name: Build Rust evidence_provenance crate
|
||||
run: |
|
||||
# The canary pair + the unit shim battery drive the COMPILED surfaces
|
||||
# (binary CLI parity, ep_loader FFI, ctypes cdylib loads).
|
||||
#
|
||||
# Toolchain law (troubleshooting history): the SYSTEM rustc is broken
|
||||
# on this host — librustc_driver-<sha>.so (pacman rust 1:1.99.0-1)
|
||||
# references std::string::_M_mutate under the LLVM_23.1 version node,
|
||||
# but /usr/lib/libLLVM.so.23.1 does not define that narrow-char
|
||||
# symbol under that node (it exports _M_create/_M_assign there, and
|
||||
# only the WIDE-char _M_mutate) — every compile dies with `symbol
|
||||
# lookup error` while `cargo --version` still prints. The rustup
|
||||
# toolchain under /home/nick/.rustup is healthy and ships its own
|
||||
# libLLVM, and /home/nick carries the x-ACL for this runner user.
|
||||
# Point PATH at the toolchain bin dir (bypasses the rustup shim,
|
||||
# which would exec the broken system rustc via RUSTUP_TOOLCHAIN).
|
||||
# Target dir is runner-owned (/tmp): the repo's target/ is nick's.
|
||||
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
|
||||
mkdir -p "$CARGO_TARGET_DIR"
|
||||
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
|
||||
- name: Run canary suite
|
||||
run: |
|
||||
FAILED=0
|
||||
@@ -156,6 +250,18 @@ jobs:
|
||||
src/rune/canary_p1_06.py \
|
||||
src/rune/canary_p1_01.py \
|
||||
src/rune/canary_query_memory.py \
|
||||
src/rune/canary_rune_entry.py \
|
||||
src/rune/canary_spine_meta.py \
|
||||
src/rune/canary_audit_ledger.py \
|
||||
src/rust/rune_spine/canary_spine_rust.py \
|
||||
src/rune/canary_rune_serve.py \
|
||||
src/rune/canary_tui_transport.py \
|
||||
src/rust/memory_scoring/canary_memory_scoring.py \
|
||||
src/rust/capability_scoring/canary_capability_scoring.py \
|
||||
src/rust/token_counting/canary_token_counting.py \
|
||||
src/rust/scaffold_core/canary_scaffold_core.py \
|
||||
src/rust/memory_storage/canary_memory_storage.py \
|
||||
src/rust/memory_retrieval/canary_memory_retrieval.py \
|
||||
src/rune/canary_unbounded.py \
|
||||
src/rune/canary_full_system.py \
|
||||
src/rune/canary_native.py; do
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint-and-format:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint Python files
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
ruff format shared/ src/ || true
|
||||
|
||||
canary-suite:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run all canaries
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
exit $FAILED
|
||||
|
||||
best-practices:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check best practices
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
grep -rn "TODO\|FIXME\|HACK\|XXX" shared/ src/ --include="*.py" | head -10 || echo " Clean"
|
||||
|
||||
documentation-check:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check documentation coverage
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
# push-triggered safety net in auto-version.yml. Detects master/tag drift
|
||||
# and files a deduped issue — it never auto-releases without the PR flow.
|
||||
release-drift-reconciliation:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -14,3 +14,4 @@ state/
|
||||
# Runtime artifacts (cwd-local event journal + capability index)
|
||||
index/
|
||||
journal/
|
||||
src/rust/*/target/
|
||||
@@ -58,3 +58,26 @@ fix, full pipeline re-entry (suite + ratchet + ruff + canaries + henxels;
|
||||
gates for anything beyond a mechanical minor), regression tests frozen forever,
|
||||
discharge recorded finding → test → commit. Symptom patches are rejected at
|
||||
review. To escape: edit `henxels.yaml` or this file — visibly.
|
||||
|
||||
## Issue organisation standard (binding, 2026-09-19)
|
||||
|
||||
One open note per idea. Issues use the template spine (kind, subsystem,
|
||||
version intent) with a plain-English heading that restates the subject —
|
||||
no meta-labels. Before minting a standalone issue, check whether the work
|
||||
belongs inside an existing parent:
|
||||
|
||||
- **Parked ideas under a blocked/future epic** are filed as checklist items
|
||||
in the parent's "Absorbed children" section, NOT as standalone issues.
|
||||
Absorption is reversible — re-mint standalone when the idea becomes live.
|
||||
- **Live, dependency-tracked, or starting work** gets a standalone issue.
|
||||
- **Closures state the reason as data**: `closed_because` in the spine
|
||||
(completed / superseded-by-#N / absorbed-into-#N / wont-do / duplicate),
|
||||
plus a standard closing comment (plain summary first).
|
||||
- The reconciliation checker (`scripts/gitea_reconciliation_check.py`) must
|
||||
stay green after any issue state change; the artifact is updated in the
|
||||
same change that moves issues.
|
||||
|
||||
## Async continuation final flow proofs (R13/M9) — 2026-09-19
|
||||
|
||||
PR-flow live proof marker: harmless documentation addition exercising the
|
||||
canonical single-continuation path on the PC-local forge. No code change.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.86.0**
|
||||
**Current release: v0.99.1**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { execFile } from "node:child_process"
|
||||
import { existsSync, appendFileSync } from "node:fs"
|
||||
|
||||
// builder-bridge — Rune Core preflight for the MiMo builder (handoff
|
||||
// 2026-09-10 §3–§7, Option C bridge tranche). For each user turn, the hook
|
||||
// extracts the user's message from the outgoing LLM request, runs the Rune
|
||||
// Core builder-bridge preflight (deterministic: route classification +
|
||||
// memory selection + active-Scaffolding projection, ZERO model calls), and
|
||||
// appends a compact capsule to the final user message. Same Rune Core the
|
||||
// TUI/operator path uses — this file is a thin adapter with no cognition.
|
||||
//
|
||||
// CANONICAL SOURCE: config/mimo-hooks/builder-bridge.ts
|
||||
// Installed to ~/.mimocode/hooks/builder-bridge.ts by
|
||||
// `scripts/install_mimo_hook.py` (hash-verified, drift-detecting — the same
|
||||
// contract as event-wait-guard.ts). T22: this file was previously
|
||||
// UNVERSIONED and hardcoded an ephemeral interpreter path (/tmp/ci-venv),
|
||||
// which silently killed the whole edge; both defects are repaired here.
|
||||
//
|
||||
// Overhead contract (§7/§8): capsule ≤ ~300 tokens; a repeat/empty task
|
||||
// costs ~0ms (TTL cache); a fresh task ~150ms (bounded retrieval). The hook
|
||||
// degrades silently to no-injection on any failure — it must never block
|
||||
// the builder.
|
||||
//
|
||||
// Injection policy: only append when the capsule is non-empty. A plain
|
||||
// "hello" gets nothing (§5: if nothing relevant exists, inject nothing).
|
||||
|
||||
const BRIDGE = "/mnt/workdrive/issue161-harness/src/rune/builder_bridge_cli.py"
|
||||
const MARKER_PREFIX = "/tmp/rune-bridge-"
|
||||
|
||||
// Interpreter resolution (T22 repair): the previous hardcoded
|
||||
// "/tmp/ci-venv/bin/python" is an EPHEMERAL path that no longer exists, so
|
||||
// every bridge call failed and the hook silently degraded — the MiMo edge
|
||||
// received no Rune Core preflight at all (zero fire-log lines). Resolve
|
||||
// against durable candidates, first existing wins; env override wins over
|
||||
// all of them. If nothing exists the hook degrades as before.
|
||||
const PY_CANDIDATES = [
|
||||
process.env.MIMO_BRIDGE_PY,
|
||||
"/mnt/workdrive/issue161-harness/.test-venv/bin/python",
|
||||
"/home/nick/.venvs/memory/bin/python3",
|
||||
"/usr/bin/python3",
|
||||
].filter((p) => typeof p === "string" && p.length > 0)
|
||||
|
||||
function resolvePy() {
|
||||
for (const p of PY_CANDIDATES) {
|
||||
try {
|
||||
if (existsSync(p)) return p
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Per-process dedupe: identical task text within the cache TTL does not need
|
||||
// a second bridge call — the CLI has its own TTL cache, but skipping the
|
||||
// process spawn entirely for the same message text within 10 minutes keeps
|
||||
// repeats at ~0ms.
|
||||
const lastText = new Map() // text -> timestamp
|
||||
const TTL = 600_000
|
||||
|
||||
function extractUserText(messages) {
|
||||
// messages: Array<{ info: Message, parts: Part[] }> — newest last.
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]
|
||||
if (m?.info?.role !== "user") continue
|
||||
const texts = []
|
||||
for (const p of m.parts ?? []) {
|
||||
if (p?.type === "text" && typeof p.text === "string") texts.push(p.text)
|
||||
}
|
||||
if (texts.length) return texts.join("\n").slice(0, 4000)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export default {
|
||||
"experimental.chat.messages.transform": async (input, output) => {
|
||||
try {
|
||||
if (!Array.isArray(output.messages) || output.messages.length === 0) return
|
||||
const userText = extractUserText(output.messages)
|
||||
if (!userText || userText.length < 12) return // trivial/noise guard
|
||||
// Harness-internal steps (checkpoint writer, compaction, system
|
||||
// reminders) are not builder tasks — no capsule (measured: the fire
|
||||
// log showed checkpoint-writer steps receiving capsules 2026-09-10).
|
||||
if (/^(<system-reminder>|EXTRACTION REQUIRED)/.test(userText.trim())) return
|
||||
const now = Date.now()
|
||||
const prev = lastText.get(userText)
|
||||
if (prev !== undefined && now - prev < TTL) return // dedupe repeats
|
||||
lastText.set(userText, now)
|
||||
if (lastText.size > 64) {
|
||||
// evict oldest
|
||||
let oldestKey = null, oldestVal = Infinity
|
||||
for (const [k, v] of lastText) if (v < oldestVal) { oldestVal = v; oldestKey = k }
|
||||
if (oldestKey) lastText.delete(oldestKey)
|
||||
}
|
||||
|
||||
const py = resolvePy()
|
||||
if (!py) return // no durable interpreter — degrade (never block)
|
||||
|
||||
const capsule = await new Promise((resolve) => {
|
||||
execFile(py, [BRIDGE, userText], { timeout: 15_000 }, (err, stdout) => {
|
||||
resolve(err ? "" : (stdout?.toString() ?? ""))
|
||||
})
|
||||
})
|
||||
if (!capsule.trim()) return
|
||||
|
||||
// Durable fire-trace: one line per ACTUAL injection into an outgoing
|
||||
// model request (handoff §1 proof requirement). Not a canary — this is
|
||||
// standing observability for the bridge's consumer edge.
|
||||
try {
|
||||
appendFileSync("/tmp/rune-bridge-fire.log",
|
||||
JSON.stringify({ ts: new Date().toISOString(), task: userText.slice(0, 80), capsule_tokens: Math.round(capsule.length / 4), py }) + "\n")
|
||||
} catch {}
|
||||
|
||||
// Append the capsule to the LAST user message (the one being answered).
|
||||
const last = output.messages[output.messages.length - 1]
|
||||
if (last?.parts && Array.isArray(last.parts)) {
|
||||
last.parts.push({
|
||||
type: "text",
|
||||
text: "\n\n<rune-preflight>\n" + capsule.trim() + "\n</rune-preflight>\n(Injected by Rune Core builder bridge — deterministic preflight, zero model calls. Relevant state above; ignore if not applicable to this task.)",
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// never block the builder on bridge failure
|
||||
}
|
||||
},
|
||||
}
|
||||
+116
-69
@@ -4,19 +4,37 @@
|
||||
"generated_at": "2026-09-16T18:23:38Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01",
|
||||
"canary_rune_entry",
|
||||
"canary_rune_serve"
|
||||
],
|
||||
"configs": [],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"description": "Tool selection and execution authority",
|
||||
"go_surfaces": [
|
||||
"rune-entry"
|
||||
],
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"adaptive_router",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"capability_broker",
|
||||
"connection_pool",
|
||||
"core_server",
|
||||
"cs_loader",
|
||||
"evidence_probe",
|
||||
"forge",
|
||||
"forge_client",
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
"message_queue",
|
||||
"native_units",
|
||||
"openai_compat_transport",
|
||||
"provider_context",
|
||||
"recall_uplift",
|
||||
@@ -24,21 +42,30 @@
|
||||
"tool_executor",
|
||||
"tool_protocol"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01"
|
||||
"rust_crates": [
|
||||
"capability_scoring"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration",
|
||||
"canary_audit_ledger",
|
||||
"canary_spine_meta"
|
||||
],
|
||||
"configs": [
|
||||
".coveragerc",
|
||||
"go-version.txt",
|
||||
"lint-baseline.json",
|
||||
"pyright-baseline.json"
|
||||
],
|
||||
"depends_on": [],
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"audit_ledger",
|
||||
"build_pipeline_policy",
|
||||
"cc_loader",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"curator",
|
||||
@@ -46,17 +73,24 @@
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer",
|
||||
"programme_gates"
|
||||
"programme_gates",
|
||||
"sc_loader"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
"rust_crates": [
|
||||
"evidence_provenance",
|
||||
"scaffold_core"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"description": "Learning and improvement",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
@@ -67,18 +101,17 @@
|
||||
"growth_simulator",
|
||||
"self_improve",
|
||||
"subconscious_pass"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"behavior",
|
||||
"identity_anchors",
|
||||
@@ -89,17 +122,19 @@
|
||||
"voice_injector",
|
||||
"voice_sync",
|
||||
"voicebox"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"description": "Model routing and outbound requests",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"inference_trace",
|
||||
"model_connect",
|
||||
@@ -110,31 +145,39 @@
|
||||
"provider_registry",
|
||||
"response_adapter",
|
||||
"standardized_providers",
|
||||
"tc_loader",
|
||||
"token_optimizer"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority"
|
||||
"rust_crates": [
|
||||
"token_counting"
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"abstraction",
|
||||
"cache",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"ffi_query_memory_smoke",
|
||||
"graph_memory",
|
||||
"graph_shard_bridge",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"memory_selector",
|
||||
"memory_store",
|
||||
"mems_loader",
|
||||
"mr_loader",
|
||||
"ms_loader",
|
||||
"query_memory",
|
||||
"recall_router",
|
||||
"retrieval_benchmark",
|
||||
@@ -142,18 +185,33 @@
|
||||
"tier_manager",
|
||||
"warm_neuron_store"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory"
|
||||
"rust_crates": [
|
||||
"memory_retrieval",
|
||||
"memory_scoring",
|
||||
"memory_storage",
|
||||
"query_memory"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"canaries": [
|
||||
"canary_cognitive_strata",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p1_07",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift",
|
||||
"canary_tui_transport"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability",
|
||||
"growth",
|
||||
"identity",
|
||||
"inference",
|
||||
"perception"
|
||||
],
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"auto_detect",
|
||||
"build_mode",
|
||||
@@ -188,32 +246,24 @@
|
||||
"task_persist",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"tui_transport",
|
||||
"tui_validator",
|
||||
"umbrella_update",
|
||||
"verification_layer",
|
||||
"warm_router",
|
||||
"work_manager"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability",
|
||||
"growth",
|
||||
"identity",
|
||||
"inference",
|
||||
"perception"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_cognitive_strata",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p1_07",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"description": "Input processing and context assembly",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
@@ -248,6 +298,8 @@
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"spine_language",
|
||||
"spine_loader",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
@@ -259,13 +311,8 @@
|
||||
"wait_guard_corpus",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
"rust_crates": [
|
||||
"rune_spine"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -272,4 +272,19 @@ observations, fresh resume.
|
||||
|
||||
Release-note generator: LIVE-PROVEN (real Gitea, 10 releases). Backfill:
|
||||
0.66.35→0.66.44 carrying historical-backfill provenance. Pre-0.66.35
|
||||
history: skipped (weak evidence — honest limitation).
|
||||
history: skipped (weak evidence — honest limitation).
|
||||
|
||||
## Contract discovery → spine (2026-10-05, #2465 CI correction)
|
||||
|
||||
The closed construction contract is the authority for discoveries. Spine
|
||||
rebuild reads its observed entities and candidate corrections; it does not
|
||||
claim a correction has been built. Qualified IDs such as
|
||||
`rust-crate:cognition_core` keep their declared kind. Bare file paths,
|
||||
test suites and other unqualified references become
|
||||
`construction-surface:<reference>`, never `module`. An observed
|
||||
surface may be PRESENT; a candidate stays DECLARED until independently
|
||||
observed. Existing bad records are migrated with their evidence and edge
|
||||
references intact. This matters: the first write-back mapped bare paths to
|
||||
modules, made 22 false module records and crashed CI on the next rebuild.
|
||||
The per-contract quick-reference markdown is generated from the scaffold
|
||||
store at close; it is a view, not another authority.
|
||||
@@ -11,24 +11,22 @@
|
||||
"NEEDS_REVALIDATION",
|
||||
"DEFERRED_FUTURE"
|
||||
],
|
||||
"live_defects": [
|
||||
324
|
||||
],
|
||||
"live_defects": [],
|
||||
"records": [
|
||||
{
|
||||
"number": 324,
|
||||
"title": "Engine defect: checkpoint writer fails deterministically on aborted worker sessions (MessageAbortedError -> empty checkpoint shells)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "FIXED_VERIFIED",
|
||||
"labels": [
|
||||
"bug",
|
||||
"custodian",
|
||||
"life",
|
||||
"memory"
|
||||
],
|
||||
"live_defect": true,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 defect no longer reproduces: zero MessageAbortedError in current logs (incl. active worker), six most recent session checkpoints full documents (111-396 lines); verified by present-absence 2026-09-19"
|
||||
},
|
||||
{
|
||||
"number": 316,
|
||||
@@ -237,8 +235,8 @@
|
||||
{
|
||||
"number": 183,
|
||||
"title": "Future: typed temporal semantics for facts, relationships, beliefs, and preferences",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -250,14 +248,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 182,
|
||||
"title": "Future: GitHub code-search prior-art audit for graph and vector memory",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -266,8 +264,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 181,
|
||||
@@ -287,13 +285,13 @@
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"next_action": "epic holds absorbed children #182 #183 + research watchlist #164 #165 #166; closes when prerequisites land or ideas dropped"
|
||||
},
|
||||
{
|
||||
"number": 179,
|
||||
"title": "Future: succession/privacy semantics and lineage evaluation",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -305,14 +303,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 178,
|
||||
"title": "Future: evidence-grounded ancestral perspective with explicit uncertainty",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -322,14 +320,14 @@
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 177,
|
||||
"title": "Future: temporal autobiographical model with provenance and reinterpretation lineage",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -340,14 +338,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 176,
|
||||
"title": "Future: lineage-aware situated curiosity for meaningful autobiographical gaps",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"feature",
|
||||
@@ -358,8 +356,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 175,
|
||||
@@ -379,13 +377,13 @@
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"next_action": "epic holds absorbed children #176-#179 as checklist items; closes when prerequisites land or ideas dropped"
|
||||
},
|
||||
{
|
||||
"number": 166,
|
||||
"title": "RESEARCH WATCHLIST: Memory-R1 (RL memory ops), Titans (test-time memory), Vespa/Weaviate/Milvus (retrieval competitors)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"growth",
|
||||
@@ -394,14 +392,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 165,
|
||||
"title": "RESEARCH: SimpleMem semantic compression + A-MEM dynamic linking + Letta sleep-time compute",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"infrastructure",
|
||||
@@ -410,14 +408,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 164,
|
||||
"title": "RESEARCH: HippoRAG 2 \u2014 PPR-style associative propagation as warm-neuron activation candidate",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"growth",
|
||||
@@ -426,27 +424,27 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 163,
|
||||
"title": "Temporal graph backend spike: Neo4j vs FalkorDB (LadybugDB experimental; Graphiti as reference)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "run the backend spike; compare on same corpus/query set; record evidence-backed recommendation"
|
||||
},
|
||||
{
|
||||
"number": 162,
|
||||
"title": "Qdrant associative-memory projection spike (dense/sparse/multivector + ColBERT late-interaction reranking)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"custodian",
|
||||
@@ -454,8 +452,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #316 (ADAPT-1 B3-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 159,
|
||||
@@ -478,8 +476,8 @@
|
||||
{
|
||||
"number": 158,
|
||||
"title": "Generated wiring-status / architecture-truth report",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P2 \u2014 medium",
|
||||
"growth",
|
||||
@@ -489,8 +487,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #315 (ADAPT-1 B2-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 156,
|
||||
@@ -1151,6 +1149,6 @@
|
||||
"next_action": ""
|
||||
}
|
||||
],
|
||||
"last_reconciled": "2026-09-12",
|
||||
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
|
||||
}
|
||||
"last_reconciled": "2026-09-19",
|
||||
"reconciled_by": "fresh-session tranche 2026-09-19: template retrofit (spine+plain-English on all open issues), closures #158/#162 (SUPERSEDED) and #324 (FIXED_VERIFIED), research labels applied to #163/#164/#165/#166/#182/#316, stale bug labels removed from #163-#166; 2026-09-19 absorption pass: parked children absorbed into parents (#175<-#176..#179; #181<-#182,#183,#164..#166) per one-open-note-per-idea standard"
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
# Language-Fit Rubric — Wave 1 (Language-Rebasing Tranche R1)
|
||||
|
||||
> STATUS: FINAL — self-verified 2026-09-29 per tranche §12 (all 6 rounds completed,
|
||||
> evidence-sufficiency statements recorded, kill tests frozen; execution proceeds on the
|
||||
> evidence, every RECODE still passes its benchmark gate — no win, no recode).
|
||||
> Per-subsystem sections filled from live research rounds (≥2 live-verified external sources per claim,
|
||||
> per-source lessons, evidence→decision mapping, kill test per candidate). No code changes in Wave 1.
|
||||
> Governing law from §11: **KEEP-PYTHON honesty determines how much of the rebase volume is real.**
|
||||
> Toolchain baseline: Rust 1.98.1 + GCC 16.2.1 present; Go NOT installed — Go earns installation only
|
||||
> through rubric evidence here.
|
||||
|
||||
## Verdict (all rounds, one paragraph)
|
||||
|
||||
**KEEP-PYTHON across the board: 40 clusters scored, 0 recode units awarded.** Six research
|
||||
rounds (W2 leaf utilities, W3 memory, W4 provider-layer, W5 orchestration, W6 runtime-core,
|
||||
W7 TUI) each characterized their subsystem's actual workload inward (file:line-anchored) and
|
||||
tested Python/Rust/Go/C against it with live-verified sources and frozen kill tests. The
|
||||
pattern is consistent: the compute-shaped work already executes in C (hashlib, sqlite3/FTS5/vec0,
|
||||
json, regex), the dominant latency is network or LLM-bound, and no cluster contains a hot loop
|
||||
that pays for a process-boundary recode through the 658 µs–0.63 ms interop crossing. **Go:
|
||||
REJECTED — does not earn installation this tranche** (D5=0 everywhere; nothing measured or
|
||||
fetch-confirmed earns it; orchestration additionally rejects its design fit; K-GO kill tests
|
||||
frozen per round should the case be revived). **Rust: toolchain-ready, justified nowhere this
|
||||
tranche** — keep as the standing option behind the CI-proven std-only adapter for any future
|
||||
measured hot loop. **C: rejected everywhere scored** (duplicates C already inside SQLite/hashlib;
|
||||
adds an unsafe-memory surface). The rebase volume the tranche hypothesized is real only as a
|
||||
small set of named improvements — see "Wave-plan consequence" below.
|
||||
|
||||
## Rubric dimensions (applied to every candidate language per subsystem)
|
||||
|
||||
| Dimension | Question | Evidence required |
|
||||
|---|---|---|
|
||||
| D1 Workload fit | Does the subsystem's dominant workload (I/O-bound, CPU-bound, FFI, protocol, UI) actually favour this language? | Benchmarks or authoritative docs, live-verified |
|
||||
| D2 Measured gain | What wins, concretely — latency, memory, startup, binary distribution? | Named hot paths from the Wave 0 inventory + numbers |
|
||||
| D3 Interop cost | What does the interop contract (§5) cost to carry for this boundary? | Adapter surface size, ABI/build complexity |
|
||||
| D4 Ecosystem | Do the needed libraries exist and are they maintained? | Live-verified crate/package/module listings |
|
||||
| D5 Toolchain reality | Is the toolchain present (or installable and justified) on this machine and the CI runner? | Runner + local `--version` evidence |
|
||||
| D6 Maintenance continuity | Can the Rune ecosystem's single-maintainer loop sustain code in this language here? | Honest judgement, stated plainly |
|
||||
| D7 Kill test | What is the CHEAPEST disproof of this candidate? | One falsifiable early test, frozen before any recode |
|
||||
|
||||
Scoring: each candidate gets D1–D6 scored 0–2 with one-line evidence each, D7 written regardless.
|
||||
Decision per subsystem: **KEEP-PYTHON** / **RECODE→<language>** — KEEP-PYTHON is the expected default;
|
||||
a RECODE that cannot pass its own kill test reverts to KEEP-PYTHON before implementation.
|
||||
|
||||
---
|
||||
|
||||
## W3 — Memory & persistence (units: memory_store 56 fns, curator 43, warm_router 32, memory_consolidation 20, sleep_time 25, + graph_memory, night_mode, identity_anchors)
|
||||
|
||||
- **Research round actor:** general-2 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/memory/`: inward.md, outward/ ×6, outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** JSONL-heavy appends, BM25/FTS ranking, consolidation passes, long-running
|
||||
scheduled jobs, file-backed state.
|
||||
- **Decision:** **KEEP-PYTHON (all 7 clusters, zero RECODE).** Cluster verdicts:
|
||||
(1) hot-store JSON persistence — KEEP with upgrade path: orjson in-process wheel
|
||||
(~10× dumps / 2× loads); a bespoke Rust CLI unit is dead on spawn cost (the
|
||||
`shared/native_units.py` crossing ~1–5 ms ≈ orjson's entire 3.4 MB `knowledge.json` decode);
|
||||
(2) SQLite store + FTS5/vec0 recall — KEEP, the compute is already C inside SQLite
|
||||
(`memory_store.py:646` vec0 MATCH, `:693` bm25); (3) neuron scoring/routing — KEEP,
|
||||
network-bound `_embed` (Ollama subprocess curl) dominates; (4) graph BFS — KEEP,
|
||||
hops=1 default, per-node SQL, tiny N; the future fix is a SQL CTE, not a language;
|
||||
(5) batch consolidation/curator — KEEP, LLM/I-O-bound and latency-insensitive,
|
||||
asyncio sweet spot; (6) orchestration/glue — KEEP, pure glue pinned by the Python-TUI
|
||||
interop contract; (7) embedding transport — KEEP, network-bound.
|
||||
- **Candidate matrix summary:** Python 7/7 · Go 0/7 (not installed, nothing measured or
|
||||
fetch-confirmed earns installation; Go's JSON stack mid-rewrite upstream) · hand-written C
|
||||
0/7 (C already battle-tested inside SQLite — hand-writing would duplicate it) · Rust 0/7
|
||||
as CLI units (spawn cost kills the one credible vector; in-process wheels excluded by
|
||||
Wave 0 policy).
|
||||
- **Kill tests (frozen in synthesis.md):** Rust-JSON — orjson microbench on live 3.4 MB
|
||||
`knowledge.json` (if drop-in and ≥2× load / ≥5× dump, the CLI unit is disproven on spawn
|
||||
cost); Rust-general — kill unless a profile shows ≥50% wall time in pure Python compute
|
||||
(none exists; reopen only on materially changed workload); Go — instrument one
|
||||
`consolidate_all` run, kill if Python-compute share <50% (it will be — LLM/I-O bound);
|
||||
C — kill unless a hot kernel absent from the SQLite extension ecosystem is identified
|
||||
(none).
|
||||
- **Sources + lessons:** 6 live-fetched 2026-09-29 (orjson, sled, sqlite-vec, SQLite FTS5
|
||||
docs, Go json/v2 rewrite issue, pydantic-core) — full annotations in
|
||||
`research/wave1/memory/outward-sources.md`.
|
||||
- **Evidence sufficiency (honest):** static analysis only, no runtime profiling;
|
||||
memory.store SQLite DB not materialized locally — kill tests specify *measured*
|
||||
thresholds rather than assertions. Verdicts rest on "Python is glue around C/network,"
|
||||
which profiling would only strengthen.
|
||||
|
||||
## W4 — Provider/model layer (units: provider_registry 28, model_connect 10, model_routing 15, model_profiles 15, provider_budget 21, + standardized_providers, model_registry, connection_pool)
|
||||
|
||||
- **Research round actor:** general-5 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/provider-layer/`: inward.md, outward-sources.md, outward/ (8 evidence
|
||||
JSONs), synthesis.md)
|
||||
- **Intention profile:** HTTP/SSE streaming, connection pooling, retries/backoff, protocol parsing,
|
||||
tool-call reliability.
|
||||
- **Intention profile correction (inward-verified):** the layer has **NO retry/backoff and NO
|
||||
SSE stub at all** (grep-verified) — `stream: False` everywhere, ~900 lines, remote-LLM-latency-
|
||||
bound. Rewrite pitches are therefore feature-adds, not ports.
|
||||
- **Candidate matrix:** 5 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
|
||||
workload): (C1) HTTP wire + transport (provider_registry, openai_compat_transport, model_connect
|
||||
discovery) Python 5 / Go 2 / Rust 1; (C2) connection pool (connection_pool.py, 144 lines —
|
||||
in-process object guards under threading.Lock, NOT a TCP pool) Python 5 / Go 2 / Rust 2;
|
||||
(C3) routing decisions (model_routing.py, 432 lines, pure decision logic, one httpx call)
|
||||
Python 5 / Go 1 / Rust 1; (C4) budget accounting (provider_budget.py, 354 lines, pure
|
||||
arithmetic + lock, no I/O) Python 5 / Go 1 / Rust 1; (C5) config/registry/normalization
|
||||
(model_profiles, model_registry, standardized_providers, model_connect tables — dataclasses +
|
||||
JSON, editable by non-systems programmers) Python 5 / Go 1 / Rust 1.
|
||||
- **Decision:** **KEEP-PYTHON (all 5 clusters).**
|
||||
- **Kill tests:** C1 Go — KILLED: toolchain absent (`which go` → not found); 28 call-site
|
||||
behaviors incl. the tool-call envelope contract (openai_compat_transport.py:67-107, 160-207)
|
||||
would be re-implemented with zero incumbent test port and NO performance signal (remote-LLM-
|
||||
latency-bound, kilobyte payloads). Re-earn path if Go is ever installed: std-only proof via
|
||||
`interop/rebase-proof/` + a benchmark harness on C1 only. C1 Rust — KILLED BY CONSTRUCTION:
|
||||
SSE parsing requires reqwest+tokio+eventsource-stream+nom (outward O3/O4/O5 — even mistral.rs
|
||||
takes the tokio stack), violating the std-only interop contract. C2–C5 Go/Rust — KILLED:
|
||||
burden of proof sits on the rewrite candidates; kill tests need contention/throughput/
|
||||
counter-evidence that does not exist; routing runs once per request at millisecond scale
|
||||
against multi-second LLM calls (5 orders of magnitude below any measurable threshold).
|
||||
- **Sources + lessons:** 8 live-fetched (O1–O8), ≥2 per major claim, coverage map in
|
||||
`outward-sources.md`. Lessons: (1) stdlib-only Rust has no realistic precedent for HTTP/SSE
|
||||
workloads — the interop contract's zero-crate standard effectively decides Rust out of all
|
||||
network-wire clusters; (2) Go's stdlib HTTP+NDJSON streaming is capability-proven
|
||||
(pkg.go.dev/net/http, ollama api/client.go, ollama's Go API server) — Go's losses here are
|
||||
evidence-absence, not capability-absence; (3) the honest reliability upgrade for this layer
|
||||
is a small Python-level httpx retry/timeout wrapper — a feature-add, not a language change.
|
||||
|
||||
## W5 — Orchestration, authority, plugins, skills (units: orchestration/ 44 files 244 fns, contracts, build_mode 34, construction_authority*, verification_construction, spine, + governance surfaces: drift_*, persona_gate, safety_override, witness, behavior, growth_delta, claim_audit)
|
||||
|
||||
- **Research round actor:** general-1 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/orchestration/`: inward.md, outward-sources.md, outward/ (4 search
|
||||
JSONs), synthesis.md)
|
||||
- **Intention profile:** contract/lifecycle state machines, authority gating, fail-closed validation,
|
||||
HMAC-chained ledgers, policy dispatch.
|
||||
- **Intention profile correction (inward-verified):** the subsystem is durable-ledger I/O,
|
||||
authority wiring, deterministic policy gates, and TUI-facing coordination — NOT hot loops
|
||||
(2 RLocks, 0 sockets, 0 thread pools across 43 files). **Scope gap: `construction_authority.py`
|
||||
and `verification_construction.py` do not exist in the tree** — the construction cluster is
|
||||
covered by build_mode.py; no evidence invented for the missing files.
|
||||
- **Candidate matrix:** 6 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
|
||||
workload): (C1) durable state & ledger stores (event_store.py:56-101, atomic.py, evidence_store.py
|
||||
— fsync-bound; hashing already C via hashlib) Python 5; (C2) composition roots & authority wiring
|
||||
(composition.py, authority.py, cognitive.py — pure glue, runtime cost ≈ 0) Python 5; (C3)
|
||||
policy/state machines/gates (lifecycle.py, scheduler.py, broker.py, no_change.py, reflex.py —
|
||||
deterministic dict/enum decisions, sub-ms; testability is the requirement) Python 5; (C4)
|
||||
structural self-model & schema tooling (spine.py:24-25, microkernel.py, contracts.py, records.py
|
||||
— introspection + canonical JSON; only candidate for a future Rust spine-diff if snapshots grow
|
||||
1000×) Python 4; (C5) TUI-facing orchestration & build pipeline (build_mode.py 11-stage pipeline,
|
||||
gateway.py, transport.py — the consumer IS the Python TUI; latency dominated by LLM/subprocess
|
||||
waits) Python 5; (C6) benchmark/migration harnesses (retrieval_benchmark.py, migrate.py,
|
||||
benchmark.py — offline tooling, recode buys nothing, loses iteration speed) Python 4.
|
||||
- **Decision:** **KEEP-PYTHON (all 6 clusters). This subsystem contributes 0 recode units to
|
||||
the wave plan.** Go: REJECTED, do not install — differentiator (goroutine-dense servers)
|
||||
targets a workload absent here; cost is exception→error-value rewrite of ~260 functions.
|
||||
Rust: FEASIBLE (toolchain proven) but UNJUSTIFIED anywhere — no cluster has the hot loop
|
||||
that pays for a process-boundary recode; keep as toolchain-ready for other subsystems.
|
||||
C: REJECTED — the only compute-shaped work (sha256) already executes C via stdlib hashlib.
|
||||
- **Kill tests:** K1 (frozen, ALREADY RUN): 100 round-trips through the rebase-proof adapter —
|
||||
mean 0.63ms, p95 0.74ms, cold 0.87ms, 100% exit-0 + ok:true → adapter overhead does NOT block
|
||||
recodes on cost; recodes die on value grounds. Re-open any cluster if profiling shows a
|
||||
function consuming >10% of a real interactive session. K-GO (frozen, ~30 min): Go prototype
|
||||
of event_store.py's batched append+fsync vs Python at 1000 events; if Python's p95 is within
|
||||
2× of Go's, Go is dead here permanently. K-RUST (frozen, ~20 min): measure spine diff on a
|
||||
10k-entity snapshot; <50ms kills any Rust spine recode by absence of pain.
|
||||
- **Sources + lessons:** 4 live-fetched web sources (pydantic-core, ruff blog, go.dev FAQ,
|
||||
Python 3.14 hashlib docs) + 1 LOCAL EXPERIMENT (n=100 adapter benchmark). Lessons: (1) the
|
||||
rebase-proof adapter round-trips at 0.63ms/op — subprocess-per-op is viable for coarse ops,
|
||||
never per-event; (2) Python's "slowness" argument dissolves where the actual compute already
|
||||
runs in C via stdlib (hashlib, sqlite) — the language question is really "where is the hot
|
||||
loop", and here the answer is nowhere; (3) assumed limits flagged honestly: human-scale event
|
||||
cadence (batch_size=8) and Linux spawn cost — both would need revisit at 10⁴+ events/sec or
|
||||
cross-platform targets.
|
||||
|
||||
## W6 — Runtime core & tooling (units: tool_executor 46, tool_protocol 27, local_tool_runtime 32, capability_broker 19, context_broker 21, work_manager 25, cron_engine 28, + failure_auto_filer 28, hook_engine, elastic_cognition, auto_tasks, goal)
|
||||
|
||||
- **Research round actor:** general-4 (COMPLETED 2026-09-29 on disk despite cancellation
|
||||
notification — artifacts at `research/wave1/runtime-core/`: inward.md, outward/,
|
||||
outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** subprocess/process supervision, native execution boundary (native_units
|
||||
adapter already exists), event loops, cron scheduling, hook dispatch.
|
||||
- **Decision:** **STAY-PYTHON (all 6 clusters).** Round scores (1–5 = fit for the cluster's
|
||||
actual inward-characterized workload): (A) subprocess lifecycle (tool_executor, cron_engine,
|
||||
auto_tasks) 4.7 — `subprocess.run(timeout=, capture_output=)` is a one-call expression of the
|
||||
pattern; Rust std::process has NO built-in child timeout (outward-verified), strictly more
|
||||
code under zero-crate; (B) protocol/JSON parsing (tool_protocol, brokers) 4.5 — inputs are
|
||||
KB-scale model fragments; Python json/regex is already C-backed, so a rewrite swaps C for
|
||||
compiled code at the same asymptotic cost; serde's typed-struct model actively mismatches
|
||||
fence-wrapped schema-flexible output; (C) model→tool control loop + HTTP
|
||||
(local_tool_runtime, elastic_cognition, goal) 4.8 — wall time is LLM HTTP latency in seconds;
|
||||
Python overhead is µs–ms noise; the loop's content is policy (circuit breaker, escalation
|
||||
ladders, routing tables); (D) context assembly (context_broker) 4.7 — correctness lives in
|
||||
admission policy and degrade-not-crash semantics, not speed; compaction is itself an LLM call;
|
||||
(E) reliability/queueing (failure_auto_filer) 4.2 — honest lowest: on paper the best Rust
|
||||
candidate (queue + worker + crash-safe JSONL spool), but perf requirement ≈ nil and the
|
||||
correctness burden is threading semantics a rewrite would re-risk for no win ("nothing
|
||||
breaks today"); (F) declarative config/brokers (work_manager, hook_engine, capability_broker)
|
||||
4.9 — literal data, no-op runtime, portable to anything valuable in nothing.
|
||||
- **Candidate matrix summary:** Python 6/6 · Go 0/6 (paper verdicts only — evidence-starved,
|
||||
not installed, kill tests unrunnable locally) · Rust 0/6 (kill tests runnable via the
|
||||
existing std-only interop harness) · C unscored (no FFI-adjacent cluster surfaced).
|
||||
Best-language moment found anywhere: robfig/cron replacing ~37 hand-rolled cron-parsing
|
||||
lines (cron_engine.py:199-236) — ergonomics nicety, not a fit verdict.
|
||||
- **Kill tests (frozen in synthesis.md):** Rust-A — re-express `_run_command`
|
||||
(tool_executor.py:1064) with std::process + manual timeout; kill if >2× the ~40-line
|
||||
Python wrapper (Q4 predicts it is); Go-A — port `cron_engine._run_loop` with os/exec,
|
||||
measure task-fire latency delta (prediction: zero — 60 s sleep and child runtime dominate);
|
||||
B — parse the largest observed tool-protocol stream 1000× in Python; kill if total <1 s
|
||||
(no rewrite yields an observable win); C — instrument `_run_loop`
|
||||
(local_tool_runtime.py:417), log non-HTTP CPU per iteration (prediction <1% of wall);
|
||||
D — time `assemble`+`_admit` on the largest stored context, kill if <10 ms; E — fault-inject
|
||||
kill -9 during spool append; if recovery semantics already hold, the rewrite has no
|
||||
correctness win to buy; F — none needed: absence of any win condition IS the disproof.
|
||||
- **Sources + lessons:** live-fetched 2026-09-29 — annotations in
|
||||
`research/wave1/runtime-core/outward-sources.md`; headline lessons: Rust std::process lacks
|
||||
child timeout (Q4); Go os/exec reaches parity but changes only syntax (Q1); Python
|
||||
json/regex C-backed (Q5b); serde typed structs mismatch dynamic model output (Q5a);
|
||||
concurrency features irrelevant at ≤2 threads (Q6).
|
||||
- **Evidence sufficiency:** Python verdicts fully evidenced inward+outward; Go execution-side
|
||||
evidence honestly starved (paper kill tests carried forward); E's JSONL-spool crash-safety
|
||||
claim rests on inward semantics only — flagged in synthesis.md.
|
||||
|
||||
## W7 — Operator + TUI (units: rune_operator 61 fns, rune_tui 161 fns — largest module, tui_validator)
|
||||
|
||||
- **Research round actor:** general-3 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/tui/`: inward.md, outward/ ×8, outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** terminal rendering, input handling, rich layout, hot store state, supervisory
|
||||
command surface. Design decision 4 holds: TUI consumes the interop contract, never rewritten first.
|
||||
- **Decision:** **KEEP-PYTHON (TUI shell; all 4 clusters, zero RECODE, no carve-out found).**
|
||||
Cluster scores (1 = recode, 5 = keep): (A) rendering/widgets 4 — no hot loop exists; render is
|
||||
plain Rich `Text` concatenation (rune_tui.py:1331, chat_bubble.py:157), only animation a 0.12 s
|
||||
spinner (:263), no markdown/Syntax parsing anywhere in src/rune; (B) event glue (~50 `_cmd_*`
|
||||
handlers, reactives, tool-widget maps) 5 — this is Textual's core competency (workers,
|
||||
`exclusive=True`, `call_from_thread`); vendor docs (ratatui) admit "little to no help in organizing
|
||||
large applications"; (C) model send path + envelope 5 — synchronous `send_task` in async worker;
|
||||
the envelope shape (`success/response/thinking/tool_events/degraded`, rune_tui.py:3624ff) is
|
||||
ALREADY the de-facto interop contract; (D) subprocess/streaming 5 — JSONL streaming precedent
|
||||
already in-tree (`_cmd_pull` :3443-3517): the pattern for consuming recoded units, not a recode
|
||||
target. Whole-TUI native recode 1/5 REJECTED — 80+ first-party module coupling, framework-provided
|
||||
concurrency re-implemented by hand; Go additionally toolchain-blocked. Countervailing evidence
|
||||
(Bubble Tea/tview/Codex TUI maturity) recorded honestly in outward-sources.md — it does not
|
||||
outweigh the coupling.
|
||||
- **⚠ CONTRACT GAP FLAGGED (binds Waves 2–7):** `shared/native_units.py` parses only the LAST
|
||||
stdout line — it must gain a JSONL streaming reader (per-line `json.loads` + terminal-envelope
|
||||
validation) before any recoded long-running unit can stream progress to the TUI. Small, testable,
|
||||
non-breaking; candidate for the first Wave 2 implementation issue.
|
||||
- **Adapter surface (TUI as consumer — adopted as the rubric's interop-contract restatement):**
|
||||
(1) single-invocation stateless CLI (`native_unit <op> [args]`, no daemon); (2) success = final
|
||||
stdout line `{"ok":true, …}` generalizing the model-send envelope — recoded units emitting
|
||||
`response`/`tool_events`/`degraded` plug in with zero TUI changes; (3) failure = `ok:false` +
|
||||
nonzero exit, stderr human-only, success = exit 0 AND not timed out; (4) mandatory `--version`
|
||||
on every unit (status bar + `canary_tui_drift.py` detect live units vs Python fallback);
|
||||
(5) JSONL streaming mode for long ops with terminal sentinel envelope; (6) per-call timeout
|
||||
budget, `timed_out` surfaced in `NativeResult` for graceful degradation; (7) streamed events
|
||||
referencing UI objects carry the TUI's stable keys — `(channel, call_id)` for tool widgets
|
||||
(:3676), WorkPane `_entries` object ids (:1239) — preserved verbatim; (8) std-only for
|
||||
proof-phase units.
|
||||
- **Candidate matrix summary:** Python 4/4 clusters · Rust/Go whole-TUI 1/5 rejected · carve-out
|
||||
candidates: none found (future markdown parsing would carve as a Rust native unit behind the
|
||||
adapter, not now).
|
||||
- **Kill tests (frozen in synthesis.md):** stay-Python — killed if >16 ms UI-thread stalls under
|
||||
heavy event bursts OR IPC latency dominates message round-trip (Textual pilot measurement);
|
||||
Rust recode — killed if the adapter must expose >~15 first-party modules (count today ≈80:
|
||||
killed NOW); Go recode — additionally killed by toolchain absence (`command -v go` empty).
|
||||
- **Sources + lessons:** 8 live-fetched 2026-09-29 (Textual workers guide, ratatui vendor docs,
|
||||
Bubble Tea/tview/Codex countervailing evidence, JSON Lines spec, push-granular-work guidance) —
|
||||
annotations in `research/wave1/tui/outward-sources.md`.
|
||||
- **Evidence sufficiency (honest):** full inward read of all 5 TUI files with line refs; ≥2 sources
|
||||
per major claim; gap = no runtime profiling (stall kill test unmeasured — but no mechanism exists
|
||||
for the stall to occur; the 120 ms 21-step pipeline was already removed, `_run_warm_router`
|
||||
:3575 is a no-op).
|
||||
|
||||
## W2 — Leaf utilities & canaries (~40 modules, low coupling: pins, time_awareness, message_queue, event_envelope, attention_capsule, complexity_scorer, token_optimizer, cache 501/17, prompt_sync, chat_bubble, ~15 canary_* modules)
|
||||
|
||||
- **Research round actor:** general-6 (COMPLETED 2026-09-29 — full report in actor notification)
|
||||
- **Intention profile:** small pure helpers, envelopes, scoring, cache; smallest blast radius; the
|
||||
factory + interop proving ground at real scale.
|
||||
- **Ground-truth measurements (local, this session):** Python fn call ~46 ns (100k timed
|
||||
iterations in-process) · native CLI roundtrip ~658 µs (subprocess to
|
||||
`interop/rebase-proof/target/release/rebase-proof --version`) · Python startup floor ~18 ms ·
|
||||
overhead ratio ≈ **14,000×**. The only CI-proven interop contract is the subprocess CLI boundary
|
||||
(`shared/native_units.py`: argv in → JSON stdout, `--version`, timeout, typed `NativeResult`).
|
||||
PyO3/in-process bindings excluded by Wave 0 policy. Therefore every per-call crossing costs
|
||||
≥658 µs — a per-turn helper would need to run >14,000× faster in Rust to break even.
|
||||
- **Candidate matrix (representative — full per-cluster 0–2 scoring in round report):**
|
||||
Python 12/12 in every cluster · Rust 5–6/12 · Go 1–3/12 (D5=0, not installed) · C 1–2/12.
|
||||
No module contains a hot loop (heaviest: 2 `re.sub` per turn in token_optimizer).
|
||||
- **Decision:** **KEEP-PYTHON (all ~40, zero RECODE).** Cluster verdicts: (a) pure CRUD helpers
|
||||
pins/time_awareness/token_optimizer — KEEP, CPython 3.11+ already 10–60% faster on this shape
|
||||
(PEP 659); (b) message/envelope/capsule structures — KEEP, recode would insert a boundary
|
||||
between the chattiest neighbors; (c) cache.py (501 L, largest in wave) — KEEP, the fast part is
|
||||
already C (sqlite3 DB-API 2.0 over SQLite's C library), Python is thin orchestration, batching
|
||||
(threshold 50 / 30 s) already amortizes writes; (d) chat_bubble.py — KEEP, **hard-pinned** to
|
||||
Python by the Textual framework (recode = full framework abandonment, bottleneck is human
|
||||
typing speed); (e) prompt_sync.py glue — KEEP, glue into Python siblings (memory_env,
|
||||
voice_sync, build_pipeline_policy); (f) ~19 canary_* — KEEP **categorically**, a canary that
|
||||
verifies Python importability cannot exist in another language.
|
||||
- **Kill tests (reopening criteria, all fail today on first cheap measurement):** Rust — microbench
|
||||
one per-turn helper behind the native_units CLI, must beat the Python in-process call >10×
|
||||
end-to-end including the 658 µs crossing (whole call <~46 µs; a trivial `--version` already
|
||||
costs 658 µs); Go — identical benchmark post-install, uninstall if roundtrip >100 µs with no
|
||||
named hot path; C — identical benchmark + required fuzz-clean JSON roundtrip, plus a
|
||||
demonstrated GC-free allocation-hot loop first; cache.py — recode only if Python-side
|
||||
orchestration (excl. sqlite3 waits) exceeds 5% of end-to-end latency; capsule/envelope/queue —
|
||||
recode only if one turn's Python total exceeds ~5 ms AND a candidate's CLI path is >10× cheaper.
|
||||
- **Sources + lessons:** (1) https://docs.python.org/3/whatsnew/3.11.html — 10–60% faster,
|
||||
PEP 659 specializing interpreter: CPython already optimized for helper-shaped code, the D2
|
||||
baseline is a moving improving target; (2) https://docs.python.org/3/whatsnew/3.13.html —
|
||||
free-threading/JIT experimental, KEEP-PYTHON ≠ frozen but no bets on experimental paths;
|
||||
(3) https://www.maturin.rs/ — in-process Rust↔Python needs a whole binding/packaging toolchain
|
||||
atop the CLI boundary, D3 worse than measured 658 µs (Wave 0 policy excludes it anyway);
|
||||
(4) https://go.dev/doc/install — Go is a pre-install prerequisite, D5=0 everywhere, never earned
|
||||
installation; (5) https://textual.textualize.io/ — Textual is a Python TUI framework,
|
||||
chat_bubble.py framework-pinned; (6) https://docs.python.org/3/library/sqlite3.html — sqlite3 is
|
||||
DB-API 2.0 over C, cache.py's perf layer already native. All fetched live 2026-09-29.
|
||||
- **⚠ Inventory drift (VERIFIED 2026-09-29):** complexity_scorer.py listed in
|
||||
rebase-inventory.json (2 mentions) but absent on disk — deleted in commit 867321e
|
||||
("governed retirement — complexity_scorer deleted (602 lines)"). Wave 0 inventory needs a
|
||||
correction pass; the module was scored nowhere (no source to characterize).
|
||||
|
||||
---
|
||||
|
||||
## Wave-plan consequence (T5 scope, from the verdict above)
|
||||
|
||||
With **0 recode units awarded**, the tranche's hypothesized rebase volume is real only as the
|
||||
work below. Waves 2–9 are **disposition + named improvements, NOT mass recoding**:
|
||||
|
||||
**Waves 2–7 — per-issue disposition (the 1,852 `[rebase w{wave}]` issues):**
|
||||
Each issue receives its tranche §7 disposition — improve / fix / missing-connectors answered
|
||||
with evidence — plus a language verdict of **KEEP-PYTHON with its rubric evidence line**
|
||||
(the round section for that subsystem names the claim). Disposition runs leaf-first
|
||||
(Wave 2 → 7), the canonical suite stays green throughout, and no issue closes without its
|
||||
evidence line. A recode is off the table unless a frozen kill test is reopened — and then
|
||||
tranche §6 still governs: **no benchmark win, no recode.** Go is not installed and earned no
|
||||
installation; Rust stays toolchain-ready but unjustified (standing option behind the
|
||||
CI-proven adapter).
|
||||
|
||||
**Named improvements — the actual rebase volume (ship alongside disposition):**
|
||||
1. **JSONL streaming reader for `shared/native_units.py`** (W7 contract gap): the adapter
|
||||
parses only the LAST stdout line; it must gain per-line `json.loads` + terminal-envelope
|
||||
validation before any recoded long-running unit could stream progress. Small, testable,
|
||||
non-breaking — first implementation issue of Wave 2.
|
||||
2. **httpx retry/timeout wrapper for the provider layer** (W4): the honest reliability
|
||||
upgrade is a feature-add, not a language change.
|
||||
3. **orjson upgrade path** (W3 cluster 1): the only credible Python-side JSON win —
|
||||
in-process, no boundary crossing. **DISPOSITION (2026-09-29, kill test EXECUTED and
|
||||
FAILED — KEEP-PYTHON stands, no dependency declared):** microbench on the live
|
||||
`~/.rune/consolidation/knowledge.json` (3,540,959 bytes, n=30 best-of): load 13.15 ms
|
||||
std vs 7.85 ms orjson = **1.68× (bar ≥2×: FAIL)**; dump 9.58 ms std vs 5.61 ms
|
||||
orjson-with-decode = **1.71× (bar ≥5×: FAIL)**; raw-bytes dump 1.39 ms = 6.91× but is
|
||||
not drop-in (every `write_text`/`json.dump`-to-file site would need a bytes→str or
|
||||
binary-write conversion). Roundtrip equality verified. Bench: `/tmp/orjson_bench.py`
|
||||
shape (uvx --with orjson). The bytes trap measured real: decode() eats ~60% of the
|
||||
dump win.
|
||||
4. **complexity_scorer.py inventory correction**: listed in `rebase-inventory.json` but
|
||||
deleted on disk (commit `867321e`, 602 lines) — Wave 0 inventory needs the correction
|
||||
pass. **DISPOSITION (2026-09-29, DONE):** inventory regenerated per the script's own
|
||||
header (never hand-edit) — 177→213 files, 1,852→2,314 functions, 13 dead files
|
||||
dropped; committed + pushed `5bc837c`. The W2 round's "2 mentions" drift claim
|
||||
verified FALSE against the live inventory (zero hits).
|
||||
5. **robfig/cron** (W6): recorded as a best-language footnote only; no action.
|
||||
|
||||
**Wave 8 — TUI integration proof:** demonstrate the TUI reading/utilising the mixed-language
|
||||
surfaces that exist (the `interop/rebase-proof` unit through the upgraded adapter), per the
|
||||
8-point adapter surface adopted above.
|
||||
**Wave 9 — close-out audit:** 100% of the 1,852 issues dispositioned, debt-table revision,
|
||||
version bump.
|
||||
|
||||
## Cross-cutting candidates (evaluated once, referenced by every subsystem)
|
||||
|
||||
- **Rust (present, 1.98.1):** interop proof unit already CI-proven (std-only, zero crates, `interop/rebase-proof/`, adapter in `shared/native_units.py`, ok = exit 0 AND not timed out).
|
||||
- **Go (absent):** must earn installation through D1–D5 evidence here before any recode targets it.
|
||||
- **C (present, GCC 16.2.1):** expected to lose D6 in most subsystems; scored where FFI-adjacent work exists.
|
||||
- **Python (KEEP baseline):** the default. Every subsystem begins here; evidence must move it.
|
||||
|
||||
## Evidence ledger (per-source lessons, all rounds)
|
||||
|
||||
Index of per-source evidence. Full annotations live in each round's
|
||||
`research/wave1/<subsystem>/outward-sources.md`; this ledger records the headline lesson
|
||||
each source contributed and the rubric claims it supports.
|
||||
|
||||
| Round | Sources (live-fetched 2026-09-29) | Headline lessons | Supports |
|
||||
|---|---|---|---|
|
||||
| W2 leaf utilities | 6: python.org 3.11 + 3.13 whatsnew, maturin.rs, go.dev/doc/install, textual.textualize.io, docs.python.org sqlite3 | CPython 3.11+ already 10–60% faster on helper-shaped code (PEP 659) — D2 baseline is a moving target; free-threading/JIT experimental, no bets; in-process bindings need a whole toolchain atop the CLI boundary (excluded by Wave 0 policy anyway); Go is pre-install prerequisite, D5=0; Textual framework-pins chat_bubble.py; sqlite3 perf layer already C | W2 KEEP-PYTHON ×~40; the 14,000× crossing-overhead law |
|
||||
| W3 memory | 6: orjson, sled, sqlite-vec, SQLite FTS5 docs, Go json/v2 rewrite issue, pydantic-core | orjson is the only credible Python-side JSON win (in-process, no boundary); SQLite FTS5/vec0 compute is already C; Go's JSON stack mid-rewrite upstream (versioning risk); pydantic-core shows the Rust-as-wheel pattern, excluded by policy | W3 KEEP-PYTHON 7/7; orjson upgrade path on cluster 1 |
|
||||
| W4 provider-layer | 8: live-fetched O1–O8 (HTTP/SSE ecosystem, tokio-stack precedents incl. mistral.rs, pkg.go.dev/net/http, ollama Go client/server) | stdlib-only Rust has no realistic HTTP/SSE precedent — the zero-crate contract decides Rust out of all network-wire clusters by construction; Go's stdlib HTTP+NDJSON streaming is capability-proven (its losses here are evidence-absence, not capability-absence); the honest reliability upgrade is a Python httpx retry/timeout wrapper — feature-add, not language change | W4 KEEP-PYTHON 5/5; SSE-cluster Rust kill-by-construction |
|
||||
| W5 orchestration | 4: pydantic-core, ruff blog, go.dev FAQ, Python 3.14 hashlib docs + 1 LOCAL EXPERIMENT (n=100 adapter benchmark) | adapter round-trips at 0.63ms/op — subprocess-per-op viable for coarse ops, never per-event; Python "slowness" dissolves where compute already runs in C via stdlib; the language question is really "where is the hot loop" (here: nowhere) | W5 KEEP-PYTHON 6/6; K1 kill test already run |
|
||||
| W6 runtime-core | headline lessons (full set in round outward-sources.md) | Rust std::process lacks built-in child timeout — strictly more code under zero-crate; Go os/exec reaches parity but changes only syntax; Python json/regex C-backed so rewrite swaps C for compiled code at equal asymptotic cost; serde typed structs mismatch schema-flexible model output; concurrency features irrelevant at ≤2 threads | W6 STAY-PYTHON 6/6; robfig/cron as best-language footnote |
|
||||
| W7 TUI | 8: Textual workers guide, ratatui vendor docs, Bubble Tea/tview/Codex TUI (countervailing), JSON Lines spec, push-granular-work guidance | Textual workers/`call_from_thread` are the core competency for event glue; ratatui admits "little to no help in organizing large applications"; countervailing native-TUI frameworks recorded honestly — do not outweigh 80+ module coupling; JSONL per-line parse + terminal sentinel is the streaming contract; vendor guidance: push granular speed-sensitive work to subcomponents only when measured | W7 KEEP-PYTHON 4/4; the 8-point adapter surface (adopted as the rubric's interop-contract restatement) |
|
||||
@@ -0,0 +1,50 @@
|
||||
# Wave 0 close-out report — language-rebasing tranche R1
|
||||
|
||||
Branch: `tranche-r1-wave-0` · Date: 2026-09-29
|
||||
|
||||
## 1. Inventory (docs/rebase/rebase-inventory.json)
|
||||
|
||||
- 177 Python modules, ~52,750 lines, 227 classes
|
||||
- TRUE unit count: **1,852 = 1,030 top-level functions + 822 class methods**
|
||||
- Zero duplicate call edges, zero method leaks (generator double-walk defect
|
||||
root-caused and fixed in scripts/rebase_inventory.py)
|
||||
- Cross-verified against an independent fresh AST walk: zero missing units;
|
||||
six inventory-"extras" are real nested/if-try-block classes the naive walk
|
||||
misses (inventory is MORE complete, not less)
|
||||
|
||||
## 2. Issue factory (scripts/rebase_issue_factory.py)
|
||||
|
||||
- One issue per unit following R1 §7; wave-batched (2–7); dedup-safe via
|
||||
deterministic title `[rebase w{wave}] {relpath}::{unit}` + existing-titles
|
||||
scan per run (retries never duplicate — preserve-first)
|
||||
- `plan` → {2: 239, 3: 251, 4: 129, 5: 544, 6: 415, 7: 274} = 1,852,
|
||||
zero unassigned
|
||||
- `sample --wave 5` → full §7 template holds; `_compute_hash` exactly once as
|
||||
labelled callee with line number
|
||||
- Contract fix during Wave 0: `--limit` default changed 5 → 0 (unlimited) for
|
||||
`create`; `sample` keeps its 5. A full-wave create with the old default
|
||||
would have silently truncated each wave to five issues.
|
||||
|
||||
## 3. Interop proof unit (R1 §5)
|
||||
|
||||
- Rust unit at interop/rebase-proof/ (std-only, zero crates): argv in → JSON
|
||||
on stdout, non-zero exit on failure, mandatory `--version`
|
||||
- Shell-proven: version flag, JSON out, exit 2 on failure, stderr diagnostic
|
||||
- Python thin adapter shared/native_units.py: typed NativeResult, timeout +
|
||||
stderr capture, unit failures as data (never exceptions)
|
||||
- Adapter contract fix during Wave 0: `ok` no longer requires a JSON
|
||||
`ok:true` field — plain-text output (`--version`) succeeds on exit code
|
||||
- tests/test_native_interop_proof.py: **8 passed** — version contract, sum
|
||||
happy path, hash determinism, failure paths, timeout, stderr capture
|
||||
|
||||
## 4. Per-wave issue creation
|
||||
|
||||
<!-- FILLED BY THE COMPLETION LOOP — do not hand-edit -->
|
||||
|
||||
- Wave 4 (129 units): created=127 skipped(dedup)=2 (includes smoke #417–418)
|
||||
- Wave 2 (239 units): created=239 skipped(dedup)=0
|
||||
- Wave 3 (251 units): created=251 skipped(dedup)=0
|
||||
- Wave 5 (544 units): created=544 skipped(dedup)=0
|
||||
- Wave 6 (415 units): created=415 skipped(dedup)=0
|
||||
- Wave 7 (274 units): created=274 skipped(dedup)=0
|
||||
- TOTAL: 1,850 created + 2 dedup = 1,852 / 1,852 ✅ full coverage
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"units_per_wave": {
|
||||
"2": 239,
|
||||
"3": 251,
|
||||
"4": 129,
|
||||
"5": 544,
|
||||
"6": 415,
|
||||
"7": 274
|
||||
},
|
||||
"unassigned_modules": {},
|
||||
"unassigned_module_count": 0
|
||||
}
|
||||
@@ -10,6 +10,7 @@ new surface._
|
||||
| `rune_tui.py` | The main Textual application — chat pane, work pane, status bar (version label + refresh), footer, dialogs | The known giant (file-size debt tracked in henxels); composition root for all UI |
|
||||
| `tui_validator.py` | Import-allowlist validation for TUI code | Drift defect tracked in #139 (allowlist vs `__future__`/relative imports) |
|
||||
| `tui_backlog.py` | Not a surface — the durable TUI Upgrade Backlog ledger (outstanding projection/control work per backend feature) | SCAFFOLD-TRANCHE-007; ledger only, never structural authority |
|
||||
| `tui_transport.py` | Not a surface — the transport seam for the TUI's operator calls (in-process default / /v1 wire opt-in) | H4 slice 1 (contract BUILDER-2026-10-04T10:32:00Z-cli); RUNE_TUI_TRANSPORT selects; coverage: canary_tui_transport.py |
|
||||
|
||||
## Adding a TUI surface
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1.27.1
|
||||
+26
-2
@@ -1,7 +1,31 @@
|
||||
settings:
|
||||
warn_about_similar_files:
|
||||
above: 0.85
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**"]
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**",
|
||||
# shared/ is the DEPLOYED-VINTAGE mirror tree (strangler era):
|
||||
# every src/rune <-> shared similarity here is intentional
|
||||
# (the shim delegates to the crate; the mirror is the deployed
|
||||
# vintage it strangles). Registering the deliberate pairs.
|
||||
"shared/**",
|
||||
"src/rune/drift_core.py", "src/rune/drift_compression.py",
|
||||
"src/rune/drift_observability.py", "src/rune/shard_manager.py",
|
||||
"src/rune/subconscious_pass.py",
|
||||
# deliberate mirror pairs outside the strangler set:
|
||||
# issue templates are mirrored into shared/issue_templates;
|
||||
# completion_file.py is the bridge/daemon mirror of scripts/;
|
||||
# interop/rebase-proof is a committed proof fixture.
|
||||
".gitea/ISSUE_TEMPLATE/**", "shared/issue_templates/**",
|
||||
"bridge/daemon/completion_file.py", "scripts/completion_file.py",
|
||||
"interop/rebase-proof/**",
|
||||
# per-crate copies by zero-dep design: each crate is
|
||||
# self-contained (no shared crate dependency), so the pyjson
|
||||
# JSON module is copied verbatim per crate and the tiny
|
||||
# zero-dep lockfiles are near-identical by construction.
|
||||
"src/rust/*/Cargo.lock", "src/rust/*/src/pyjson.rs",
|
||||
"src/rust/query_memory/src/json_min.rs",
|
||||
# store/scaffold is data (proof fixtures, gate objects),
|
||||
# same class as the already-ignored store/spine.
|
||||
"store/scaffold/**"]
|
||||
|
||||
henxels:
|
||||
- henxel: "Canaries live at the repo's canary slot and are registered in CI"
|
||||
@@ -20,7 +44,7 @@ henxels:
|
||||
- henxel: "Top-level docs are markdown or yaml references"
|
||||
in: ./docs/*
|
||||
allowed_filetypes: [.md, .yaml]
|
||||
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json"]
|
||||
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json", "./docs/rebase/**"]
|
||||
|
||||
- henxel: "The known giant stays visible — file-size debt is tracked, not silent"
|
||||
in: ./src/rune/*
|
||||
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "rebase-proof"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "rebase-proof"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# Wave 0 interop proof unit (R1 §5). Std-only by design: no crates, no network,
|
||||
# deterministic build. The pattern under proof is the CLI boundary itself, not the
|
||||
# computation inside it.
|
||||
|
||||
[[bin]]
|
||||
name = "rebase-proof"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Wave 0 interop proof unit (R1 §5 of the language-rebasing tranche).
|
||||
//!
|
||||
//! Contract under proof:
|
||||
//! - standalone binary; argv in, JSON on stdout
|
||||
//! - non-zero exit on failure (JSON error object still printed to stdout)
|
||||
//! - `--version` flag mandatory
|
||||
//! - Python wraps it in a thin adapter (`shared/native_units.py`); tests call
|
||||
//! it the same way.
|
||||
//!
|
||||
//! Ops: sum / echo / hash (fnv1a64) / sleep / fail. All std-only.
|
||||
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// FNV-1a 64-bit — dependency-free deterministic hash for the proof unit.
|
||||
fn fnv1a64(data: &[u8]) -> u64 {
|
||||
let mut hash: u64 = 0xcbf29ce484222325;
|
||||
for byte in data {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn print_json(line: &str) {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
fn emit_ok(body: &str) {
|
||||
print_json(&format!("{{\"ok\":true,{body}}}"));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fn emit_err(message: &str) -> ! {
|
||||
// Escape the minimal set so the JSON stays parseable for arbitrary op names.
|
||||
let escaped: String = message
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'"' => "\\\"".to_string(),
|
||||
'\\' => "\\\\".to_string(),
|
||||
'\n' => "\\n".to_string(),
|
||||
'\r' => "\\r".to_string(),
|
||||
'\t' => "\\t".to_string(),
|
||||
other => other.to_string(),
|
||||
})
|
||||
.collect();
|
||||
print_json(&format!("{{\"ok\":false,\"error\":\"{escaped}\"}}"));
|
||||
exit(2);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
|
||||
if args.as_slice() == ["--version"] || args.as_slice() == ["-V"] {
|
||||
println!("rebase-proof {VERSION}");
|
||||
exit(0);
|
||||
}
|
||||
if args.is_empty() {
|
||||
eprintln!("usage: rebase-proof <op> [args...] | --version");
|
||||
emit_err("no op given");
|
||||
}
|
||||
|
||||
let op = args[0].as_str();
|
||||
let rest = &args[1..];
|
||||
|
||||
match op {
|
||||
"sum" => {
|
||||
let mut total: i128 = 0;
|
||||
for a in rest {
|
||||
match a.parse::<i128>() {
|
||||
Ok(v) => total += v,
|
||||
Err(_) => emit_err(&format!("sum: not an integer: {a}")),
|
||||
}
|
||||
}
|
||||
emit_ok(&format!("\"op\":\"sum\",\"result\":{total}"));
|
||||
}
|
||||
"echo" => {
|
||||
let joined = rest.join(" ");
|
||||
let escaped: String = joined
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'"' => "\\\"".to_string(),
|
||||
'\\' => "\\\\".to_string(),
|
||||
'\n' => "\\n".to_string(),
|
||||
'\r' => "\\r".to_string(),
|
||||
'\t' => "\\t".to_string(),
|
||||
other => other.to_string(),
|
||||
})
|
||||
.collect();
|
||||
emit_ok(&format!("\"op\":\"echo\",\"text\":\"{escaped}\""));
|
||||
}
|
||||
"hash" => {
|
||||
let joined = rest.join(" ");
|
||||
let h = fnv1a64(joined.as_bytes());
|
||||
emit_ok(&format!("\"op\":\"hash\",\"fnv1a64\":\"{h:016x}\""));
|
||||
}
|
||||
"sleep" => {
|
||||
let ms: u64 = match rest.first().map(|s| s.parse::<u64>()) {
|
||||
Some(Ok(v)) => v,
|
||||
_ => emit_err("sleep: expected milliseconds"),
|
||||
};
|
||||
thread::sleep(Duration::from_millis(ms));
|
||||
emit_ok(&format!("\"op\":\"sleep\",\"slept_ms\":{ms}"));
|
||||
}
|
||||
"fail" => {
|
||||
// Deliberate failure: stderr diagnostic + JSON error + exit 2.
|
||||
// Proves stderr capture and non-zero exit in the adapter.
|
||||
eprintln!("rebase-proof: deliberate failure for adapter proof");
|
||||
emit_err("deliberate failure");
|
||||
}
|
||||
other => emit_err(&format!("unknown op: {other}")),
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 347,
|
||||
"BLE001": 345,
|
||||
"S110": 75
|
||||
}
|
||||
@@ -1 +1,401 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"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).",
|
||||
"capabilities": [
|
||||
"read_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path exists and is a readable file"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "read_file read a file with optional windowed viewing (offset/limit). returns numbered lines. use offset/limit for large files (100 lines at a time). native tool"
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
|
||||
"capabilities": [
|
||||
"edit_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"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"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "edit_file surgical search/replace edit. more reliable than full file rewrites. the search string must match exactly. native tool"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"capabilities": [
|
||||
"write_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"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"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "write_file write content to a file. creates parent directories if needed. shows diff before writing. use for new files or full rewrites. native tool"
|
||||
},
|
||||
{
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
||||
"capabilities": [
|
||||
"glob_files"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "glob_files find files matching a glob pattern. returns file paths sorted by modification time. native tool"
|
||||
},
|
||||
{
|
||||
"name": "grep_files",
|
||||
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
|
||||
"capabilities": [
|
||||
"grep_files"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "grep_files search for a pattern in files. returns file paths and line numbers. use include to filter by file type. native tool"
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "List files and directories at a path. Returns names with / suffix for directories.",
|
||||
"capabilities": [
|
||||
"list_directory"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path is a readable directory"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "list_directory list files and directories at a path. returns names with / suffix for directories. native tool"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"capabilities": [
|
||||
"lint_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"file exists",
|
||||
"a linter is configured for the language"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "lint_file lint a file for syntax errors and style issues. blocks bad edits. supports python (ruff), js/ts (eslint), go (golangci-lint), and more. native tool"
|
||||
},
|
||||
{
|
||||
"name": "run_tests",
|
||||
"description": "Run tests for a file or project. Auto-detects test runner from language.",
|
||||
"capabilities": [
|
||||
"run_tests"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"a test runner is configured for the project"
|
||||
],
|
||||
"side_effects": [
|
||||
"runs project test commands which may create or delete files"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "run_tests run tests for a file or project. auto-detects test runner from language. native tool"
|
||||
},
|
||||
{
|
||||
"name": "format_code",
|
||||
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
|
||||
"capabilities": [
|
||||
"format_code"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"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"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "format_code auto-format code files. supports python (black/ruff), js/ts (prettier), go (gofmt), and more. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Show git diff for staged or unstaged changes.",
|
||||
"capabilities": [
|
||||
"git_diff"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_diff show git diff for staged or unstaged changes. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Show recent git commits.",
|
||||
"capabilities": [
|
||||
"git_log"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_log show recent git commits. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit changes with a message. Auto-stages modified files.",
|
||||
"capabilities": [
|
||||
"git_commit"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"working directory is a git repository",
|
||||
"there are changes to commit"
|
||||
],
|
||||
"side_effects": [
|
||||
"creates a commit on the current branch"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_commit commit changes with a message. auto-stages modified files. native tool"
|
||||
},
|
||||
{
|
||||
"name": "repo_map",
|
||||
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
|
||||
"capabilities": [
|
||||
"repo_map"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"target directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "repo_map generate an ast-based codebase map showing key symbols (classes, functions, imports). uses tree-sitter for 100+ languages. native tool"
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
|
||||
"capabilities": [
|
||||
"run_command"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"operator approves the command",
|
||||
"required binaries are on PATH"
|
||||
],
|
||||
"side_effects": [
|
||||
"executes arbitrary commands with their full side effects"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "run_command run an approved command. prefer argv (shell=false); shell syntax requires shell=true and explicit user approval. native tool"
|
||||
},
|
||||
{
|
||||
"name": "read_persona",
|
||||
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
|
||||
"capabilities": [
|
||||
"read_persona"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"persona MEMORY.md exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "read_persona read rune's persona definition. returns the full persona block with voice, identity, and rules. native tool"
|
||||
},
|
||||
{
|
||||
"name": "query_memory",
|
||||
"description": "Query Rune's memory store. Returns relevant memories for a query.",
|
||||
"capabilities": [
|
||||
"query_memory"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"memory store database is available"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "query_memory query rune's memory store. returns relevant memories for a query. native tool"
|
||||
},
|
||||
{
|
||||
"name": "check_growth",
|
||||
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
|
||||
"capabilities": [
|
||||
"check_growth"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"growth log file exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "check_growth check rune's growth log. returns recent growth entries and patterns. native tool"
|
||||
},
|
||||
{
|
||||
"name": "diagnose_behavior",
|
||||
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
|
||||
"capabilities": [
|
||||
"diagnose_behavior"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"recent response text provided"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "diagnose_behavior diagnose rune's current behavior. returns analysis of persona adherence, voice consistency, and drift. native tool"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"capabilities": [
|
||||
"find_capability"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"a capability query describing the needed outcome"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "find_capability 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. native tool"
|
||||
}
|
||||
]
|
||||
+38886
-15
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,48 @@
|
||||
# Phase G slice 1 — retrieval-index authority supersession ledger
|
||||
|
||||
**Date:** 2026-10-02 · **Branch:** `phase-g-strangler-retrieval-index` · **Unit issue:** conductor/rune#2448
|
||||
|
||||
## What was superseded
|
||||
|
||||
The pure-Python `query_memory` implementation (formerly `src/rune/query_memory.py`,
|
||||
deployed at `~/.config/opencode/scripts/query_memory.py`) is RETIRED as a production
|
||||
implementation. Production authority now rests with:
|
||||
|
||||
1. **Implementation:** Rust crate `src/rust/query_memory/` (lib.rs core + cdylib, ten
|
||||
`extern "C"` FFI symbols, zero external crates).
|
||||
2. **Production service surface:** `shared/query_memory.py` — a ctypes-backed shim
|
||||
(179 lines) over `qm_loader.py`, byte-deployed to `~/.config/opencode/scripts/query_memory.py`,
|
||||
serving the FULL prior Python class API including the `self.history` snapshot.
|
||||
|
||||
## Strangler doctrine (master doc lines 685–770) — step discharge
|
||||
|
||||
| Step | Requirement | Evidence |
|
||||
|------|-------------|----------|
|
||||
| 1–4 | candidate built, contract frozen, parity proven | commits `9b76585`+`54d2db9`; `/tmp/phaseG_retrieval_index_contract.md` frozen; parity harness ALL GREEN |
|
||||
| 5 | qualification | `/tmp/phaseG_qual_bench.out` (Rust CLI ~45× faster end-to-end); canaries both sides |
|
||||
| 6 | canaries conformance | Python canary 12/12; Rust canary 27/27 |
|
||||
| 7 | shadow comparison vs live store | `/tmp/phaseG_shadow.py` 46/46 exact-equal vs 77-record production store |
|
||||
| 8 | authority switch + observed consumer evidence | commit `6f44a87`; both consumers verified live — `memory_store.py` (lines 820–821, live 76-record store, nested stats shape) + `growth_simulator.py` (record_query through Rust core); #2448 comment id 33795 |
|
||||
| 9 | runtime evidence post-switch | post-switch verification wall ALL GREEN: Python canary 12/12, FFI smoke 41/41, Rust canary 27/27, parity harness ALL GREEN |
|
||||
| 10 | old production path unreachable | ALL import paths (`import query_memory`) now resolve to the deployed shim; `shared/graph_shard_bridge.py` tolerant import serves the shim; the orphan `src/rune/query_memory.py` had ZERO importers (verified by sweep incl. path-string references) |
|
||||
| 11 | old implementation removed or reference-only | `src/rune/query_memory.py` DELETED (this commit); the only stale reference (`scripts/structural_coverage_audit.py` registry mapping shared/ as a mirror of src/rune) corrected — query_memory removed from `SRC_A`/`SHARED_D`, added to `SHARED_B` (canonical, ops-harness owned) |
|
||||
| 12 | supersession ledger | THIS FILE |
|
||||
|
||||
Deletion discipline (language-governance rule ⑥) satisfied: old code removed only after
|
||||
the new implementation was PROVEN — wall green, shadow 46/46, both production consumers
|
||||
observed live on the Rust core before deletion.
|
||||
|
||||
## Consumer map at supersession
|
||||
|
||||
- `~/.config/opencode/scripts/memory_store.py` — in-process, `adaptive_exploration_rate` (line 820).
|
||||
- `~/.config/opencode/scripts/growth_simulator.py` — in-process, `record_query`.
|
||||
- `shared/graph_shard_bridge.py` — tolerant import (try/except), follows the switch.
|
||||
- `src/rune/tool_executor.py` — NOT a consumer (nominal grep hit only; goes through `memory_store.cached_recall`).
|
||||
|
||||
## Verification commands (reproducible)
|
||||
|
||||
- `/home/nick/.venvs/memory/bin/python3 src/rune/canary_query_memory.py` → 12/12
|
||||
- `/home/nick/.venvs/memory/bin/python3 src/rust/query_memory/canary_rust.py` → 27/27
|
||||
- `/home/nick/.venvs/memory/bin/python3 src/rune/ffi_query_memory_smoke.py` → 41/41
|
||||
- `/home/nick/.venvs/memory/bin/python3 /tmp/phaseG_parity.py` → ALL GREEN
|
||||
- `/home/nick/.venvs/memory/bin/python3 scripts/structural_coverage_audit.py` → exit 0, coverage 100%
|
||||
@@ -0,0 +1,66 @@
|
||||
# Phase G Slice 2 — FROZEN CONTRACT: evidence-provenance (T7.3)
|
||||
|
||||
Frozen: 2026-10-03, session ses_ffe5f08a7892affeGzZWhhm0wR. Branch `phase-g-strangler-evidence-provenance` (cut from slice-1 head `2ac2d0a`). Gitea issue #2449. Nothing below may change without a recorded amendment in this file.
|
||||
|
||||
## 1. Scope (observed, not inferred)
|
||||
|
||||
Six modules in `src/rune/`, 1796 lines total, one test file each as marked:
|
||||
|
||||
| Module | Lines | Surface (grep-verified 2026-10-03) |
|
||||
|---|---|---|
|
||||
| `audit_ledger.py` | 231 | `_compute_hash(prev_hash, entry)`, `append_entry(entry_type, entry_id, details)`, `verify_chain() -> {valid, entries, reason}`, `get_entries(limit=50)`, `log_promotion(entry_id, old_status, new_status, wash_count, reason)`, `log_gate_result(entry_id, verdict, critics)`, `log_drift_alert(alert_type, message, severity)`, `main()` |
|
||||
| `behavior.py` | 411 | `connect()`, `add_correction(original, corrected, scope_tags=None, domain="", pattern="")`, `add_habit(trigger, action, name="", context_tags=None, min_activations=3)`, `match_context(context, match_type=None)`, `reinforce(item_id, outcome, item_type="habit")`, `list_items(item_type="all", limit=20)`, `behavior_stats()`, `main()`, `PLUGIN` dict |
|
||||
| `behaviour_ledger.py` | 251 | 9 state constants (UNVALIDATED…NOT_APPLICABLE) + `STATES` + `CONFIRMED_STATES` tuples, `BehaviourLedger` class |
|
||||
| `claim_audit.py` | 430 | `PATTERNS` list, `EVIDENCE_TAG` regex, `LEDGER_MARKERS`, `reframe(text)`, `scan(text)`, `verify(claim)`, `degrade(claim, verdict)`, `journal(entry)`, `report()`, `audit(draft)`, `handle_hook(event, data=None)`, `main()`, `PLUGIN` |
|
||||
| `evidence_probe.py` | 214 | `PROBE_STATUSES`, `PROBE_FIELDS`, `MAX_PROBES_PER_TASK=2`, `ProbeBudgetExceeded`, `ProbeResult`, `build_probe_prompt(question, artifact_path)`, `validate_probe(artifact)`, `count_probes(artifact_dir)`, `check_budget(artifact_dir)`, `record_probe(result, artifact_dir)`, `run_probe(question, artifact_dir, runner=None, model="UNKNOWN")` |
|
||||
| `verification_layer.py` | 259 | `VerificationResult`, seven `verify_*` functions (exit_code, unit_test, schema, file_exists, file_changed, tool_receipt, constraint_check), `verify(result, verifier_type)`, `handle_hook`, `PLUGIN` |
|
||||
|
||||
Plus `shared/audit_ledger.py` (210 ln) — OLDER vintage of audit_ledger.
|
||||
|
||||
## 2. Authority decisions (per module)
|
||||
|
||||
- **src/rune/ is the authority vintage for all six modules** — hardened newer code (memory_env-pathed storage, defensive handling). The deployed `~/.config/opencode/scripts/{audit_ledger,behavior,claim_audit}.py` copies are OLDER vintages (hardcoded expanduser paths). The authority switch is a VINTAGE UPGRADE plus a language swap — not a mirror replacement.
|
||||
- `shared/audit_ledger.py` is superseded alongside the deployed copy (same older vintage, same defects).
|
||||
|
||||
## 3. Live-data contract (the linchpin — observed)
|
||||
|
||||
- **Live ledger:** `/home/nick/.config/opencode/memory/promotion_ledger.jsonl` (21 entries). `verify_chain()` under the deployed module's own defaults returns `{'valid': True, 'entries': 21, 'reason': 'chain intact'}` — OBSERVED 2026-10-03.
|
||||
- **Live signing key: `rune-drift-integrity-2024`** (the deployed/shared default). The src/rune default `rune-governed-adaptation-2024` FAILS the live chain — it is a latent break, recorded as a defect this slice fixes.
|
||||
- **Hash scheme (both vintages identical):** `HMAC-SHA256(key, (prev_hash + json.dumps(entry_without_hash, sort_keys=True)).encode()).hexdigest()`; genesis prev_hash = `"genesis"`; entry lines written with `json.dumps(entry)` in insertion order (type, entry_id, details, timestamp, prev_hash, hash); timestamp = `datetime.now(timezone.utc).isoformat()`.
|
||||
- **Key rule for the Rust candidate:** read `AUDIT_LEDGER_KEY` env; DEFAULT = `rune-drift-integrity-2024` (the key that verifies the live chain). Never default to the governed key — it verifies nothing that exists.
|
||||
- **Live `behavior.db`:** SQLite at memory_env memory dir, touched Sep 24 — must remain readable by the Rust candidate's storage layer.
|
||||
|
||||
## 4. Rust candidate design
|
||||
|
||||
Crate `/mnt/workdrive/rune/src/rust/evidence_provenance/` following the proven slice-1 shape: zero-dep core in `lib.rs`, `cdylib` FFI exports, hand-rolled JSON serialization where byte-parity demands it. **Port order within the slice** (dependency + risk order):
|
||||
|
||||
1. **audit_ledger** — linchpin: live data, external consumers, key contract.
|
||||
2. **verification_layer** — pure logic, no storage, one test file; fastest port.
|
||||
3. **evidence_probe** — file-based budget/records; `rune_operator.py:581` consumer.
|
||||
4. **behaviour_ledger** — state machine, src/rune-only.
|
||||
5. **behavior** — SQLite habits; live DB; deployed vintage upgrade + `rune_tui.py` consumer.
|
||||
6. **claim_audit** — heaviest (warm-neuron store + graph DB + regex engine); last.
|
||||
|
||||
Python-side ctypes loaders (`*_loader.py`) + deployed shims per slice-1 pattern, so every consumer keeps its import surface unchanged.
|
||||
|
||||
## 5. Parity + qualification criteria
|
||||
|
||||
- **Byte-parity:** Rust `verify_chain` output byte-identical to Python's on the live ledger AND on a mutation battery (tampered entry, broken chain, invalid JSON, empty ledger). Parity harness both sides, slice-1 pattern.
|
||||
- **Canaries:** Python-side canary module per ported module + Rust canary; both green before any switch.
|
||||
- **Shadow:** shadow-compare Rust vs Python on live-shaped data before authority switch.
|
||||
- **Performance:** like-for-like benchmark on the real consumption shape; no regression (Nick's law: combination does not decrease speed).
|
||||
|
||||
## 6. Consumers + adoption plan (Nick's directive: adoption during the refactor)
|
||||
|
||||
| Consumer | Imports | Switch |
|
||||
|---|---|---|
|
||||
| `~/.config/opencode/scripts/curator.py` | audit_ledger | at audit_ledger switch |
|
||||
| `canary_tui_drift.py`, `canary_full_system.py` | audit_ledger | at audit_ledger switch |
|
||||
| `rune_tui.py` (L62, L83) | claim_audit, behavior | at each module's switch |
|
||||
| `rune_operator.py:581` | evidence_probe (relative) | at evidence_probe switch |
|
||||
|
||||
Deployed copies become ctypes shims at each module's authority switch (vintage upgrade included — the shim serves the src/rune contract, not the old vintage's). Old deployed Python deletions follow language-governance ⑥: only after the new is proven.
|
||||
|
||||
## 7. Exit criteria
|
||||
|
||||
All twelve doctrine steps discharged per module-port with observed evidence on #2449; coverage-audit registry remapped; supersession ledger written; audit regression gate green.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Phase G slice 2 — evidence-provenance authority supersession ledger
|
||||
|
||||
**Date:** 2026-10-03 · **Branch:** `phase-g-strangler-evidence-provenance` · **Unit issue:** conductor/rune#2449
|
||||
|
||||
## What was superseded
|
||||
|
||||
The pure-Python evidence-provenance implementations (six modules in `src/rune/`, 1796 lines; plus the two OLD deployed vintages and `shared/audit_ledger.py`) are RETIRED as production implementations for their ported surfaces. Production authority now rests with:
|
||||
|
||||
1. **Rust crate `src/rust/evidence_provenance/`** — six cores: `lib.rs` (audit_ledger chain + FFI), `verification.rs` (7 verifiers), `probe.rs` (schema gate + budget), `behaviour.rs` (state machine), `habitmath.rs` (reinforce math), `claims.rs` (claim-audit decision skeleton); cdylib FFI (`ep_*` symbols, 21 total), zero external crates.
|
||||
2. **Python shims** at `src/rune/{audit_ledger,verification_layer,evidence_probe,behaviour_ledger,behavior,claim_audit}.py` — serving the src/rune authority contract through `ep_loader.py` (in-process, no interpreter startup for the ported logic).
|
||||
3. **Deployed shims** at `~/.config/opencode/scripts/{audit_ledger,behavior,claim_audit}.py` — same shape; audit_ledger + claim_audit + behavior carry the LIVE data-path rules (drift-key ledger, live behavior.db, real journal); claim_audit additionally carries the preserved SYMMETRY GUARD (deployed-vintage feature) and the repaired `scan_text()` TUI surface.
|
||||
|
||||
## Port ledger (all six, in contract §4 order)
|
||||
|
||||
| Port | Module | Rust core | Parity | Commit | Consumer proof |
|
||||
|------|--------|-----------|--------|--------|----------------|
|
||||
| 1 | audit_ledger | lib.rs chain ops | shadow 18/18; canaries 28/28+23/23 | `114c394`+`4b0d1d2` | live ledger entries 4242 (post-switch write), verify 23-intact |
|
||||
| 2 | verification_layer | verification.rs | 33/33 (7 verifiers + CLI render) | `9cb93e6` | contracts.py, canary_native, rune_operator:323 dataclass |
|
||||
| 3 | evidence_probe | probe.rs | 24/24 + consumer tests 23/23 | `3450a1c` | rune_operator:581/603, research_stage_executor:312 |
|
||||
| 4 | behaviour_ledger | behaviour.rs | 13/13 full-scenario | `8973caa` | testing_mode:19, iteration:23, rune_tui:3152 |
|
||||
| 5 | behavior | habitmath.rs | 16/16 + full-stack live-db | `2732677` | rune_tui:83; deployed vintage upgraded (live db read-verified) |
|
||||
| 6 | claim_audit | claims.rs | 17/17 + real-journal aggregate (1127 rows) | `969e39f` | rune_tui:62/2468/3752, contracts.py, hook_engine:270 |
|
||||
|
||||
Errors: byte-equal (duplicate/unknown-behaviour/unknown-state; probe schema-gate lists incl. Python repr rendering; CLI argparse shapes + exit codes).
|
||||
|
||||
## Strangler doctrine (master doc lines 685–770) — step discharge (slice level)
|
||||
|
||||
| Step | Requirement | Evidence |
|
||||
|------|-------------|----------|
|
||||
| 1–4 | candidate built, contract frozen, parity proven | commits `114c394` → `969e39f`; frozen contract `plans/2026-10-03-phase-g-evidence-provenance-contract.md` (commit `7c68e41`); per-port parity harnesses ALL GREEN |
|
||||
| 5 | qualification | per-port parity + decision-math batteries; canary pair per port (Python 74/74, Rust 66/66 — all six port sections accumulated); performance follows the slice-1 in-process shape (no interpreter startup; same cdylib) |
|
||||
| 6 | canaries conformance | canary_audit_ledger.py 74/74; canary_evidence_provenance_rust.py 66/66 (per-port sections for ALL six ports; dual-mode LIVE/ISOLATED for live-ledger pins — run 18141 repair); embedded tests through shims (verification test, 23 consumer tests) |
|
||||
| 7 | shadow comparison vs live-shaped data | port-1 shadow 18/18 vs `939883d:shared/audit_ledger.py` (banked at `114c394`); ports 2–6 use direct byte-parity batteries vs the live originals (same discipline, stronger: full-document byte-quality) |
|
||||
| 8 | authority switch + observed consumer evidence | shims deployed + live-verified per port: entry 4242 (first post-switch ledger write), live behavior.db read through the port-5 shim, real 1127-row journal through the port-6 aggregation, TUI backend trio imports, TUI scan_text FIRST-EVER live call |
|
||||
| 9 | runtime evidence post-switch | post-switch walls per port: parity round-trips, embedded tests, consumer chains (rune.behaviour_ledger / rune.evidence_probe / rune.claim_audit + deployed trio) — all GREEN |
|
||||
| 10 | old production path unreachable | `import audit_ledger` (deployed consumers) → shim; `rune.behaviour_ledger` / `rune.evidence_probe` / `rune.claim_audit` / `rune.verification_layer` → shims; `shared/audit_ledger.py` had ZERO importers (verified by sweep incl. path-string forms) — DELETED |
|
||||
| 11 | old implementation removed or reference-only | `shared/audit_ledger.py` DELETED (this commit, after the proven wall + zero-importer sweep); six src/rune modules remain as shims (the API contract); original Python lives in git history. Coverage registry remapped: `audit_ledger` removed from SHARED_D; gate exit 0 post-deletion |
|
||||
| 12 | supersession ledger | THIS FILE |
|
||||
|
||||
Deletion discipline (language-governance rule ⑥): `shared/audit_ledger.py` removed only after (a) the deployed shim proved live against the real ledger, (b) the shadow wall was green, (c) a zero-importer sweep across src/ shared/ scripts/ tests/. The remaining five src/rune originals were rewritten AS shims in their own commits (git history holds each original); they are the API contract and stay.
|
||||
|
||||
## Vintage-merge disclosures
|
||||
|
||||
- **SYMMETRY GUARD preserved:** the deployed `claim_audit` vintage carried `research_lens.symmetry_check` integration (2026-08-15) absent from src/rune — preserved in both shims (feature-preserving merge; load-bearing behavior).
|
||||
- **DEAD CONSUMER REPAIRED:** `rune_tui.py:3752` `claim_audit.scan_text()` had NO implementation in either vintage (silent try/except death) — now implemented, first-class, live-verified.
|
||||
- **Sequence break (port 1):** the audit_ledger shim was deployed BEFORE its canary pair existed; both canaries subsequently written and green — disclosed on #2449 comment 33817, not retrofitted.
|
||||
|
||||
## Consumer map at supersession
|
||||
|
||||
- `~/.config/opencode/scripts/rune_tui.py` — claim_audit (report L2468, scan_text L3752), behaviour (L3152), behavior (L83).
|
||||
- `src/rune/rune_operator.py` — evidence_probe (L581/603), VerificationResult dataclass (L323).
|
||||
- `src/rune/contracts.py` — dynamic `__import__` surfaces (verification_layer, claim_audit).
|
||||
- `src/rune/testing_mode.py:19` / `src/rune/iteration.py:23` — behaviour_ledger constants + class.
|
||||
- `scripts/research_stage_executor.py:312` — evidence_probe orchestration.
|
||||
- `~/.config/opencode/scripts/curator.py` / `canary_full_system.py` — audit_ledger (deployed shim).
|
||||
- `src/rune/dependency_tracker.py` — map-strings only (NOT an import).
|
||||
|
||||
## Live-data rules (contract §3) — honored in the shims
|
||||
|
||||
- Live promotion ledger: drift key verified 23-intact through the shim + FFI; explicit `AUDIT_LEDGER_KEY` override honored.
|
||||
- Live behavior.db: read/written through the deployed shim at `~/.config/opencode/memory/behavior.db`; parity scenarios ran on temp dirs only.
|
||||
- Live claim_audit journal (1127 rows): read-only aggregate through the Rust core; writes stay Python.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Phase H plan — Go front end / Rust back end (Nick's architecture directive)
|
||||
|
||||
Status: PLAN (awaiting Nick's sign-off before any code)
|
||||
Directive: USER-LOCKED (2026-10-04). Supersedes the KEEP-PYTHON verdicts in
|
||||
docs/rebase-language-rubric.md wherever they conflict.
|
||||
|
||||
## 1. The directive
|
||||
|
||||
Go owns the front end: runtime entry points, the first-call surface, the
|
||||
user-facing interface and its calls. Rust owns the back end: every core
|
||||
logic module. Python survives only as transitional glue inside a module
|
||||
until its own Go/Rust surface lands.
|
||||
|
||||
## 2. What "front end" means concretely here
|
||||
|
||||
From the runtime-audit memory (2026-09-10): the live entry chain is
|
||||
rune_tui -> send_task -> ContextBroker -> ModelRouter -> local/Cloud
|
||||
transports. The user-facing calls are: the TUI process itself, the CLI
|
||||
entry (python -m rune / rune binary), the operator message path, and the
|
||||
OpenAI-compatible /v1 endpoints MiMoCode serves.
|
||||
|
||||
Candidates for the Go first-call layer, in dependency order:
|
||||
- rune binary (process entry, arg parsing, process lifecycle) — replaces
|
||||
the Python module-loading bootstrap.
|
||||
- CLI/IPC command dispatch — the first-call surface that routes a user
|
||||
command to the right back-end service.
|
||||
- The OpenAI-compatible HTTP endpoint surface (/v1 chat completions) —
|
||||
stdlib net/http + NDJSON streaming is exactly the area the rubric called
|
||||
Go's only capability-proven fit (W4 evidence, unchanged by the directive).
|
||||
- TUI: NOT in the first Go surface. Textual coupling is deep (80+ modules
|
||||
per the rubric); the front-end Go layer first exposes the API the TUI
|
||||
consumes, and the TUI itself migrates later as its own wave.
|
||||
|
||||
## 3. What "back end" means concretely here
|
||||
|
||||
All pure-logic modules move into the two existing zero-dep crates
|
||||
(evidence_provenance, query_memory) or a third crate for the remaining
|
||||
domains. The zero-external-crate discipline stands (the rubric's W4 Rust
|
||||
kill-rule for network code stays valid INSIDE Rust crates: no network code
|
||||
in crates, that belongs to the Go layer).
|
||||
|
||||
Back-end candidates not yet ported (pure logic, no external deps):
|
||||
- drift_core, drift_cache, drift_compression (scored math) — memory umbrella
|
||||
- warm_neuron_store/warm_router scoring paths — memory umbrella
|
||||
- memory_store consolidation/scoring helpers — memory umbrella
|
||||
- capability_fit/capability_broker scoring — capability umbrella
|
||||
- tokenizer/counting helpers in token_optimizer — inference umbrella
|
||||
Each port follows the slice-2 pipeline: contract, zero-dep Rust, byte-parity
|
||||
battery vs the Python vintage, dual-mode canary, consumer import chain,
|
||||
CI green, supersession ledger.
|
||||
|
||||
## 4. Sequencing (per wave, each independently mergeable)
|
||||
|
||||
H0 — enablement (no behavior change):
|
||||
- Install Go on the host + the runner user's PATH; pin a version in
|
||||
requirements-ci-equivalent (a go-version file + CI check).
|
||||
- CI: add a Go build+vet+test job (empty module, exits 0) so the pipeline
|
||||
carries Go before any code lands.
|
||||
|
||||
H1 — Go first-call shim:
|
||||
- Go binary `rune` replacing the Python entrypoint's process bootstrap:
|
||||
arg parsing, env resolution, spawn the Python runtime (transitional).
|
||||
- Canary: same first-call contract (exit codes, env handoff, TTY attach).
|
||||
|
||||
H2 — Go command dispatch + /v1 endpoint:
|
||||
- Go serves /v1 chat completions + CLI dispatch, calling the Python core
|
||||
through the same local transport the TUI uses today.
|
||||
- Parity battery: request/response shapes vs the Python server for the
|
||||
same canned conversations (byte-level for metadata, streaming NDJSON).
|
||||
|
||||
H3 — back-end wave (parallel to H1/H2 where independent):
|
||||
- Ports listed in section 3, one or two modules per slice, same slice-2
|
||||
discipline. No port starts without its parity battery written first.
|
||||
|
||||
H4 — TUI migration wave (its own contract, later):
|
||||
- Only after H1-H2 prove the Go front end serves the same surfaces the TUI
|
||||
consumes. The TUI is the largest single module (4k+ lines, Textual) and
|
||||
earns its own contract.
|
||||
|
||||
## 5. Honest constraints (carried from the rubric, not contradicted)
|
||||
|
||||
- Go must be installed first (rubric D5 gate: it failed this before; the
|
||||
directive now outweighs the gate but installation is still step zero).
|
||||
- Network/streaming code lives in the Go layer, never in Rust crates
|
||||
(rubric W4: stdlib-only Rust has no credible HTTP/SSE precedent).
|
||||
- SQLite/regex/file-IO stay at their current language boundaries per the
|
||||
zero-dep split law from slice 2 (math Rust, IO at the edges).
|
||||
- Every surface gets the same proof pipeline as slice 2: observed-not-
|
||||
invented expectations, byte parity, dual-mode canary, CI green.
|
||||
|
||||
## 6. Success measure
|
||||
|
||||
The tranche is complete when: the `rune` process starts as a Go binary;
|
||||
user-facing calls (CLI, /v1, operator path) are served by Go; every core
|
||||
logic module on the port list is byte-parity Rust; Python remains only in
|
||||
surfaces explicitly contracted as keep-Python (TUI internals until H4,
|
||||
test harness, canary frameworks).
|
||||
@@ -0,0 +1,285 @@
|
||||
# TOTAL REBASE TRANCHE — Rune to Go/Rust (R2)
|
||||
|
||||
**Status:** DESIGN — awaiting Nick's approval gate (TRANCHE-DEPTH TEMPLATE §4)
|
||||
**Supersedes:** the KEEP-PYTHON rubric verdicts (already superseded by Nick's
|
||||
Go-front/Rust-back directive) and extends Phase H (H0–H4 slice 1, merged
|
||||
`693a494`, v0.90.0) from first ports to total rebase.
|
||||
**Nick's directives (verbatim anchors):**
|
||||
- "i want the whole codebase rebased in rust and go"
|
||||
- "everything should initiate with go as the runtime, and reactive adaptable
|
||||
runtime should be go"
|
||||
- "the deterministic backend i.e memory storage, architectures and elastic
|
||||
cognition and everything like that should be in rust"
|
||||
- "go should be the reactive front end to what interfaces with the user —
|
||||
not gui — the deliverables and chat interactions as well as how rune
|
||||
handles thoughts and runtime"
|
||||
- "the zero dependency rule should be in place but the rebase should rebuild
|
||||
those aspects into the new architecture and make them operational"
|
||||
- "the textual tui should be last but everything should be wired into the
|
||||
textual tui"
|
||||
- "start with the scaffolding first as everything relies on that, then use
|
||||
the new rust scaffolding to make sure everything else is built towards
|
||||
scaffold protocol"
|
||||
- "the scaffolding function should be a rust execution but go start"
|
||||
|
||||
## 1. Grounding summary (measured 2026-10-04, from the spine store + wc)
|
||||
|
||||
- 161 modules with source, **64,482 lines** of `src/rune/` Python.
|
||||
- Draft classification (rule-based, to be function-level verified in Wave 0):
|
||||
- **RUST (deterministic backend):** 49 modules, 26,772 lines — memory,
|
||||
drift, warm, capability, token, spine, scaffold, construction_*,
|
||||
structural_store, curator, consolidation, elastic cognition, ledgers,
|
||||
audit, evidence, contracts, sleep_time, subconscious, persona_gate,
|
||||
context_broker, growth.
|
||||
- **GO (reactive runtime + user-facing):** 10 modules, 7,043 lines —
|
||||
operator, transports, dispatch, provider registry, adaptive router,
|
||||
hook engine, local tool runtime, work manager, deploy_qualify.
|
||||
- **TUI-last:** 4 modules, 4,889 lines — rune_tui (4,041), tui_validator,
|
||||
tui_backlog, tui_transport.
|
||||
- **UNCLASSIFIED (Wave 0 must disposition each with evidence):** 73
|
||||
modules, 21,473 lines — voice_*, tool_executor, model_routing/registry,
|
||||
event_bus, cron_engine, message_queue, identity_anchors, pins,
|
||||
stage_guards, tier_manager, witness, etc.
|
||||
- **Test infra:** 23 canary/test modules, 4,211 lines — rewritten per wave
|
||||
(Rust canaries port with their surface; Go canaries for the front).
|
||||
- **Retires:** `__init__`, `__main__`, `memory_env` (bootstrap glue —
|
||||
subsumed by the Go runtime's env contract).
|
||||
- Existing Rust (merged, production-proven): 6 zero-dep crates
|
||||
(query_memory, evidence_provenance, rune_spine, memory_scoring,
|
||||
capability_scoring, token_counting), 338k bytes, all with parity
|
||||
batteries. Go: rune-entry (H1 shim + H2 /v1 front door).
|
||||
- Stragglers from R1 folded in: #2171, #2119 (TUI), #1844 (salience_gate),
|
||||
#1755 (goal._load), #1738 (failure_auto_filer).
|
||||
|
||||
## 2. Objective
|
||||
|
||||
Every line of rune's Python runtime is rebased into the dichotomy —
|
||||
**Go initiates and reacts; Rust determines and stores** — with the
|
||||
scaffolding protocol rebuilt FIRST (Go start / Rust execution) so every
|
||||
subsequent conversion is driven, checked, and closed by the new protocol
|
||||
itself. Python is deleted only after the replacement is parity-proven and
|
||||
CI-green (strangler exit law). The Textual TUI is last and stays the
|
||||
integration surface: everything wires into it over the transport seam.
|
||||
|
||||
## 3. The hard constraint analysis (zero-dep + total rebase — feasibility)
|
||||
|
||||
Nick asked: confirm or deny. **Confirmed possible**, amended per his
|
||||
critiques (2026-10-04):
|
||||
|
||||
- **Hybridized membrane, not a wall (Nick's correction):** Go remains the
|
||||
reactive initiator, but "Rust never touches a socket" is WITHDRAWN as an
|
||||
absolute. The languages interoperate where coherence demands: Rust cores
|
||||
callable from Go (the proven cdylib pattern), Go surfaces invocable from
|
||||
Rust through defined channels, shared wire protocols. The dichotomy is
|
||||
the DEFAULT placement rule; hybrid junctions are deliberate, contracted
|
||||
exceptions — each recorded in the unit's contract.
|
||||
- **SQLite STAYS (Nick's ruling):** no bespoke storage engine in this
|
||||
tranche. SQLite remains and is wired into the new architecture — the
|
||||
Rust side reaches it through system-library FFI (legal under zero-dep:
|
||||
a system library is not a crate dependency). The bespoke memory-system
|
||||
redesign is a LATER, separate program.
|
||||
- **Reactivity/async:** Go goroutines + channels own the event loops;
|
||||
Rust's deterministic cores stay synchronous behind the membrane.
|
||||
- **Regex:** hand-rolled matchers per case (proven: pyjson, word-run
|
||||
scanner, run-collapse). No general regex engine needed.
|
||||
- **Parity-plus-improvement (Nick's upgrade):** parity is the FLOOR, not
|
||||
the ceiling. Every unit answers "parity OR measurably better" —
|
||||
efficiency improvements are in scope per function (Rust zero-copy and
|
||||
exactness, Go goroutine-native concurrency), bloat from bad syntax is
|
||||
removed, and functionality is the non-negotiable gate. Improvements
|
||||
carry measurements; the parity battery proves no behavior regression
|
||||
either way.
|
||||
- **TUI wiring is continuous (Nick's correction):** each landed surface
|
||||
wires toward the Textual TUI over the transport seam as part of its
|
||||
unit contract — the final TUI wave integrates a system already aimed
|
||||
at it, not a retrofit.
|
||||
- **Language intelligence in the protocol (Nick's addition):** the
|
||||
scaffolding gains a first-class placement decision — for rune, the
|
||||
Go/Rust dichotomy rule (reactive/initiating → Go; deterministic/
|
||||
stateful → Rust); for other projects, a language-fit assessment at
|
||||
construction-classification time (the protocol recommends the right
|
||||
language for the task's characteristics).
|
||||
- **Provenance lock (Nick's verification):** the scaffolding being ported
|
||||
in Wave 1 is the V1-ACCEPTED system per docs/SCAFFOLDING_V1_DEFINITION.md
|
||||
(T18 §18B; North Star = Nick's verbatim words; 17/17 acceptance gates).
|
||||
The Wave 1 parity replay against the production contract ledger proves
|
||||
the rebuild is that design, not a drift.
|
||||
|
||||
## 4. Wave structure (each wave independently mergeable, scaffolding-gated)
|
||||
|
||||
### Wave 0 — Grounding + inventory verification (the template's bar)
|
||||
- Function-level mapping of all 161 modules (the spine gives structure;
|
||||
wave 0 verifies 100% of functions and classifies the 73 UNCLASSIFIED
|
||||
with evidence).
|
||||
- Disposition the 5 R1 stragglers.
|
||||
- Produce the per-unit issue factory input: one issue per function,
|
||||
each answering improve/fix/missing-connectors with evidence.
|
||||
- Deliverable: `plans/total-rebase-inventory.md` + issues filed.
|
||||
- Exit gate: inventory signed off by Nick (the approval gate).
|
||||
|
||||
### Wave 1 — The scaffolding engine reborn (Go start / Rust execution)
|
||||
**Everything depends on this — it goes first, per Nick's directive.**
|
||||
- The rebuild target is the V1-ACCEPTED system (Nick's design —
|
||||
docs/SCAFFOLDING_V1_DEFINITION.md, North Star verbatim, 17/17
|
||||
acceptance gates). The parity replay against the production contract
|
||||
ledger is the proof the rebuild IS that design.
|
||||
- Rust crate `scaffold_core`: the construction machinery as pure
|
||||
deterministic logic — contract lifecycle (draft→approved→building→
|
||||
verifying→closed), closure classification (correctly_predicted et al.),
|
||||
scope resolution, legal transitions, the contract store.
|
||||
Source modules: scaffold.py, construction_interface.py,
|
||||
construction_decision.py, construction_intent.py, structural_store.py
|
||||
(parts not already in rune_spine), contracts.py, toolchain.py.
|
||||
- Go surface `rune-scaffold` (extends rune-entry): the initiation layer —
|
||||
submit-need/observe/transition/close CLI + /v1 endpoints, delegating to
|
||||
the Rust core.
|
||||
- NEW per Nick: the **language-placement decision** lands in the protocol
|
||||
here — construction classification gains a language-fit assessment
|
||||
(rune: the Go/Rust dichotomy rule; other projects: characteristic-based
|
||||
recommendation). Every later contract carries its placement verdict.
|
||||
- NEW per Nick: parity is the floor — the engine itself is the first
|
||||
beneficiary of the parity-plus-improvement rule (measured improvements
|
||||
allowed, functionality preserved, no-regression proven).
|
||||
- Exit gate: the R2 tranche's own remaining waves are driven THROUGH the
|
||||
new engine (the protocol builds itself under its own rules).
|
||||
|
||||
### Wave 2 — Memory/storage backend (Rust, SQLite retained)
|
||||
- SQLite is NOT replaced (Nick's ruling). The Rust memory core reaches it
|
||||
via system-library FFI (zero-dep legal); memory_store.py (2,435 lines)
|
||||
is decomposed: query paths and consolidation logic into Rust, the SQLite
|
||||
mechanics behind a thin storage trait that maps to libsqlite3 today and
|
||||
to the future bespoke engine LATER without consumer churn.
|
||||
- elastic_cognition, memory_consolidation, sleep_time, curator,
|
||||
subconscious_pass, drift_* (remaining DB-backed surfaces), journal,
|
||||
recall_* (recall_router is routing → boundary checked at Wave 0
|
||||
disposition).
|
||||
- Data safety gate: checksum-verified read-back of the live store through
|
||||
the new engine; zero writes until the read path is proven identical.
|
||||
|
||||
### Wave 3 — Cognition + architecture engines (Rust)
|
||||
- The remaining deterministic cores: capability_broker decision logic,
|
||||
context_broker, persona_gate, growth, identity_anchors, pins,
|
||||
stage_guards, tier_manager, witness, safety_override, abstraction,
|
||||
verification surfaces.
|
||||
- Ports already merged (memory_scoring, capability_scoring,
|
||||
token_counting, rune_spine) are verified against the new scaffold
|
||||
protocol and wired into the final architecture.
|
||||
|
||||
### Wave 4 — The reactive runtime (Go)
|
||||
- The operator turn loop (rune_operator.py, 2,697 lines), chat
|
||||
interactions, deliverables production, model routing at call time
|
||||
(adaptive_router, model_registry/profiles/routing, provider_*),
|
||||
tool execution (tool_executor, tool_protocol, local_tool_runtime),
|
||||
event surfaces (event_bus, message_queue, cron_engine, hook_engine),
|
||||
voice output chain (voice_*) as deliverable production.
|
||||
- The /v1 surface deepens to carry the full interaction contract;
|
||||
NDJSON streaming becomes the standard response shape.
|
||||
|
||||
### Wave 5 — Python retirement sweep
|
||||
- Every Python module whose surfaces have landed is deleted (strangler
|
||||
exit: parity proven + CI green + deployed one cycle). Shared/ vintage
|
||||
mirrors retired with them.
|
||||
- The glue shims (ms_loader, cs_loader, tc_loader, spine_loader) retire
|
||||
when their consumers no longer speak Python at all.
|
||||
|
||||
### Wave 6 — TUI last (but wired continuously)
|
||||
- Every wave before this one ships its transport-seam wiring toward the
|
||||
TUI as part of each unit's contract (Nick's correction: natural,
|
||||
continuous wiring — not a retrofit wave).
|
||||
- Then the TUI itself: a thin Textual render shell over the wire (all
|
||||
logic already Go/Rust by then) or a port — disposition decided on the
|
||||
evidence of everything that came before.
|
||||
|
||||
### Wave 7 — Final audit
|
||||
- Language-bar measurement, the full scaffolding ledger replayed, the
|
||||
tranche's closure report with per-wave evidence.
|
||||
|
||||
## 5. Per-unit discipline (the template, unchanged)
|
||||
|
||||
- One issue per function via the issue factory; each answers
|
||||
improve/fix/missing-connectors with evidence.
|
||||
- Every unit runs the scaffolding pipeline end-to-end: submit-need →
|
||||
contract → approval → canary-first (red proven) → implementation →
|
||||
parity battery vs the Python vintage → closure audit (structural ids,
|
||||
never file paths — the closure vocabulary law).
|
||||
- Per-unit decisions on evidence, never pre-decided globally: each
|
||||
function's Go-vs-Rust placement is assessed by the dichotomy rule with
|
||||
the measured call graph, not by the draft classification alone.
|
||||
- Bug-fix contract governs every defect found en route (red test first,
|
||||
regression frozen).
|
||||
|
||||
## 6. Verification gates
|
||||
|
||||
- CI green per wave (crate builds, Go vet/test/build, canaries, henxels,
|
||||
type ratchet until the Python ratchet itself retires in Wave 5).
|
||||
- Parity batteries are byte-level for deterministic surfaces, shape-level
|
||||
modulo volatile fields for wire surfaces.
|
||||
- The scaffolding closure audit for every contract: correctly_predicted
|
||||
or honestly flagged, never silent.
|
||||
- Deploy qualification per merge (the pipeline that proved itself today).
|
||||
|
||||
### The 90/100 coverage law (Nick, 2026-10-05 — GOVERNING from W3.4 on)
|
||||
|
||||
Per Nick: "if it's realistically achievable but aim higher also if that's
|
||||
realistically achievable. go to 100% where achievable."
|
||||
|
||||
- **100% where achievable — the deterministic Rust kernels.** Every kernel
|
||||
crate (memory_retrieval, cognition_core, scaffold_core, memory_storage,
|
||||
and every future crate) targets 100% BRANCH coverage via its parity
|
||||
matrices + the pytest shim battery: every branch has real reachable
|
||||
semantics, and exhaustive enumeration is exactly what the batteries do.
|
||||
Achievability is measured per crate at wave close; a kernel below 100%
|
||||
carries the named unreachable case or it isn't done.
|
||||
- **≥90% on the changed-lines bar (diff-cover), aiming higher.** The floor
|
||||
stays a floor; the working standard per slice is 90%+ on changed lines;
|
||||
whatever a slice achieves beyond 90 becomes the next slice's personal
|
||||
benchmark (per-unit "aim higher" — the number climbs wave over wave).
|
||||
The floor ratchet itself moves after consecutive clean waves
|
||||
(70 → 76 → 84 → 90; each raise is a commit, never a step down).
|
||||
- **Glue/failure branches: 90%+ with byte-compare proofs.** The stubs and
|
||||
probe branches that only fire in a broken world are covered by honest
|
||||
fixtures (real import-failure probes like the from-import resolution
|
||||
law) — never by fake-triggered pragmas. A branch covered by a lying
|
||||
fixture is WORSE than uncovered; if it can't be honestly reached, it's
|
||||
named in the wave's closure report with the reason.
|
||||
- **Assertion quality is part of the bar.** A test that touches a line
|
||||
without verifying its OUTPUT doesn't count toward the bar — byte-compare
|
||||
/ exact-value asserts are the standard; "is not None" assertions reject
|
||||
at review.
|
||||
|
||||
## 7. Risk register
|
||||
|
||||
- **SQLite FFI correctness** (Wave 2) — the storage layer is retained but
|
||||
re-wired; mitigations: read-path proven identical before any write,
|
||||
checksum-verified read-back of the live store, the storage trait
|
||||
boundary keeps the future bespoke swap cheap.
|
||||
- **Hybrid membrane drift** — junctions multiply ad-hoc if ungoverned;
|
||||
every Go↔Rust interaction point is a contracted, recorded decision in
|
||||
its unit's contract (the dichotomy is the default; hybrids justify
|
||||
themselves per unit).
|
||||
- **Improvement regressions** — parity-plus-improvement invites subtle
|
||||
behavior changes; every improvement carries a measurement AND the parity
|
||||
battery stays authoritative for no-regression.
|
||||
- **Go↔Rust call latency** — measure in Wave 1; the architecture assumes
|
||||
local transport cost is negligible vs model latency; if measured wrong,
|
||||
the membrane's transport moves (decision recorded, not improvised).
|
||||
- **Context-window economics** — 1,852-style issue volumes mean the
|
||||
pipeline must multiplex (no-wait law) and local models carry routine
|
||||
units (custodian pattern).
|
||||
- **Scope creep during Wave 0** — the inventory is measured and frozen
|
||||
before design; additions go through scope amendments.
|
||||
|
||||
## 8. Rollback
|
||||
|
||||
- Every wave merges independently; master stays deployable at each
|
||||
boundary (the strangler's whole point).
|
||||
- The Python vintage remains until Wave 5 deletion gates pass; a wave
|
||||
revert = revert the merge commit (deploy qualification re-runs).
|
||||
- The scaffolding engine's Wave 1 parity replay means even the protocol's
|
||||
own replacement is reversible against recorded history.
|
||||
|
||||
## 9. Approval gate
|
||||
|
||||
This tranche does not execute until Nick approves. On approval, Wave 0
|
||||
starts immediately and runs without consent blockers (standing directive),
|
||||
with self-verified checkpoints and defect filing.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Total-Rebase Inventory (R2 Wave 0.1) — measured 2026-10-04
|
||||
|
||||
Generated from `store/rebase/inventory.json` (AST function-level extraction) +
|
||||
`store/rebase/classification.json` (import/content evidence per module).
|
||||
Placement law: reactive/initiating/user-facing/network/turn-time → GO;
|
||||
deterministic/stateful/storage/cognition → RUST; Textual rendering → TUI-LAST;
|
||||
pure compute with no I/O markers defaults to the deterministic backend.
|
||||
|
||||
## Totals
|
||||
|
||||
| Side | Modules | Lines | Functions |
|
||||
|---|---|---|---|
|
||||
| GO | 43 | 19495 | 708 |
|
||||
| RUST | 93 | 36215 | 1091 |
|
||||
| TUI-LAST | 7 | 5438 | 225 |
|
||||
| TEST-INFRA | 29 | 5395 | 158 |
|
||||
| RETIRES | 2 | 93 | 9 |
|
||||
|
||||
## Per-module classification (evidence-carrying)
|
||||
|
||||
| Module | Side | Lines | Functions | Evidence |
|
||||
|---|---|---|---|---|
|
||||
| rune_tui | TUI-LAST | 4041 | 166 | textual/rich imports: ['rich', 'textual'] |
|
||||
| rune_operator | GO | 2697 | 68 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
|
||||
| memory_store | RUST | 2435 | 56 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| curator | RUST | 1838 | 43 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| tool_executor | GO | 1812 | 47 | network/concurrency imports: ['subprocess']; reactive/initiating domain |
|
||||
| warm_router | RUST | 1536 | 36 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; reactive/initiating domain; deterministic/state domain |
|
||||
| construction_interface | RUST | 1039 | 25 | deterministic/state domain |
|
||||
| spine | RUST | 1019 | 31 | deterministic/state domain |
|
||||
| sleep_time | RUST | 971 | 25 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
|
||||
| construction_decision | RUST | 949 | 18 | deterministic/state domain |
|
||||
| capability_broker | RUST | 900 | 21 | deterministic/state domain |
|
||||
| hook_engine | GO | 860 | 8 | reactive/initiating domain |
|
||||
| scaffold | RUST | 854 | 31 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| local_tool_runtime | GO | 806 | 32 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
|
||||
| context_broker | GO | 768 | 23 | network/concurrency imports: ['threading'] |
|
||||
| elastic_cognition | RUST | 765 | 13 | network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| memory_consolidation | RUST | 733 | 20 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| provider_registry | GO | 666 | 28 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
|
||||
| persona_gate | RUST | 661 | 21 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
|
||||
| structural_store | RUST | 656 | 33 | deterministic/state domain |
|
||||
| contracts | RUST | 626 | 7 | deterministic/state domain |
|
||||
| adaptive_router | GO | 625 | 19 | reactive/initiating domain |
|
||||
| work_manager | GO | 591 | 25 | reactive/initiating domain |
|
||||
| abstraction | RUST | 587 | 12 | sqlite3 (storage-backed); network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| construction_intent | RUST | 555 | 16 | deterministic/state domain |
|
||||
| canary_audit_ledger | TEST-INFRA | 540 | 2 | canary/test module |
|
||||
| safety_override | RUST | 530 | 18 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| failure_auto_filer | GO | 527 | 28 | network/concurrency imports: ['threading'] |
|
||||
| build_mode | GO | 517 | 34 | network/concurrency imports: ['subprocess'] |
|
||||
| drift_core | RUST | 517 | 14 | deterministic/state domain |
|
||||
| event_wait | GO | 517 | 14 | network/concurrency imports: ['subprocess', 'urllib']; reactive/initiating domain |
|
||||
| tui_validator | TUI-LAST | 514 | 13 | tui family |
|
||||
| memory_selector | RUST | 511 | 14 | deterministic/state domain |
|
||||
| system_registry | RUST | 501 | 14 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| construction_pattern | RUST | 496 | 13 | deterministic/state domain |
|
||||
| cache | GO | 492 | 17 | sqlite3 (storage-backed); network/concurrency imports: ['threading'] |
|
||||
| claim_audit | RUST | 483 | 14 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| graph_memory | RUST | 474 | 9 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| identity_anchors | RUST | 474 | 8 | deterministic/state domain |
|
||||
| model_connect | GO | 462 | 10 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| auto_tasks | GO | 460 | 14 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess'] |
|
||||
| capability_fit | RUST | 455 | 14 | deterministic/state domain |
|
||||
| tool_protocol | GO | 455 | 27 | reactive/initiating domain |
|
||||
| drift_ledger | RUST | 453 | 16 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| witness | RUST | 451 | 10 | network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| goal | RUST | 441 | 20 | network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| release_notes | GO | 439 | 17 | network/concurrency imports: ['urllib'] |
|
||||
| cron_engine | GO | 436 | 28 | network/concurrency imports: ['subprocess', 'threading']; reactive/initiating domain |
|
||||
| model_profiles | GO | 435 | 15 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| microkernel | GO | 434 | 12 | network/concurrency imports: ['subprocess', 'threading'] |
|
||||
| model_routing | GO | 432 | 15 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| voice_hybrid | RUST | 421 | 10 | sqlite3 (storage-backed); reactive/initiating domain |
|
||||
| behavior | RUST | 407 | 9 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| token_optimizer | RUST | 407 | 23 | deterministic/state domain |
|
||||
| capability_ledger | RUST | 405 | 19 | deterministic/state domain |
|
||||
| night_mode | GO | 405 | 19 | network/concurrency imports: ['subprocess', 'threading', 'urllib'] |
|
||||
| chat_bubble | TUI-LAST | 401 | 16 | textual/rich imports: ['rich', 'textual'] |
|
||||
| growth_delta | RUST | 400 | 16 | network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
|
||||
| construction_mode | RUST | 395 | 5 | deterministic/state domain |
|
||||
| prompt_sync | RUST | 376 | 14 | sqlite3 (storage-backed) |
|
||||
| provider_budget | GO | 354 | 21 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| structural_model | RUST | 352 | 21 | deterministic/state domain |
|
||||
| programme_continuation | RUST | 349 | 11 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_rune_serve | TEST-INFRA | 329 | 13 | canary/test module |
|
||||
| builder_bridge | GO | 326 | 11 | reactive/initiating domain |
|
||||
| canary_p1_07 | TEST-INFRA | 324 | 12 | canary/test module |
|
||||
| layout_contract | RUST | 316 | 6 | deterministic/state domain |
|
||||
| message_queue | GO | 316 | 14 | reactive/initiating domain |
|
||||
| drift_observability | RUST | 315 | 9 | deterministic/state domain |
|
||||
| build_basis | RUST | 314 | 12 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_p0_07 | TEST-INFRA | 310 | 11 | canary/test module |
|
||||
| drift_compression | RUST | 307 | 12 | deterministic/state domain |
|
||||
| event_envelope | GO | 303 | 8 | reactive/initiating domain |
|
||||
| attention_capsule | RUST | 296 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_p1_01 | TEST-INFRA | 294 | 14 | canary/test module |
|
||||
| voice_sync | RUST | 293 | 10 | sqlite3 (storage-backed); reactive/initiating domain |
|
||||
| drift_cache | RUST | 292 | 16 | deterministic/state domain |
|
||||
| outcome_ledger | RUST | 286 | 8 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| time_awareness | RUST | 283 | 15 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| tiered_executor | GO | 282 | 9 | network/concurrency imports: ['urllib'] |
|
||||
| stage_guards | RUST | 281 | 11 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| standardized_providers | GO | 281 | 5 | reactive/initiating domain |
|
||||
| canary_p1_06 | TEST-INFRA | 280 | 9 | canary/test module |
|
||||
| progressive_escalation | RUST | 279 | 9 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| pins | RUST | 276 | 11 | deterministic/state domain |
|
||||
| canary_p1_03 | TEST-INFRA | 271 | 9 | canary/test module |
|
||||
| auto_context | GO | 270 | 8 | network/concurrency imports: ['subprocess'] |
|
||||
| task_persist | RUST | 269 | 10 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| context_pressure | RUST | 262 | 9 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_gitea_automation | TEST-INFRA | 261 | 3 | canary/test module |
|
||||
| canary_p1_02 | TEST-INFRA | 253 | 12 | canary/test module |
|
||||
| construction_patterns_builtin | RUST | 249 | 1 | deterministic/state domain |
|
||||
| canary_full_system | TEST-INFRA | 244 | 2 | canary/test module |
|
||||
| event_bus | GO | 244 | 7 | reactive/initiating domain |
|
||||
| salience_gate | RUST | 243 | 4 | deterministic/state domain |
|
||||
| self_improve | RUST | 242 | 5 | sqlite3 (storage-backed) |
|
||||
| construction_activation | RUST | 237 | 4 | deterministic/state domain |
|
||||
| toolchain | RUST | 236 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| task_gate | RUST | 235 | 8 | sqlite3 (storage-backed) |
|
||||
| core_server | GO | 234 | 13 | network/concurrency imports: ['http', 'urllib']; reactive/initiating domain |
|
||||
| audit_ledger | RUST | 231 | 8 | deterministic/state domain |
|
||||
| canary_native | TEST-INFRA | 231 | 3 | canary/test module |
|
||||
| openai_compat_transport | GO | 227 | 10 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| shard_manager | RUST | 223 | 10 | sqlite3 (storage-backed) |
|
||||
| response_adapter | RUST | 222 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| evidence_probe | RUST | 212 | 9 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| model_registry | GO | 211 | 8 | reactive/initiating domain |
|
||||
| persona_definition | GO | 210 | 8 | network/concurrency imports: ['subprocess'] |
|
||||
| cognitive_budgeter | RUST | 208 | 3 | deterministic/state domain |
|
||||
| canary_p0_04 | TEST-INFRA | 207 | 8 | canary/test module |
|
||||
| forge_client | GO | 203 | 19 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| dependency_tracker | RUST | 202 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_context_broker | TEST-INFRA | 199 | 10 | canary/test module |
|
||||
| canary_p0_05 | TEST-INFRA | 199 | 16 | canary/test module |
|
||||
| testing_mode | RUST | 196 | 13 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| parallel_processor | GO | 194 | 17 | network/concurrency imports: ['threading'] |
|
||||
| canary_spine_meta | TEST-INFRA | 192 | 7 | canary/test module |
|
||||
| iteration | RUST | 190 | 8 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| verification_layer | RUST | 189 | 12 | deterministic/state domain |
|
||||
| subconscious_pass | RUST | 187 | 6 | deterministic/state domain |
|
||||
| ffi_query_memory_smoke | RUST | 182 | 5 | deterministic/state domain |
|
||||
| tier_manager | RUST | 182 | 9 | deterministic/state domain |
|
||||
| canary_tui_transport | TEST-INFRA | 174 | 7 | canary/test module |
|
||||
| behaviour_ledger | RUST | 173 | 13 | deterministic/state domain |
|
||||
| construction_commands | RUST | 173 | 4 | deterministic/state domain |
|
||||
| canary_tui_drift | TEST-INFRA | 170 | 2 | canary/test module |
|
||||
| intent_continuity | RUST | 168 | 11 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| rune_profiler | RUST | 165 | 3 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| inference_trace | RUST | 164 | 6 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| ms_loader | RUST | 163 | 16 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| subsystem_health | GO | 163 | 8 | network/concurrency imports: ['threading'] |
|
||||
| recall_router | GO | 162 | 3 | reactive/initiating domain |
|
||||
| voicebox | GO | 161 | 11 | network/concurrency imports: ['subprocess']; reactive/initiating domain |
|
||||
| canary_gitea_pipeline | TEST-INFRA | 159 | 2 | canary/test module |
|
||||
| forge | GO | 146 | 12 | network/concurrency imports: ['subprocess', 'urllib'] |
|
||||
| rune_graphic | TUI-LAST | 145 | 5 | textual/rich imports: ['rich'] |
|
||||
| connection_pool | GO | 144 | 9 | sqlite3 (storage-backed); network/concurrency imports: ['threading'] |
|
||||
| recall_uplift | RUST | 141 | 1 | sqlite3 (storage-backed) |
|
||||
| project_registry | RUST | 139 | 12 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| construction_depth | RUST | 138 | 2 | deterministic/state domain |
|
||||
| voice_injector | GO | 138 | 5 | reactive/initiating domain |
|
||||
| canary_rune_entry | TEST-INFRA | 137 | 3 | canary/test module |
|
||||
| construction_projection | RUST | 135 | 3 | deterministic/state domain |
|
||||
| rune_spinner | RUST | 130 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| tui_backlog | TUI-LAST | 125 | 5 | tui family |
|
||||
| warm_neuron_store | RUST | 122 | 6 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| drift_resume | RUST | 118 | 3 | deterministic/state domain |
|
||||
| wait_guard | RUST | 116 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_conversation_state | TEST-INFRA | 115 | 2 | canary/test module |
|
||||
| canary_query_memory | TEST-INFRA | 110 | 2 | canary/test module |
|
||||
| failure_recovery | RUST | 108 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| graph_projection | RUST | 108 | 5 | deterministic/state domain |
|
||||
| tui_transport | TUI-LAST | 108 | 12 | tui family |
|
||||
| spine_loader | RUST | 105 | 7 | deterministic/state domain |
|
||||
| canary_unbounded | TEST-INFRA | 104 | 2 | canary/test module |
|
||||
| message_widgets | TUI-LAST | 104 | 8 | textual/rich imports: ['rich', 'textual'] |
|
||||
| cs_loader | RUST | 90 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| tc_loader | RUST | 88 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_schema_migration | TEST-INFRA | 86 | 1 | canary/test module |
|
||||
| programme_gates | RUST | 85 | 6 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| structural_migration | RUST | 77 | 4 | deterministic/state domain |
|
||||
| memory_env | RETIRES | 72 | 8 | bootstrap glue subsumed by the Go runtime env contract |
|
||||
| impact_preview | RUST | 69 | 2 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| provider_context | GO | 48 | 1 | reactive/initiating domain |
|
||||
| wait_guard_corpus | RUST | 46 | 0 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_cognitive_strata | TEST-INFRA | 44 | 1 | canary/test module |
|
||||
| builder_bridge_cli | GO | 42 | 1 | reactive/initiating domain |
|
||||
| route_risk_flags | RUST | 37 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_memory_synthesis | TEST-INFRA | 33 | 1 | canary/test module |
|
||||
| canary_resource_authority | TEST-INFRA | 33 | 1 | canary/test module |
|
||||
| canary_surface_parity | TEST-INFRA | 33 | 1 | canary/test module |
|
||||
| canary_orchestration_foundation | TEST-INFRA | 32 | 1 | canary/test module |
|
||||
| canary_gitea_delegation | TEST-INFRA | 31 | 1 | canary/test module |
|
||||
| __main__ | RETIRES | 21 | 1 | bootstrap glue subsumed by the Go runtime env contract |
|
||||
+2
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.86.0"
|
||||
version = "0.99.1"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencies = [
|
||||
"aiohttp>=3.9.0",
|
||||
"websockets>=12.0",
|
||||
"psutil>=5.9.0",
|
||||
"pillow>=10.0",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
|
||||
+5
-1
@@ -29,6 +29,10 @@
|
||||
"**/__pycache__"
|
||||
],
|
||||
"extraPaths": [
|
||||
"scripts"
|
||||
"scripts",
|
||||
"src/rune",
|
||||
"shared",
|
||||
"src/rust/evidence_provenance",
|
||||
"src/rust/query_memory"
|
||||
]
|
||||
}
|
||||
+11
-9
@@ -1,8 +1,8 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml -o /tmp/req_new.txt --extra dev --extra memory --python-version 3.11
|
||||
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
|
||||
aiohappyeyeballs==2.7.1
|
||||
# via aiohttp
|
||||
aiohttp==3.14.3
|
||||
aiohttp==3.14.4
|
||||
# via rune (pyproject.toml)
|
||||
aiosignal==1.4.0
|
||||
# via aiohttp
|
||||
@@ -18,9 +18,9 @@ certifi==2026.7.22
|
||||
# httpx
|
||||
chardet==7.6.0
|
||||
# via diff-cover
|
||||
coverage==7.16.1
|
||||
coverage==7.16.2
|
||||
# via rune (pyproject.toml)
|
||||
diff-cover==10.5.1
|
||||
diff-cover==10.6.0
|
||||
# via rune (pyproject.toml)
|
||||
frozenlist==1.8.0
|
||||
# via
|
||||
@@ -50,21 +50,23 @@ markdown-it-py==4.2.0
|
||||
# mdit-py-plugins
|
||||
# rich
|
||||
# textual
|
||||
markupsafe==3.0.3
|
||||
markupsafe==3.0.4
|
||||
# via jinja2
|
||||
mdit-py-plugins==0.6.1
|
||||
# via textual
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
multidict==6.9.0
|
||||
multidict==6.9.1
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
nodeenv==1.10.0
|
||||
nodeenv==1.11.0
|
||||
# via pyright
|
||||
packaging==26.3
|
||||
# via pytest
|
||||
platformdirs==4.11.11
|
||||
pillow==12.3.0
|
||||
# via rune (pyproject.toml)
|
||||
platformdirs==4.12.3
|
||||
# via textual
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
@@ -116,7 +118,7 @@ typing-extensions==4.16.0
|
||||
# pyright
|
||||
# textual
|
||||
# textual-autocomplete
|
||||
websockets==17.1
|
||||
websockets==17.2
|
||||
# via rune (pyproject.toml)
|
||||
yarl==1.25.1
|
||||
# via aiohttp
|
||||
+17
-3
@@ -148,9 +148,18 @@ def sha_branch(sha: str) -> str:
|
||||
|
||||
|
||||
def sha_on_master(sha: str) -> bool:
|
||||
out = _git("branch", "--contains", sha)
|
||||
return any(l.strip().endswith("master") or l.strip() == "master"
|
||||
for l in out.splitlines())
|
||||
"""SHA is an ancestor of master (local or remote-tracking refs).
|
||||
|
||||
M10 correction (2026-09-19): --all so a freshly fetched master
|
||||
(refs/remotes/gitea/master) counts; local branches alone go stale
|
||||
between fetches. derive_op_type refreshes refs before calling this.
|
||||
"""
|
||||
out = _git("branch", "--all", "--contains", sha)
|
||||
for l in out.splitlines():
|
||||
s = l.strip().removeprefix("remotes/").removesuffix(" *")
|
||||
if s == "master" or s.endswith("/master"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def open_pr_for_sha(sha: str) -> int | None:
|
||||
@@ -184,6 +193,11 @@ def derive_op_type(sha: str) -> tuple[str, str]:
|
||||
were created from model-supplied defaults and cannot be trusted.
|
||||
Never trusts model input.
|
||||
"""
|
||||
# 0. M10 correction (2026-09-19): refresh remote-tracking refs before
|
||||
# inference. A just-merged master commit is invisible to
|
||||
# branch --contains until refs are fetched; stale local git state
|
||||
# must never outrank repo truth. Bounded by GIT_TIMEOUT, fail-open.
|
||||
_git("fetch", "--quiet", "gitea", "+refs/heads/*:refs/remotes/gitea/*")
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
|
||||
@@ -86,9 +86,12 @@ def main() -> int:
|
||||
if live_state == "open" and rec["tracker_state"] == "closed" and rec["reconciliation_state"] in TERMINAL_STATES:
|
||||
drift.append(f"#{num}: live state open but artifact says closed/{rec['reconciliation_state']}")
|
||||
live_bug = "bug" in [l["name"] for l in li.get("labels", [])]
|
||||
if live_bug and not rec.get("live_defect"):
|
||||
# Same open-only exemption as the artifact-records block above: a
|
||||
# closed issue's labels are historical record; FIXED_VERIFIED issues
|
||||
# legitimately keep the bug label after closure.
|
||||
if live_bug and rec.get("tracker_state") == "open" and not rec.get("live_defect"):
|
||||
drift.append(f"#{num}: live bug label present but artifact records no live defect")
|
||||
if not live_bug and rec.get("live_defect"):
|
||||
if not live_bug and rec.get("live_defect") and rec.get("tracker_state") == "open":
|
||||
drift.append(f"#{num}: artifact records live defect but live bug label absent")
|
||||
|
||||
# Live defects in the artifact must all be OPEN in the tracker
|
||||
|
||||
@@ -1,43 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_mimo_hook.py — T21.2 §21.2D: canonical MiMo hook installer.
|
||||
"""install_mimo_hook.py — T21.2 §21.2D (T22: multi-hook): canonical MiMo
|
||||
hook installer.
|
||||
|
||||
Canonical source: config/mimo-hooks/event-wait-guard.ts
|
||||
Target: ~/.mimocode/hooks/event-wait-guard.ts
|
||||
Verifies hash after install. Detects drift in both directions."""
|
||||
Canonical sources: config/mimo-hooks/*.ts
|
||||
Targets: ~/.mimocode/hooks/<same-name>.ts
|
||||
|
||||
Every hook is hash-verified after install; drift is detected in both
|
||||
directions. T22 added builder-bridge.ts to the managed set — it was
|
||||
previously installed by hand, unversioned, which is how it silently rotted
|
||||
(hardcoded ephemeral interpreter). A hook that reaches the builder must be
|
||||
version-controlled and drift-checked like any other authority.
|
||||
|
||||
Usage:
|
||||
python3 scripts/install_mimo_hook.py # sync all managed hooks
|
||||
python3 scripts/install_mimo_hook.py --check # verify only, install nothing
|
||||
"""
|
||||
import hashlib
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL = Path(__file__).resolve().parent.parent / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
TARGET = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL_DIR = REPO / "config" / "mimo-hooks"
|
||||
TARGET_DIR = Path.home() / ".mimocode" / "hooks"
|
||||
|
||||
# The managed set. Adding a file here is what makes a hook version-controlled.
|
||||
MANAGED = ["event-wait-guard.ts", "builder-bridge.ts"]
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not CANONICAL.is_file():
|
||||
print(f"canonical source missing: {CANONICAL}")
|
||||
return 1
|
||||
canon_hash = sha256(CANONICAL)
|
||||
if TARGET.is_file():
|
||||
target_hash = sha256(TARGET)
|
||||
if target_hash == canon_hash:
|
||||
print(f"in-sync: {target_hash[:12]}")
|
||||
return 0
|
||||
print(f"DRIFT: canonical={canon_hash[:12]} installed={target_hash[:12]}")
|
||||
def sync_one(name: str, check_only: bool) -> bool:
|
||||
canonical = CANONICAL_DIR / name
|
||||
target = TARGET_DIR / name
|
||||
if not canonical.is_file():
|
||||
print(f"canonical source missing: {canonical}")
|
||||
return False
|
||||
canon_hash = sha256(canonical)
|
||||
if target.is_file() and sha256(target) == canon_hash:
|
||||
print(f"in-sync: {name} {canon_hash[:12]}")
|
||||
return True
|
||||
if check_only:
|
||||
state = "DRIFT" if target.is_file() else "MISSING"
|
||||
inst = sha256(target)[:12] if target.is_file() else "-"
|
||||
print(f"{state}: {name} canonical={canon_hash[:12]} installed={inst}")
|
||||
return False
|
||||
if target.is_file():
|
||||
print(f"DRIFT: {name} canonical={canon_hash[:12]} installed={sha256(target)[:12]}")
|
||||
else:
|
||||
print(f"hook missing at {TARGET}")
|
||||
TARGET.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(CANONICAL, TARGET)
|
||||
installed_hash = sha256(TARGET)
|
||||
if installed_hash != canon_hash:
|
||||
print(f"INSTALL FAILED: hash mismatch after copy")
|
||||
return 1
|
||||
print(f"installed: {installed_hash[:12]}")
|
||||
return 0
|
||||
print(f"hook missing at {target}")
|
||||
TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(canonical, target)
|
||||
if sha256(target) != canon_hash:
|
||||
print(f"INSTALL FAILED: {name} hash mismatch after copy")
|
||||
return False
|
||||
print(f"installed: {name} {canon_hash[:12]}")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
check_only = "--check" in sys.argv
|
||||
ok = True
|
||||
for name in MANAGED:
|
||||
ok = sync_one(name, check_only) and ok
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tranche R1 Wave 0 — whole-project inventory generator.
|
||||
|
||||
Parses every .py file under src/rune/ with the stdlib AST and emits
|
||||
docs/rebase/rebase-inventory.json: files, classes, functions (with signatures and
|
||||
line ranges), imports, and best-effort caller/callee edges (intra-module call
|
||||
graph + cross-module edges resolved through import tables).
|
||||
|
||||
Deterministic and idempotent: same tree in, same JSON out (sorted keys,
|
||||
sorted files). The issue-factory reads this file; nothing else may edit it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SRC = REPO / "src" / "rune"
|
||||
OUT = REPO / "docs" / "rebase" / "rebase-inventory.json"
|
||||
|
||||
|
||||
def _arg_str(a: ast.arg) -> str:
|
||||
s = a.arg
|
||||
if a.annotation is not None:
|
||||
# Degraded signature (no annotation) is deterministic metadata — never
|
||||
# let one pathological annotation node abort the whole inventory walk.
|
||||
try:
|
||||
s += f": {ast.unparse(a.annotation)}"
|
||||
except (AttributeError, RecursionError, ValueError):
|
||||
pass
|
||||
return s
|
||||
|
||||
|
||||
def _signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
||||
parts = [_arg_str(a) for a in node.args.args]
|
||||
if node.args.vararg:
|
||||
parts.append(f"*{node.args.vararg.arg}")
|
||||
elif node.args.kwonlyargs:
|
||||
parts.append("*")
|
||||
parts += [_arg_str(a) for a in node.args.kwonlyargs]
|
||||
if node.args.kwarg:
|
||||
parts.append(f"**{node.args.kwarg.arg}")
|
||||
ret = ""
|
||||
if node.returns is not None:
|
||||
# Same degraded-metadata contract as _arg_str.
|
||||
try:
|
||||
ret = f" -> {ast.unparse(node.returns)}"
|
||||
except (AttributeError, RecursionError, ValueError):
|
||||
pass
|
||||
return f"({', '.join(parts)}){ret}"
|
||||
|
||||
|
||||
class FileVisitor(ast.NodeVisitor):
|
||||
def __init__(self) -> None:
|
||||
self.functions: list[dict] = []
|
||||
self.classes: list[dict] = []
|
||||
self.imports: list[dict] = []
|
||||
self.calls: list[tuple[int, str]] = [] # (lineno, dotted call name)
|
||||
|
||||
def _fn(self, node, parents: list[str]) -> dict:
|
||||
return {
|
||||
"name": ".".join([*parents, node.name]),
|
||||
"signature": _signature(node),
|
||||
"line_start": node.lineno,
|
||||
"line_end": node.end_lineno or node.lineno,
|
||||
"decorators": [ast.unparse(d) for d in node.decorator_list
|
||||
if isinstance(d, (ast.Name, ast.Attribute, ast.Call))],
|
||||
"async": isinstance(node, ast.AsyncFunctionDef),
|
||||
}
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
methods = []
|
||||
for child in node.body:
|
||||
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
methods.append(self._fn(child, [node.name]))
|
||||
self.calls.extend(
|
||||
(n.lineno, ast.unparse(n.func))
|
||||
for n in ast.walk(child)
|
||||
if isinstance(n, ast.Call) and not isinstance(n.func, ast.Name if False else type(None))
|
||||
and isinstance(n.func, (ast.Name, ast.Attribute))
|
||||
)
|
||||
self.classes.append({
|
||||
"name": node.name,
|
||||
"bases": [ast.unparse(b) for b in node.bases],
|
||||
"line_start": node.lineno,
|
||||
"line_end": node.end_lineno or node.lineno,
|
||||
"methods": methods,
|
||||
})
|
||||
# visit only non-function children: methods are already captured above,
|
||||
# and a generic_visit would re-fire visit_FunctionDef on them (duplicate
|
||||
# units + duplicate call edges). Nested classes are still visited.
|
||||
for child in node.body:
|
||||
if not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
self.visit(child)
|
||||
|
||||
def visit_FunctionDef(self, node) -> None:
|
||||
# top-level only reached here (methods captured in ClassDef)
|
||||
self.functions.append(self._fn(node, []))
|
||||
self.calls.extend(
|
||||
(n.lineno, ast.unparse(n.func))
|
||||
for n in ast.walk(node)
|
||||
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute))
|
||||
)
|
||||
# do not descend into nested defs separately; they are folded into parents
|
||||
# (their calls are already captured by the ast.walk above). No generic_visit:
|
||||
# it would re-collect nested defs as top-level functions and duplicate edges.
|
||||
|
||||
visit_AsyncFunctionDef = visit_FunctionDef
|
||||
|
||||
def visit_Import(self, node: ast.Import) -> None:
|
||||
for a in node.names:
|
||||
self.imports.append({"module": a.name, "as": a.asname, "line": node.lineno})
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
mod = node.module or ""
|
||||
for a in node.names:
|
||||
self.imports.append({"module": f"{mod}.{a.name}" if a.name != "*" else mod,
|
||||
"as": a.asname or a.name, "line": node.lineno,
|
||||
"from": True})
|
||||
|
||||
|
||||
def collect_file(path: Path) -> dict:
|
||||
src_text = path.read_text(encoding="utf-8", errors="replace")
|
||||
tree = ast.parse(src_text)
|
||||
v = FileVisitor()
|
||||
v.visit(tree)
|
||||
|
||||
# module-level calls (outside defs/classes) — walk tree body directly.
|
||||
# SKIP def/class statements: their bodies are walked by the visit_ methods;
|
||||
# walking them here duplicated every call edge inside every top-level function.
|
||||
for stmt in tree.body:
|
||||
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
continue
|
||||
for n in ast.walk(stmt):
|
||||
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute)):
|
||||
v.calls.append((n.lineno, ast.unparse(n.func)))
|
||||
|
||||
defined = {f["name"].split(".")[-1] for f in v.functions}
|
||||
defined |= {m["name"].split(".")[-1] for c in v.classes for m in c["methods"]}
|
||||
defined |= {c["name"] for c in v.classes}
|
||||
|
||||
callees = []
|
||||
for lineno, call in v.calls:
|
||||
root = call.split(".")[0].split("(")[0].strip()
|
||||
callees.append({"line": lineno, "call": call,
|
||||
"internal": root in defined or root == "self"})
|
||||
|
||||
return {
|
||||
"path": str(path.relative_to(REPO)),
|
||||
"lines": len(src_text.splitlines()),
|
||||
"imports": sorted({i["module"] for i in v.imports}),
|
||||
"import_lines": v.imports,
|
||||
"functions": sorted(v.functions, key=lambda f: (f["line_start"], f["name"])),
|
||||
"classes": sorted(v.classes, key=lambda c: c["line_start"]),
|
||||
"calls": callees,
|
||||
}
|
||||
|
||||
|
||||
def cross_module_edges(files: dict[str, dict]) -> dict[str, set[str]]:
|
||||
"""Resolve import tables: module-stem -> set of imported module stems
|
||||
(rune-internal only), so the factory can attribute cross-module edges."""
|
||||
graph = defaultdict(set)
|
||||
for rel, info in files.items():
|
||||
stem = Path(rel).stem
|
||||
for imp in info["imports"]:
|
||||
top = imp.split(".")[0]
|
||||
second = ".".join(imp.split(".")[:2])
|
||||
if top == "rune":
|
||||
graph[stem].add(imp.replace("rune.", "", 1).split(".")[0])
|
||||
elif not imp.startswith("_") and top not in sys.stdlib_module_names:
|
||||
graph[stem].add(f"ext:{top}")
|
||||
return graph
|
||||
|
||||
|
||||
def main() -> None:
|
||||
py_files = sorted(SRC.rglob("*.py"))
|
||||
files: dict[str, dict] = {}
|
||||
for p in py_files:
|
||||
try:
|
||||
info = collect_file(p)
|
||||
except SyntaxError as e: # a file that cannot parse is a finding, not a skip
|
||||
info = {"path": str(p.relative_to(REPO)), "lines": 0, "imports": [],
|
||||
"import_lines": [], "functions": [], "classes": [], "calls": [],
|
||||
"parse_error": f"{type(e).__name__}: {e}"}
|
||||
files[info["path"]] = info
|
||||
|
||||
graph = cross_module_edges(files)
|
||||
total_fns = sum(len(f["functions"]) + sum(len(c["methods"]) for c in f["classes"])
|
||||
for f in files.values())
|
||||
doc = {
|
||||
"_meta": {
|
||||
"generator": "scripts/rebase_inventory.py",
|
||||
"tranche": "R1 language rebase, Wave 0",
|
||||
"note": "Machine-generated. Regenerate with the script; never hand-edit.",
|
||||
"files": len(files),
|
||||
"functions_total": total_fns,
|
||||
"classes_total": sum(len(f["classes"]) for f in files.values()),
|
||||
"lines_total": sum(f["lines"] for f in files.values()),
|
||||
},
|
||||
"import_graph": {k: sorted(v) for k, v in sorted(graph.items())},
|
||||
"files": dict(sorted(files.items())),
|
||||
}
|
||||
OUT.write_text(json.dumps(doc, indent=1, sort_keys=False) + "\n", encoding="utf-8")
|
||||
m = doc["_meta"]
|
||||
print(f"inventory written: {OUT}")
|
||||
print(f"files={m['files']} functions={m['functions_total']} "
|
||||
f"classes={m['classes_total']} lines={m['lines_total']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tranche R1 Wave 0 — per-function issue factory.
|
||||
|
||||
Reads docs/rebase/rebase-inventory.json and emits one issue per unit (top-level
|
||||
function or class method) following tranche R1 §7. Wave-batched, dedup-safe
|
||||
(deterministic title + existing-issue scan before create), rate-limited.
|
||||
|
||||
Modes:
|
||||
plan — coverage report: units per wave, unassigned modules (no silent drops)
|
||||
sample — emit N rendered issues to a file for review (no Gitea call)
|
||||
create — create issues in Gitea for one wave
|
||||
|
||||
Dedup contract: issue titles are deterministic —
|
||||
[rebase w{wave}] {relpath}::{unit}
|
||||
The factory fetches existing issue titles once per run and skips any that
|
||||
already exist, so retries never duplicate (preserve-first Gitea addendum).
|
||||
|
||||
The factory NEVER writes behaviour prose. Current-behaviour paragraphs and
|
||||
the three evidence answers (IMPROVE / FIX / MISSING CONNECTORS) are filled at
|
||||
disposition time by the working agent — the issue carries the mandatory
|
||||
fields and line ranges, not fabricated content.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INVENTORY = REPO_ROOT / "docs" / "rebase" / "rebase-inventory.json"
|
||||
COVERAGE_OUT = REPO_ROOT / "docs" / "rebase" / "rebase-wave-coverage.json"
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://gitea.thingscouldgetdicey.online")
|
||||
REPO = "conductor/rune"
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "shared"))
|
||||
from gitea_auth import load_gitea_token # noqa: E402
|
||||
|
||||
TRANCHE_REF = "tranche R1 language rebase (2026-09-29), ~/.config/opencode/tranches/2026-09-29-language-rebase-tranche-R1.md"
|
||||
|
||||
# Wave membership from R1 §4 (initial, measured). Directory entries match by
|
||||
# path prefix; bare stems match the file stem. Modules matching no entry are
|
||||
# reported UNASSIGNED by `plan` — never silently dropped (R1 §9 risk 7).
|
||||
WAVE_PLAN = {
|
||||
2: [ # leaf utilities & canaries
|
||||
"pins", "time_awareness", "message_queue", "event_envelope",
|
||||
"attention_capsule", "complexity_scorer", "token_optimizer", "cache",
|
||||
"prompt_sync", "chat_bubble", "canary_*",
|
||||
],
|
||||
3: [ # memory & persistence
|
||||
"memory_store", "curator", "warm_router", "memory_consolidation",
|
||||
"graph_memory", "sleep_time", "night_mode", "identity_anchors",
|
||||
"query_memory", "memory_env", "warm_neuron_store", "subconscious_pass",
|
||||
"task_persist",
|
||||
],
|
||||
4: [ # provider/model layer
|
||||
"provider_registry", "model_connect", "model_routing", "model_profiles",
|
||||
"provider_budget", "standardized_providers", "model_registry",
|
||||
"connection_pool", "forge_client",
|
||||
],
|
||||
5: [ # orchestration, authority, plugins, skills, governance
|
||||
"orchestration/", "microkernel", "contracts", "build_mode", "plugins/",
|
||||
"skills/", "construction_authority", "construction_authority_cli",
|
||||
"construction_decision", "construction_mode", "verification_construction",
|
||||
"authority_recovery", "spine", "drift_core", "drift_ledger",
|
||||
"drift_observability", "persona_gate", "safety_override", "witness",
|
||||
"behavior", "growth_delta", "claim_audit", "verification_layer",
|
||||
"drift_compression", "drift_cache", "self_improve", "persona_definition",
|
||||
"outcome_ledger", "audit_ledger", "subsystem_health",
|
||||
],
|
||||
6: [ # runtime core & tooling
|
||||
"tool_executor", "tool_protocol", "local_tool_runtime",
|
||||
"capability_broker", "context_broker", "work_manager", "cron_engine",
|
||||
"auto_tasks", "goal", "failure_auto_filer", "hook_engine",
|
||||
"elastic_cognition", "event_bus", "system_registry", "abstraction",
|
||||
"tiered_executor", "progressive_escalation", "tier_manager",
|
||||
"parallel_processor", "shard_manager", "response_adapter",
|
||||
"context_pressure", "auto_context", "cognitive_budgeter",
|
||||
"recall_router", "inference_trace", "task_gate", "salience_gate",
|
||||
"dependency_tracker", "rune_profiler",
|
||||
],
|
||||
7: [ # operator + TUI, last by design
|
||||
"rune_operator", "rune_tui", "tui_validator", "voice_hybrid",
|
||||
"voice_sync", "voicebox", "voice_injector", "message_widgets",
|
||||
"rune_graphic", "rune_spinner", "__main__",
|
||||
],
|
||||
}
|
||||
|
||||
WAVE_NAMES = {
|
||||
2: "leaf utilities & canaries",
|
||||
3: "memory & persistence",
|
||||
4: "provider/model layer",
|
||||
5: "orchestration, authority, plugins, skills, governance",
|
||||
6: "runtime core & tooling",
|
||||
7: "operator + TUI",
|
||||
}
|
||||
|
||||
|
||||
def match_module(relpath: str):
|
||||
"""Return the wave number for a module path, or None if unassigned."""
|
||||
stem = Path(relpath).stem
|
||||
for wave, patterns in WAVE_PLAN.items():
|
||||
for pat in patterns:
|
||||
if pat.endswith("/"):
|
||||
if pat in relpath:
|
||||
return wave
|
||||
elif pat.endswith("*"):
|
||||
if stem.startswith(pat[:-1]):
|
||||
return wave
|
||||
elif stem == pat:
|
||||
return wave
|
||||
return None
|
||||
|
||||
|
||||
def iter_units(inv):
|
||||
"""Yield (relpath, unit_name, entry) for every function and method."""
|
||||
for relpath, mod in sorted(inv["files"].items()):
|
||||
for fn in mod["functions"]:
|
||||
yield relpath, fn["name"], fn
|
||||
for cls in mod["classes"]:
|
||||
for m in cls.get("methods", []):
|
||||
yield relpath, f"{cls['name']}.{m['name']}", m
|
||||
|
||||
|
||||
def internal_edges(relpath: str, entry: dict):
|
||||
"""Collect internal (in-repo) call names and line numbers for a unit."""
|
||||
lines = (entry["line_start"], entry["line_end"])
|
||||
mod = inv_cache["files"][relpath]
|
||||
calls = [c for c in mod.get("calls", [])
|
||||
if lines[0] <= c["line"] <= lines[1] and c.get("internal")]
|
||||
return calls
|
||||
|
||||
|
||||
inv_cache = {}
|
||||
|
||||
|
||||
def issue_title(wave: int, relpath: str, unit: str) -> str:
|
||||
return f"[rebase w{wave}] {relpath}::{unit}"
|
||||
|
||||
|
||||
def render_issue(wave: int, relpath: str, unit: str, entry: dict) -> dict:
|
||||
calls = internal_edges(relpath, entry)
|
||||
call_lines = "\n".join(
|
||||
f" - `{c['call']}` (line {c['line']})" for c in calls[:20]
|
||||
) or " - (no internal call edges in range)"
|
||||
sig = entry.get("signature", "()")
|
||||
body = f"""\
|
||||
|
||||
## Identity (from the Wave 0 inventory — machine-generated)
|
||||
|
||||
- **File**: `{relpath}`
|
||||
- **Unit**: `{unit}`
|
||||
- **Signature**: `def {unit}{sig}` (async: {entry.get('async', False)})
|
||||
- **Line range**: {entry['line_start']}–{entry['line_end']}
|
||||
- **Decorators**: {entry.get('decorators', []) or 'none'}
|
||||
- **Internal call edges** (callers/callees inside the repo, from inventory):
|
||||
{call_lines}
|
||||
|
||||
## Current behaviour
|
||||
|
||||
One honest paragraph from READING THE CODE at the line range above — what it
|
||||
actually does, not what the docstring promises. Write this at disposition
|
||||
time; the factory does not fabricate it.
|
||||
|
||||
## The three questions — each answered WITH EVIDENCE
|
||||
|
||||
1. **IMPROVE?** Can the unit be improved within its scope? Evidence or "no — because …".
|
||||
(Bounded to unit scope; larger findings spawn NEW issues, never expand this one.)
|
||||
2. **FIX?** Is it broken, buggy, or failing its expected outcome? Evidence: a failing
|
||||
test, a reproduced fault, or a code-level defect citation. A "no" needs evidence too.
|
||||
3. **MISSING CONNECTORS?** Are there expected outcomes this unit should connect to but
|
||||
doesn't (unwired events, dead parameters, promised-but-absent calls)? Evidence required.
|
||||
|
||||
A silent skip on any question is a factory bug — answer all three.
|
||||
|
||||
## Language verdict (per tranche R1 §6 rubric — evidence beats instinct)
|
||||
|
||||
Choose exactly one, with the evidence line:
|
||||
- **KEEP-PYTHON** — one sentence of justification.
|
||||
- **RECODE-<lang>** — rubric scores (all 7 dimensions) + benchmark-gate plan: the compiled
|
||||
replacement must beat the Python original on the MEASURED hot path, or the recode is
|
||||
rejected and recorded. No win, no recode.
|
||||
- **MERGE-into:<module>** — name the target and why.
|
||||
- **DEPRECATE** — name the caller that no longer needs it.
|
||||
|
||||
Go is NOT pre-decided. Python is not pre-decided. The rubric decides.
|
||||
|
||||
## Constraint context
|
||||
|
||||
- henxels structural contract: this unit's module ≤ 1,200 lines (or on the debt table's
|
||||
exception list — check before adding lines).
|
||||
- Spine: any module-surface change requires `store/spine/current.json` regen with the code
|
||||
change (`test_manifest_maps_every_module` is the arbiter for new top-level modules).
|
||||
- Debt table: docs/structural-contract.md.
|
||||
|
||||
## Pipeline plan
|
||||
|
||||
1. Branch `issue-<N>` from current master (existing issue-N convention).
|
||||
2. Scaffolding activation for any construction (construction_mode/decision per unit change).
|
||||
3. Research rounds are MANDATORY for any RECODE verdict (research-stage-headless; ≥2
|
||||
live-verified external sources per claim; per-source lessons; kill test per candidate).
|
||||
4. Implementation behind the interop contract (R1 §5): compiled units are standalone
|
||||
binaries — argv in, JSON on stdout, non-zero exit on failure, `--version` mandatory;
|
||||
Python wraps in a thin adapter; store stays the shared truth.
|
||||
5. Local gates: pytest + ruff + spine-store regen (if surface changed) + henxels.
|
||||
6. PR → pipeline-gate + ci + required checks → merge-robot → auto-version.
|
||||
|
||||
## Verification
|
||||
|
||||
- The specific test proving this unit's change: (write at disposition).
|
||||
- The keep-green set it must not break: full suite must stay green; named
|
||||
keep-green tests for this unit's callers.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Tranche: {TRANCHE_REF}
|
||||
- Wave: {wave} ({WAVE_NAMES.get(wave, 'unassigned')})
|
||||
- Inventory: docs/rebase/rebase-inventory.json (regenerate with scripts/rebase_inventory.py — never hand-edit)
|
||||
- Factory: scripts/rebase_issue_factory.py
|
||||
"""
|
||||
return {
|
||||
"title": issue_title(wave, relpath, unit),
|
||||
"body": body,
|
||||
"labels": ["rebase", f"wave-{wave}"],
|
||||
"meta": {
|
||||
"wave": wave,
|
||||
"file": relpath,
|
||||
"unit": unit,
|
||||
"line_start": entry["line_start"],
|
||||
"line_end": entry["line_end"],
|
||||
"async": entry.get("async", False),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def fetch_existing_titles() -> set:
|
||||
"""Fetch all issue titles in the repo (paginated) for dedup."""
|
||||
token = load_gitea_token()
|
||||
titles = set()
|
||||
page = 1
|
||||
while True:
|
||||
url = (f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
|
||||
f"?state=all&type=issues&limit=50&page={page}")
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
batch = json.loads(resp.read())
|
||||
if not batch:
|
||||
break
|
||||
titles.update(i["title"] for i in batch)
|
||||
if len(batch) < 50:
|
||||
break
|
||||
page += 1
|
||||
return titles
|
||||
|
||||
|
||||
def create_issue(entry: dict) -> dict:
|
||||
token = load_gitea_token()
|
||||
url = f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
|
||||
payload = json.dumps({
|
||||
"title": entry["title"],
|
||||
"body": entry["body"],
|
||||
"labels": [], # label NAMES are set via the labels endpoint by auto-label; keep create minimal
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=payload, method="POST",
|
||||
headers={"Authorization": f"token {token}",
|
||||
"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def cmd_plan(inv):
|
||||
counts = {}
|
||||
unassigned_modules = {}
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
wave = match_module(relpath)
|
||||
counts[wave] = counts.get(wave, 0) + 1
|
||||
if wave is None:
|
||||
unassigned_modules.setdefault(relpath, inv["files"][relpath]["lines"])
|
||||
report = {
|
||||
"units_per_wave": {str(k if k is not None else "UNASSIGNED"): v
|
||||
for k, v in sorted(counts.items(), key=lambda kv: str(kv[0]))},
|
||||
"unassigned_modules": dict(sorted(unassigned_modules.items(), key=lambda kv: -kv[1])),
|
||||
"unassigned_module_count": len(unassigned_modules),
|
||||
}
|
||||
print(json.dumps(report, indent=1))
|
||||
COVERAGE_OUT.write_text(json.dumps(report, indent=1) + "\n")
|
||||
print(f"\ncoverage written to {COVERAGE_OUT.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
return 0 if not unassigned_modules else 1
|
||||
|
||||
|
||||
def cmd_sample(inv, wave, limit, out):
|
||||
emitted = 0
|
||||
records = []
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
if match_module(relpath) != wave:
|
||||
continue
|
||||
records.append(render_issue(wave, relpath, unit, entry))
|
||||
emitted += 1
|
||||
if emitted >= limit:
|
||||
break
|
||||
Path(out).write_text("\n".join(json.dumps(r) for r in records) + "\n")
|
||||
print(f"sampled {emitted} issues for wave {wave} -> {out}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_create(inv, wave, limit, dry_gap):
|
||||
existing = fetch_existing_titles()
|
||||
created, skipped = 0, 0
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
if match_module(relpath) != wave:
|
||||
continue
|
||||
title = issue_title(wave, relpath, unit)
|
||||
if title in existing:
|
||||
skipped += 1
|
||||
continue
|
||||
issue = create_issue(render_issue(wave, relpath, unit, entry))
|
||||
created += 1
|
||||
print(f"created #{issue.get('number')}: {title}", file=sys.stderr)
|
||||
if limit and created >= limit:
|
||||
break
|
||||
time.sleep(dry_gap)
|
||||
print(f"\nwave {wave}: created={created} skipped(dedup)={skipped}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("command", choices=["plan", "sample", "create"])
|
||||
ap.add_argument("--wave", type=int, help="wave number (2-7)")
|
||||
ap.add_argument("--limit", type=int, default=0, help="max issues for sample/create (0 = unlimited; sample defaults to 5)")
|
||||
ap.add_argument("--out", default=None, help="sample output file")
|
||||
ap.add_argument("--gap", type=float, default=0.5, help="seconds between creates")
|
||||
args = ap.parse_args()
|
||||
|
||||
inv_cache.update(json.loads(INVENTORY.read_text()))
|
||||
inv = inv_cache
|
||||
|
||||
if args.command == "plan":
|
||||
return cmd_plan(inv)
|
||||
if args.command == "sample":
|
||||
if not args.wave:
|
||||
ap.error("--wave required")
|
||||
out = args.out or f"docs/rebase-factory-sample-w{args.wave}.json"
|
||||
return cmd_sample(inv, args.wave, args.limit or 5, out)
|
||||
if args.command == "create":
|
||||
if not args.wave:
|
||||
ap.error("--wave required")
|
||||
return cmd_create(inv, args.wave, args.limit, args.gap)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -50,7 +50,7 @@ SRC_A = {
|
||||
"memory_store": "memory", "memory_selector": "memory",
|
||||
"memory_consolidation": "memory", "memory_env": "memory",
|
||||
"shard_manager": "memory", "tier_manager": "memory",
|
||||
"graph_memory": "memory", "query_memory": "memory",
|
||||
"graph_memory": "memory",
|
||||
"curator": "memory", "sleep_time": "memory",
|
||||
"subconscious_pass": "memory", "warm_neuron_store": "memory",
|
||||
"warm_router": "memory", "abstraction": "memory",
|
||||
@@ -166,14 +166,12 @@ SCRIPTS_C = {"fake_worker", "tui_startup_canary"}
|
||||
|
||||
# shared/ — harness-facing layer; mirrors of canonical src modules = D
|
||||
SHARED_D = {
|
||||
"audit_ledger": "src/rune/audit_ledger.py",
|
||||
"dependency_tracker": "src/rune/dependency_tracker.py",
|
||||
"drift_cache": "src/rune/drift_cache.py",
|
||||
"drift_compression": "src/rune/drift_compression.py",
|
||||
"drift_core": "src/rune/drift_core.py",
|
||||
"drift_observability": "src/rune/drift_observability.py",
|
||||
"gitea_auth": "scripts/gitea_auth.py",
|
||||
"query_memory": "src/rune/query_memory.py",
|
||||
"shard_manager": "src/rune/shard_manager.py",
|
||||
"subconscious_pass": "src/rune/subconscious_pass.py",
|
||||
"tier_manager": "src/rune/tier_manager.py",
|
||||
@@ -182,7 +180,8 @@ SHARED_B_OWNER = "ops-harness"
|
||||
SHARED_B = {"auto_detect", "autonomy_loop", "build_orchestrator",
|
||||
"build_pipeline_policy", "cognitive_loop", "custodian",
|
||||
"dream_cycle", "gitea_pipeline", "gitea_provision",
|
||||
"graph_shard_bridge", "growth_simulator", "umbrella_update"}
|
||||
"graph_shard_bridge", "growth_simulator", "query_memory",
|
||||
"umbrella_update"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -47,7 +47,19 @@ def main() -> int:
|
||||
p for p in env.get("PATH", "").split(os.pathsep)
|
||||
if "ci-venv" not in p and ".type-venv" not in p and "venv/bin" not in p)
|
||||
result = subprocess.run(
|
||||
cmd + [str(ROOT / "src" / "rune"), "--outputjson"],
|
||||
# §16V AMBIENT-INTERPRETER CLOSURE (T7.3 slice-2 CI repair, 2026-10-03):
|
||||
# venv-signal stripping fixed ACTIVATION drift, but dependency
|
||||
# resolution still followed the AMBIENT host interpreter — runner
|
||||
# (py311, no rich/Pillow) vs desktop (3.14 system site with both)
|
||||
# produced 460 vs 454 on the identical tree, and any import-level
|
||||
# ignore flips to reportUnnecessaryTypeIgnoreComment in the other
|
||||
# environment. Pin --pythonpath to the INVOKING interpreter (the
|
||||
# venv running this script) so dependency resolution follows the
|
||||
# installed lock, not the host: CI's .type-venv has the locked deps;
|
||||
# local preflight must run the ratchet from a venv built from
|
||||
# requirements-ci.txt (same lock) — that is now the contract.
|
||||
cmd + [str(ROOT / "src" / "rune"), "--outputjson",
|
||||
"--pythonpath", sys.executable],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""audit_ledger.py — HMAC-chained tamper-evident promotion ledger.
|
||||
|
||||
Every promotion decision is recorded in a hash-chained ledger.
|
||||
Tampering with any entry breaks the chain.
|
||||
|
||||
Research basis:
|
||||
- PoEM (arXiv:2608.16032): HMAC-chained tamper-evident execution ledger
|
||||
- Autogenesis (arXiv:2604.15034): versioned interfaces, auditable lineage
|
||||
|
||||
Run under ~/.venvs/memory/bin/python3.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
LEDGER_FILE = os.path.expanduser("~/.config/opencode/memory/promotion_ledger.jsonl")
|
||||
SECRET_KEY = os.environ.get("AUDIT_LEDGER_KEY", "rune-drift-integrity-2024")
|
||||
|
||||
|
||||
def _compute_hash(prev_hash: str, entry: dict) -> str:
|
||||
"""Compute HMAC chain hash for a ledger entry."""
|
||||
payload = json.dumps(entry, sort_keys=True).encode()
|
||||
msg = (prev_hash + payload.decode()).encode()
|
||||
return hmac.new(SECRET_KEY.encode(), msg, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
|
||||
"""Append an entry to the promotion ledger.
|
||||
|
||||
Returns the ledger entry with chain hash.
|
||||
"""
|
||||
# Read last hash
|
||||
prev_hash = "genesis"
|
||||
if os.path.exists(LEDGER_FILE):
|
||||
with open(LEDGER_FILE, "r") as f:
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
try:
|
||||
last = json.loads(lines[-1])
|
||||
prev_hash = last.get("hash", "genesis")
|
||||
except json.JSONDecodeError:
|
||||
prev_hash = "genesis"
|
||||
|
||||
entry = {
|
||||
"type": entry_type,
|
||||
"entry_id": entry_id,
|
||||
"details": details,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"prev_hash": prev_hash,
|
||||
}
|
||||
|
||||
# Compute chain hash
|
||||
entry["hash"] = _compute_hash(prev_hash, entry)
|
||||
|
||||
# Append to ledger
|
||||
os.makedirs(os.path.dirname(LEDGER_FILE), exist_ok=True)
|
||||
with open(LEDGER_FILE, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def verify_chain() -> dict:
|
||||
"""Verify the integrity of the entire ledger chain.
|
||||
|
||||
Returns dict with verification result.
|
||||
"""
|
||||
if not os.path.exists(LEDGER_FILE):
|
||||
return {"valid": True, "entries": 0, "reason": "empty ledger"}
|
||||
|
||||
with open(LEDGER_FILE, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
prev_hash = "genesis"
|
||||
for i, line in enumerate(lines):
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
return {"valid": False, "entries": i, "reason": f"invalid JSON at line {i+1}"}
|
||||
|
||||
# Verify prev_hash chain
|
||||
if entry.get("prev_hash") != prev_hash:
|
||||
return {
|
||||
"valid": False,
|
||||
"entries": i,
|
||||
"reason": f"chain broken at line {i+1}: expected prev_hash={prev_hash}, got {entry.get('prev_hash')}"
|
||||
}
|
||||
|
||||
# Verify HMAC
|
||||
stored_hash = entry.get("hash")
|
||||
computed = _compute_hash(prev_hash, {k: v for k, v in entry.items() if k != "hash"})
|
||||
if stored_hash != computed:
|
||||
return {
|
||||
"valid": False,
|
||||
"entries": i,
|
||||
"reason": f"HMAC mismatch at line {i+1}: stored={stored_hash[:16]}..., computed={computed[:16]}..."
|
||||
}
|
||||
|
||||
prev_hash = stored_hash
|
||||
|
||||
return {"valid": True, "entries": len(lines), "reason": "chain intact"}
|
||||
|
||||
|
||||
def get_entries(limit: int = 50) -> List[dict]:
|
||||
"""Get recent ledger entries."""
|
||||
if not os.path.exists(LEDGER_FILE):
|
||||
return []
|
||||
|
||||
with open(LEDGER_FILE, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
entries = []
|
||||
for line in lines[-limit:]:
|
||||
try:
|
||||
entries.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return entries
|
||||
|
||||
|
||||
def log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: int, reason: str) -> dict:
|
||||
"""Log a promotion decision."""
|
||||
return append_entry("promotion", entry_id, {
|
||||
"old_status": old_status,
|
||||
"new_status": new_status,
|
||||
"wash_count": wash_count,
|
||||
"reason": reason,
|
||||
})
|
||||
|
||||
|
||||
def log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
|
||||
"""Log a pre-commit gate result."""
|
||||
return append_entry("gate", entry_id, {
|
||||
"verdict": verdict,
|
||||
"critics": critics,
|
||||
})
|
||||
|
||||
|
||||
def log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
|
||||
"""Log a drift alert."""
|
||||
return append_entry("alert", 0, {
|
||||
"alert_type": alert_type,
|
||||
"message": message,
|
||||
"severity": severity,
|
||||
})
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Promotion audit ledger")
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
|
||||
# verify
|
||||
sub.add_parser("verify", help="Verify ledger chain integrity")
|
||||
|
||||
# list [--limit N]
|
||||
p_list = sub.add_parser("list", help="Show recent ledger entries")
|
||||
p_list.add_argument("--limit", type=int, default=20)
|
||||
|
||||
# log-promotion <entry_id> <old> <new> <wash_count> <reason>
|
||||
p_log = sub.add_parser("log-promotion", help="Log a promotion")
|
||||
p_log.add_argument("entry_id", type=int)
|
||||
p_log.add_argument("old_status")
|
||||
p_log.add_argument("new_status")
|
||||
p_log.add_argument("wash_count", type=int)
|
||||
p_log.add_argument("reason")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.cmd:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if args.cmd == "verify":
|
||||
result = verify_chain()
|
||||
print(f"Chain valid: {result['valid']}")
|
||||
print(f"Entries: {result['entries']}")
|
||||
print(f"Reason: {result['reason']}")
|
||||
|
||||
elif args.cmd == "list":
|
||||
entries = get_entries(args.limit)
|
||||
if entries:
|
||||
print(f"Recent ledger entries ({len(entries)}):")
|
||||
for e in entries:
|
||||
print(f" [{e.get('type')}] entry_id={e.get('entry_id')} @ {e.get('timestamp', '')[:19]}")
|
||||
details = e.get('details', {})
|
||||
if e.get('type') == 'promotion':
|
||||
print(f" {details.get('old_status')} → {details.get('new_status')} (wash={details.get('wash_count')})")
|
||||
elif e.get('type') == 'gate':
|
||||
print(f" verdict={details.get('verdict')}")
|
||||
else:
|
||||
print("No ledger entries.")
|
||||
|
||||
elif args.cmd == "log-promotion":
|
||||
entry = log_promotion(args.entry_id, args.old_status, args.new_status, args.wash_count, args.reason)
|
||||
print(f"Logged: {entry['hash'][:16]}...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+88
-12
@@ -33,7 +33,7 @@ SCRIPTS_DIR = os.path.expanduser("~/.config/opencode/scripts")
|
||||
SHARED_DIR = os.path.expanduser("~/.config/opencode/shared")
|
||||
CUSTODIAN_STATE_FILE = os.path.expanduser("~/.config/opencode/memory/custodian_state.json")
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://gitea.thingscouldgetdicey.online")
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "http://127.0.0.1:3000") # gitea.internal (PC); external = https://gitea.thingscouldgetdicey.online via Pi Caddy
|
||||
GITEA_TOKEN = load_gitea_token()
|
||||
REPO = "conductor/rune"
|
||||
TUI_DIR = "/mnt/workdrive/rune"
|
||||
@@ -54,6 +54,15 @@ UMBRELLA_TAGS = {
|
||||
}
|
||||
|
||||
|
||||
# Issue template directory — canonical copies live beside this script
|
||||
# (mirrored to .gitea/ISSUE_TEMPLATE/ for the web-UI submission path).
|
||||
ISSUE_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "issue_templates")
|
||||
|
||||
# kind -> Gitea label that MUST be present on the issue
|
||||
KIND_LABEL = {"feature": "feature", "bug": "bug", "research": "research"}
|
||||
VERSION_INTENTS = ("major", "minor", "patch", "none")
|
||||
|
||||
|
||||
class Custodian:
|
||||
"""The Custodian agent for Gitea-based self-improvement."""
|
||||
|
||||
@@ -105,23 +114,90 @@ class Custodian:
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:200]}
|
||||
|
||||
def create_issue(self, title: str, body: str, labels: List[str] = None) -> dict:
|
||||
"""Create a Gitea issue with plain-language description.
|
||||
def _ensure_label(self, name: str, description: str = "", color: str = "#6f42c1") -> bool:
|
||||
"""Make sure a label exists on the repo; return its id or None."""
|
||||
labels = self._gitea_api("labels")
|
||||
if isinstance(labels, list):
|
||||
for l in labels:
|
||||
if l["name"] == name:
|
||||
return l["id"]
|
||||
data = {"name": name, "description": description or name, "color": color}
|
||||
created = self._gitea_api("labels", method="POST", data=data)
|
||||
return created.get("id") if isinstance(created, dict) else None
|
||||
|
||||
Labels should be umbrella names (memory, governance, etc).
|
||||
They get converted to IDs automatically.
|
||||
def _load_issue_template(self, kind: str) -> str:
|
||||
"""Load the canonical issue template for a kind, WITHOUT the web-UI
|
||||
frontmatter (name/about/title/labels — that metadata belongs to the
|
||||
submission form, not the issue body). Empty string if absent."""
|
||||
path = os.path.join(ISSUE_TEMPLATE_DIR, f"{kind}.md")
|
||||
try:
|
||||
with open(path) as f:
|
||||
text = f.read()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
if text.startswith("---"):
|
||||
end = text.find("\n---", 3)
|
||||
if end != -1:
|
||||
text = text[end + 4:]
|
||||
return text.lstrip("\n")
|
||||
|
||||
def create_issue(
|
||||
self,
|
||||
title: str,
|
||||
body: str,
|
||||
labels: List[str] = None,
|
||||
kind: str = "feature",
|
||||
version_intent: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a Gitea issue assembled from the canonical template.
|
||||
|
||||
kind: feature | bug | research — selects the template and forces the
|
||||
matching label (the research label is auto-created if missing).
|
||||
version_intent: major | minor | patch | none — pre-ticks the matching
|
||||
box in the template's Version impact section. The pipeline
|
||||
verifies the real bump at merge and flags mismatches.
|
||||
labels: umbrella names (memory, governance, ...); converted to IDs.
|
||||
|
||||
The caller's body replaces the SUBMITTED_BODY marker inside the
|
||||
template's Plain English section — plain terms first, always.
|
||||
|
||||
ORGANISATION STANDARD (one open note per idea, 2026-09-19): before
|
||||
minting a standalone issue, check whether the work belongs inside an
|
||||
existing parent. Ideas parked under a blocked/future epic are filed
|
||||
as checklist items in the parent's "Absorbed children" section, NOT
|
||||
as standalone issues — the board stays uncluttered and every idea
|
||||
has exactly one home. Mint a standalone issue only when the work is
|
||||
live, dependency-tracked, or about to start. Absorbed ideas are
|
||||
reversible: re-mint standalone when they become live.
|
||||
"""
|
||||
# Convert label names to IDs
|
||||
# 1. assemble body from template (graceful: unformatted if template missing)
|
||||
template = self._load_issue_template(kind)
|
||||
text = body.strip()
|
||||
if template and "<!-- SUBMITTED_BODY -->" in template:
|
||||
text = template.replace("<!-- SUBMITTED_BODY -->", text, 1)
|
||||
if version_intent in VERSION_INTENTS:
|
||||
text = text.replace(f"- [ ] {version_intent} ", f"- [x] {version_intent} ", 1)
|
||||
|
||||
# 2. labels: umbrella names + mandatory kind label
|
||||
labels = list(labels or [])
|
||||
kind_label = KIND_LABEL.get(kind)
|
||||
if kind_label and kind_label not in labels:
|
||||
labels.append(kind_label)
|
||||
|
||||
all_labels = self._gitea_api("labels")
|
||||
label_map = {l["name"]: l["id"] for l in all_labels} if isinstance(all_labels, list) else {}
|
||||
label_ids = []
|
||||
if labels:
|
||||
all_labels = self._gitea_api("labels")
|
||||
if isinstance(all_labels, list):
|
||||
label_map = {l["name"]: l["id"] for l in all_labels}
|
||||
label_ids = [label_map[l] for l in labels if l in label_map]
|
||||
for l in labels:
|
||||
if l in label_map:
|
||||
label_ids.append(label_map[l])
|
||||
else:
|
||||
lid = self._ensure_label(l)
|
||||
if lid:
|
||||
label_ids.append(lid)
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"body": text,
|
||||
"labels": label_ids,
|
||||
}
|
||||
result = self._gitea_api("issues", method="POST", data=data)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
|
||||
**WHAT (bug):**
|
||||
**WHERE:**
|
||||
**EVIDENCE:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every fix must clear these before the issue closes. -->
|
||||
- [ ] TDD: regression test written first, red → green
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
WHY — design-spec reference (S#) or reason.
|
||||
FILES — affected components.
|
||||
ACCEPTANCE — frozen-test file + test count + canary name.
|
||||
DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
|
||||
**WHAT:**
|
||||
**WHY:**
|
||||
**FILES:**
|
||||
**ACCEPTANCE:**
|
||||
**DEPENDENCIES:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every stage must clear these before the issue closes. -->
|
||||
- [ ] TDD: frozen tests written first, red → green (N passed)
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -0,0 +1,216 @@
|
||||
"""Thin adapter for native (compiled) units — R1 §5 interop contract, Wave 0.
|
||||
|
||||
The contract under proof: compiled units are standalone binaries exposing a
|
||||
CLI boundary — argv in, JSON on stdout, non-zero exit on failure, a mandatory
|
||||
``--version`` flag. This module is the Python side: run a unit, enforce a
|
||||
timeout, capture stderr, and return a TYPED result. It never raises for unit
|
||||
failures (those are data in the result); it raises only for adapter misuse
|
||||
(missing binary, bad timeout).
|
||||
|
||||
No IPC daemon, no PyO3 — the file-based store stays the shared truth. If
|
||||
research later surfaces a materially better pattern it competes through the
|
||||
research pipeline; the CLI boundary is the default, not a cage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
PROOF_UNIT = REPO_ROOT / "interop" / "rebase-proof" / "target" / "release" / "rebase-proof"
|
||||
|
||||
|
||||
@dataclass
|
||||
class NativeResult:
|
||||
"""Typed result of one native-unit invocation.
|
||||
|
||||
ok — the invocation succeeded (exit 0 AND not timed out); when
|
||||
the unit emitted JSON with an ``ok`` field, that field must
|
||||
not be False (plain-text output like ``--version`` has none)
|
||||
exit_code — raw process exit code
|
||||
data — parsed JSON object from stdout, or None if unparseable
|
||||
stderr — captured stderr text (may be non-empty even on success)
|
||||
duration_ms — wall-clock cost of the invocation
|
||||
timed_out — True when the unit exceeded the timeout and was killed
|
||||
events — in streaming mode: every parsed JSON object line, in
|
||||
arrival order (empty for non-streaming :func:`run_native`)
|
||||
"""
|
||||
|
||||
ok: bool
|
||||
exit_code: int
|
||||
data: dict | None
|
||||
stderr: str
|
||||
duration_ms: int
|
||||
timed_out: bool = False
|
||||
raw_stdout: str = field(default="", repr=False)
|
||||
events: list[dict] = field(default_factory=list)
|
||||
|
||||
|
||||
def run_native(binary: Path, args: list[str], timeout_s: float = 10.0) -> NativeResult:
|
||||
"""Invoke a native unit and return a typed result.
|
||||
|
||||
Raises FileNotFoundError only when *binary* itself is absent (adapter
|
||||
misuse) — every unit-level failure is reported via the result, never an
|
||||
exception, so callers can branch on ``result.ok`` without try/except.
|
||||
"""
|
||||
if not binary.is_file():
|
||||
raise FileNotFoundError(f"native unit not built: {binary}")
|
||||
|
||||
started = time.monotonic()
|
||||
timed_out = False
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[str(binary), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_s,
|
||||
check=False, # exit codes are data, never exceptions
|
||||
)
|
||||
exit_code, stdout, stderr = proc.returncode, proc.stdout, proc.stderr
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
timed_out = True
|
||||
exit_code = -1
|
||||
stdout = exc.stdout or "" if isinstance(exc.stdout, str) else (exc.stdout or b"").decode(errors="replace")
|
||||
stderr = exc.stderr or "" if isinstance(exc.stderr, str) else (exc.stderr or b"").decode(errors="replace")
|
||||
|
||||
duration_ms = int((time.monotonic() - started) * 1000)
|
||||
|
||||
data: dict | None = None
|
||||
stripped = stdout.strip()
|
||||
if stripped:
|
||||
try:
|
||||
parsed = json.loads(stripped.splitlines()[-1])
|
||||
if isinstance(parsed, dict):
|
||||
data = parsed
|
||||
except json.JSONDecodeError:
|
||||
data = None
|
||||
|
||||
ok = (not timed_out) and exit_code == 0 and (data is None or data.get("ok") is not False)
|
||||
return NativeResult(
|
||||
ok=ok,
|
||||
exit_code=exit_code,
|
||||
data=data,
|
||||
stderr=stderr,
|
||||
duration_ms=duration_ms,
|
||||
timed_out=timed_out,
|
||||
raw_stdout=stdout,
|
||||
)
|
||||
|
||||
|
||||
def run_proof(args: list[str], timeout_s: float = 10.0) -> NativeResult:
|
||||
"""Run the Wave 0 proof unit with the standard contract defaults."""
|
||||
return run_native(PROOF_UNIT, args, timeout_s=timeout_s)
|
||||
|
||||
|
||||
def stream_native(
|
||||
binary: Path,
|
||||
args: list[str],
|
||||
timeout_s: float = 10.0,
|
||||
on_event=None,
|
||||
):
|
||||
"""Run a native unit in JSONL streaming mode, yielding events as they arrive.
|
||||
|
||||
The JSONL streaming contract (rubric §W7): one JSON object per stdout
|
||||
line — ``{"ok": true, "event": ...}`` progress objects along the way, and
|
||||
a terminal envelope as the FINAL line. This generator yields each parsed
|
||||
event dict the moment it arrives (so a long op surfaces partial progress
|
||||
instead of blocking until exit), then yields exactly one terminal
|
||||
:class:`NativeResult`. *on_event*, when given, is called with each parsed
|
||||
event as it arrives (the TUI wraps this in ``call_from_thread``).
|
||||
|
||||
Stream-mode ok-semantics are stricter than :func:`run_native`: ``ok``
|
||||
requires exit 0, no timeout, AND a valid terminal envelope (the final
|
||||
stdout line parsed as a JSON object whose ``ok`` is not False). Malformed
|
||||
interior lines are skipped and counted in ``malformed_lines``; malformed
|
||||
FINAL line means no valid terminal envelope, so ``ok`` is False — partial
|
||||
progress stays available in ``events``. That is the point of streaming.
|
||||
|
||||
Same failure discipline as :func:`run_native`: raises FileNotFoundError
|
||||
when *binary* is absent; every unit-level failure (bad envelope, timeout,
|
||||
non-zero exit) is data on the terminal result, never an exception.
|
||||
"""
|
||||
if on_event is not None and not callable(on_event):
|
||||
raise TypeError("on_event must be callable")
|
||||
if not binary.is_file():
|
||||
raise FileNotFoundError(f"native unit not built: {binary}")
|
||||
|
||||
started = time.monotonic()
|
||||
proc = subprocess.Popen(
|
||||
[str(binary), *args],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# stderr must drain concurrently — reading it after the stdout loop
|
||||
# deadlocks when a unit floods stderr past the pipe buffer mid-stream.
|
||||
stderr_parts: list[str] = []
|
||||
stderr_drain = threading.Thread(
|
||||
target=lambda: stderr_parts.append(proc.stderr.read()), daemon=True
|
||||
)
|
||||
stderr_drain.start()
|
||||
|
||||
killed = threading.Event()
|
||||
# The watchdog's kill and the flag are one step so an adapter-side
|
||||
# timeout is never mistaken for a unit dying by its own signal.
|
||||
watchdog = threading.Timer(timeout_s, lambda: (killed.set(), proc.kill()))
|
||||
watchdog.start()
|
||||
|
||||
events: list[dict] = []
|
||||
malformed_lines = 0
|
||||
# True only while the most recent non-empty stdout line parsed as a JSON
|
||||
# object — a malformed final line invalidates the terminal envelope.
|
||||
final_line_is_json = False
|
||||
try:
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
parsed = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
malformed_lines += 1
|
||||
final_line_is_json = False
|
||||
continue
|
||||
if not isinstance(parsed, dict):
|
||||
malformed_lines += 1
|
||||
final_line_is_json = False
|
||||
continue
|
||||
events.append(parsed)
|
||||
final_line_is_json = True
|
||||
if on_event is not None:
|
||||
on_event(parsed)
|
||||
yield parsed
|
||||
proc.wait()
|
||||
finally:
|
||||
watchdog.cancel()
|
||||
stderr_drain.join(timeout=5.0)
|
||||
|
||||
timed_out = killed.is_set()
|
||||
duration_ms = int((time.monotonic() - started) * 1000)
|
||||
exit_code = proc.returncode if proc.returncode is not None else -1
|
||||
if timed_out:
|
||||
exit_code = -1 # adapter-side kill, never a unit's own exit
|
||||
|
||||
terminal = events[-1] if (events and final_line_is_json) else None
|
||||
ok = (
|
||||
not timed_out
|
||||
and exit_code == 0
|
||||
and terminal is not None
|
||||
and terminal.get("ok") is not False
|
||||
)
|
||||
yield NativeResult(
|
||||
ok=ok,
|
||||
exit_code=exit_code,
|
||||
data=terminal,
|
||||
stderr=stderr_parts[0] if stderr_parts else "",
|
||||
duration_ms=duration_ms,
|
||||
timed_out=timed_out,
|
||||
events=events,
|
||||
)
|
||||
|
||||
+60
-134
@@ -1,17 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""query_memory.py — Query memory system for adaptive retrieval.
|
||||
|
||||
The system remembers what kinds of queries it gets, what retrieval
|
||||
strategy worked, and adapts its gate logic accordingly.
|
||||
AUTHORITY-SWITCHED (Phase G strangler slice 1, doctrine step 8): the
|
||||
classification core now lives in Rust (`src/rust/query_memory/src/lib.rs`,
|
||||
cdylib `libquery_memory.so`). This module is a thin ctypes-backed shim over
|
||||
`qm_loader.py` serving the FULL historical Python class API — the deployed
|
||||
in-process consumers (`memory_store.py`, `growth_simulator.py`) call
|
||||
`QueryMemory()` exactly as before and get the Rust core.
|
||||
|
||||
This prevents dead weight — memories get tested based on usage
|
||||
patterns, not just random exploration.
|
||||
Parity evidence: FFI-gap parity EXACT-EQUAL on `get_topic_stats` /
|
||||
`get_dead_weight_topics`; verification wall green (FFI smoke 41/41, parity
|
||||
harness ALL GREEN, Python canary 12/12, Rust canary 27/27); shadow comparison
|
||||
46/46 exact-equal vs the live production store.
|
||||
|
||||
Research basis:
|
||||
- IGMiRAG (arXiv:2602.07525): Intuition-guided retrieval
|
||||
- Task-Adaptive Retrieval (arXiv:2604.07863): Learning from interaction history
|
||||
- MemCon (arXiv:2607.13591): Learned policy for consolidation/forgetting
|
||||
- Agent-Native Memory (arXiv:2606.24775): Workload-adaptive routing
|
||||
The Python implementation this replaces is preserved in git history on branch
|
||||
`phase-g-strangler-retrieval-index` (pre-shim commit); per deletion discipline
|
||||
it is not kept as a silent fallback — a missing cdylib fails loudly.
|
||||
|
||||
Store resolution is unchanged: `$HOME/.config/opencode/memory/query_memory`
|
||||
(the Rust core resolves `$HOME` natively; history.jsonl / topic_stats.json
|
||||
formats are byte-parity-proven).
|
||||
|
||||
Run under ~/.venvs/memory/bin/python3.
|
||||
"""
|
||||
@@ -19,15 +27,14 @@ Run under ~/.venvs/memory/bin/python3.
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
QUERY_MEMORY_DIR = os.path.expanduser("~/.config/opencode/memory/query_memory")
|
||||
QUERY_HISTORY_FILE = os.path.join(QUERY_MEMORY_DIR, "history.jsonl")
|
||||
TOPIC_STATS_FILE = os.path.join(QUERY_MEMORY_DIR, "topic_stats.json")
|
||||
|
||||
# Topic keywords for classification
|
||||
# Topic keywords (classification table — tie-break law: insertion order is
|
||||
# part of the contract; the Rust core carries the identical table).
|
||||
TOPIC_KEYWORDS = {
|
||||
"guitar": ["guitar", "music", "teaching", "course", "lesson", "chord", "scale"],
|
||||
"coding": ["code", "python", "script", "function", "debug", "build", "deploy"],
|
||||
@@ -41,17 +48,47 @@ TOPIC_KEYWORDS = {
|
||||
"creative": ["write", "design", "create", "build", "make", "art", "music"],
|
||||
}
|
||||
|
||||
# Where the Rust core (qm_loader.py + target/release/libquery_memory.so) lives.
|
||||
_QM_CORE_DIR = os.environ.get(
|
||||
"QM_CORE_DIR", "/mnt/workdrive/rune/src/rust/query_memory"
|
||||
)
|
||||
|
||||
class QueryMemory:
|
||||
"""Query memory system for adaptive retrieval."""
|
||||
if _QM_CORE_DIR not in sys.path:
|
||||
sys.path.insert(0, _QM_CORE_DIR)
|
||||
|
||||
try:
|
||||
from qm_loader import QueryMemory as _RustQueryMemory
|
||||
except ImportError as _e: # pragma: no cover - deployment integrity failure
|
||||
raise ImportError(
|
||||
f"query_memory shim: cannot load the Rust core from {_QM_CORE_DIR!r} "
|
||||
f"({_e}). The cdylib must be built: `cargo build --release` in "
|
||||
f"{_QM_CORE_DIR}. No Python fallback exists by design."
|
||||
) from _e
|
||||
|
||||
|
||||
class QueryMemory(_RustQueryMemory):
|
||||
"""Query memory system for adaptive retrieval (Rust core, Python API).
|
||||
|
||||
API (identical to the historical Python implementation):
|
||||
* classify_query(query) -> str
|
||||
* adaptive_exploration_rate(query) -> float
|
||||
* record_query(query, strategy, success) -> None
|
||||
* get_topic_stats() -> dict (topic -> stats, insertion order)
|
||||
* get_dead_weight_topics() -> list[str]
|
||||
* get_health_report() -> dict
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Match the historical constructor: ensure the store dir exists,
|
||||
# resolve the store from $HOME (qm_new's native behaviour).
|
||||
os.makedirs(QUERY_MEMORY_DIR, exist_ok=True)
|
||||
self.history: List[dict] = self._load_history()
|
||||
self.topic_stats: Dict[str, dict] = self._load_topic_stats()
|
||||
super().__init__()
|
||||
# Historical in-memory attribute: query history loaded at construction,
|
||||
# appended by record_query. (canary check 2 reads len(qm.history).)
|
||||
self.history = self._load_history()
|
||||
|
||||
def _load_history(self) -> List[dict]:
|
||||
"""Load query history from disk."""
|
||||
def _load_history(self):
|
||||
"""Load query history from disk (historical shape: list of dicts)."""
|
||||
if not os.path.exists(QUERY_HISTORY_FILE):
|
||||
return []
|
||||
entries = []
|
||||
@@ -63,127 +100,16 @@ class QueryMemory:
|
||||
continue
|
||||
return entries
|
||||
|
||||
def _save_history(self, entry: dict):
|
||||
"""Append a query entry to history."""
|
||||
with open(QUERY_HISTORY_FILE, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
def _load_topic_stats(self) -> Dict[str, dict]:
|
||||
"""Load topic statistics from disk."""
|
||||
if os.path.exists(TOPIC_STATS_FILE):
|
||||
try:
|
||||
with open(TOPIC_STATS_FILE) as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _save_topic_stats(self):
|
||||
"""Save topic statistics to disk."""
|
||||
with open(TOPIC_STATS_FILE, "w") as f:
|
||||
json.dump(self.topic_stats, f, indent=2)
|
||||
|
||||
def classify_query(self, query: str) -> str:
|
||||
"""Classify a query into a topic.
|
||||
|
||||
Returns the topic with the most keyword matches.
|
||||
Falls back to "general" if no strong match.
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
scores = {}
|
||||
for topic, keywords in TOPIC_KEYWORDS.items():
|
||||
score = sum(1 for kw in keywords if kw in query_lower)
|
||||
if score > 0:
|
||||
scores[topic] = score
|
||||
|
||||
if not scores:
|
||||
return "general"
|
||||
|
||||
return max(scores, key=scores.get)
|
||||
|
||||
def record_query(self, query: str, strategy: str, success: bool):
|
||||
"""Record a query and its outcome.
|
||||
|
||||
Args:
|
||||
query: The query text
|
||||
strategy: "consolidated" or "full_retrieval"
|
||||
success: Whether the retrieval produced useful results
|
||||
"""
|
||||
topic = self.classify_query(query)
|
||||
entry = {
|
||||
"""Record through the Rust core, then mirror the historical in-memory append."""
|
||||
super().record_query(query, strategy, success)
|
||||
self.history.append({
|
||||
"query": query[:100],
|
||||
"topic": topic,
|
||||
"topic": self.classify_query(query),
|
||||
"strategy": strategy,
|
||||
"success": success,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
self.history.append(entry)
|
||||
self._save_history(entry)
|
||||
|
||||
# Update topic stats
|
||||
if topic not in self.topic_stats:
|
||||
self.topic_stats[topic] = {
|
||||
"total_queries": 0,
|
||||
"successful_queries": 0,
|
||||
"consolidated_hits": 0,
|
||||
"full_retrieval_hits": 0,
|
||||
"exploration_rate": 0.30, # default
|
||||
}
|
||||
|
||||
stats = self.topic_stats[topic]
|
||||
stats["total_queries"] += 1
|
||||
if success:
|
||||
stats["successful_queries"] += 1
|
||||
if strategy == "consolidated":
|
||||
stats["consolidated_hits"] += 1
|
||||
else:
|
||||
stats["full_retrieval_hits"] += 1
|
||||
|
||||
# Adaptive exploration rate: decrease for well-tested topics
|
||||
if stats["total_queries"] >= 10:
|
||||
success_rate = stats["successful_queries"] / stats["total_queries"]
|
||||
if success_rate > 0.8:
|
||||
stats["exploration_rate"] = max(0.10, stats["exploration_rate"] - 0.05)
|
||||
elif success_rate < 0.5:
|
||||
stats["exploration_rate"] = min(0.50, stats["exploration_rate"] + 0.05)
|
||||
|
||||
self._save_topic_stats()
|
||||
|
||||
def get_topic_stats(self) -> Dict[str, dict]:
|
||||
"""Get topic statistics."""
|
||||
return self.topic_stats
|
||||
|
||||
def adaptive_exploration_rate(self, query: str) -> float:
|
||||
"""Get the exploration rate for a query based on its topic.
|
||||
|
||||
Well-tested topics have lower exploration rates.
|
||||
Untested topics have higher exploration rates.
|
||||
"""
|
||||
topic = self.classify_query(query)
|
||||
if topic in self.topic_stats:
|
||||
return self.topic_stats[topic].get("exploration_rate", 0.30)
|
||||
return 0.50 # untested topics get high exploration
|
||||
|
||||
def get_dead_weight_topics(self) -> List[str]:
|
||||
"""Get topics with low proven uses (dead weight)."""
|
||||
dead = []
|
||||
for topic, stats in self.topic_stats.items():
|
||||
if stats["total_queries"] < 5:
|
||||
dead.append(topic)
|
||||
return dead
|
||||
|
||||
def get_health_report(self) -> dict:
|
||||
"""Get a health report of the query memory system."""
|
||||
total_queries = len(self.history)
|
||||
topics = len(self.topic_stats)
|
||||
dead_topics = len(self.get_dead_weight_topics())
|
||||
|
||||
return {
|
||||
"total_queries": total_queries,
|
||||
"topics_tracked": topics,
|
||||
"dead_weight_topics": dead_topics,
|
||||
"topic_stats": self.topic_stats,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune-entry
|
||||
|
||||
go 1.27.1
|
||||
@@ -0,0 +1,172 @@
|
||||
// Command rune is the Go first-call entry for the Rune runtime (Phase H1).
|
||||
//
|
||||
// Directive (USER-LOCKED 2026-10-04): Go owns the front end — runtime entry
|
||||
// points, the first-call surface, user-facing calls. This binary is the
|
||||
// transitional first-call shim: it replicates the EXACT contract of the
|
||||
// deployed bash launcher (~/.local/bin/rune) — same cwd, same env, same
|
||||
// handoff — and exec-replaces itself with the Python runtime so exit codes
|
||||
// and signals pass through 1:1. The Python core shrinks behind this surface
|
||||
// as the back-end ports land.
|
||||
//
|
||||
// Contract (H1 canary, observed from the deployed launcher — not invented):
|
||||
// cwd: /mnt/workdrive/rune-code/current/src (the deployed revision)
|
||||
// env: RUNE_HOME=/mnt/workdrive/rune
|
||||
// RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory
|
||||
// MIMO_PROBE_ENABLED=1
|
||||
// handoff: ~/.venvs/memory/bin/python3 -m rune [args...]
|
||||
//
|
||||
// Flags consumed by the Go layer (everything else passes through):
|
||||
// --version print "rune <version>" (deployed pyproject), exit 0
|
||||
// --python-only print the resolved handoff command, exit 0
|
||||
// --help usage, exit 0
|
||||
// unknown flag usage on stderr, exit 2
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const usage = `rune — Go first-call entry (Phase H1 transitional shim)
|
||||
|
||||
usage:
|
||||
rune [args...] hand off to the Python runtime (deployed revision)
|
||||
rune serve H2 front door: /v1 surface proxying the Python core
|
||||
rune scaffold R2 construction protocol initiation (Go start / Rust core)
|
||||
rune --version print the runtime version
|
||||
rune --python-only print the resolved Python handoff command and exit
|
||||
rune --help this text
|
||||
|
||||
Environment contract (same as the deployed bash launcher):
|
||||
cwd /mnt/workdrive/rune-code/current/src
|
||||
RUNE_HOME data home (stable across code deploys)
|
||||
RUNE_MEMORY_DIR memory dir
|
||||
MIMO_PROBE_ENABLED=1
|
||||
Explicitly-set RUNE_HOME / RUNE_MEMORY_DIR in the ambient environment win.
|
||||
`
|
||||
|
||||
const (
|
||||
deployRoot = "/mnt/workdrive/rune-code"
|
||||
dataHome = "/mnt/workdrive/rune"
|
||||
defaultMemDir = "/home/nick/.config/opencode/memory"
|
||||
)
|
||||
|
||||
// deployedSrc resolves the runtime source dir: the deployed current
|
||||
// revision when it exists (the launcher's authority), else the binary's own
|
||||
// repo checkout (development runs), else the desktop default.
|
||||
func deployedSrc() string {
|
||||
current := filepath.Join(deployRoot, "current", "src")
|
||||
if st, err := os.Stat(filepath.Join(current, "rune", "__main__.py")); err == nil && !st.IsDir() {
|
||||
return current
|
||||
}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
dir := filepath.Dir(exe)
|
||||
for i := 0; i < 8; i++ {
|
||||
cand := filepath.Join(dir, "src")
|
||||
if st, err := os.Stat(filepath.Join(cand, "rune", "__main__.py")); err == nil && !st.IsDir() {
|
||||
return cand
|
||||
}
|
||||
dir = filepath.Dir(dir)
|
||||
}
|
||||
}
|
||||
return filepath.Join(dataHome, "src")
|
||||
}
|
||||
|
||||
func version(srcDir string) string {
|
||||
data, err := os.ReadFile(filepath.Join(filepath.Dir(srcDir), "pyproject.toml"))
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
const prefix = `version = "`
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if rest, ok := strings.CutPrefix(line, prefix); ok {
|
||||
if end := strings.IndexByte(rest, '"'); end > 0 {
|
||||
return rest[:end]
|
||||
}
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func pythonBin() string {
|
||||
cand := filepath.Join(os.Getenv("HOME"), ".venvs", "memory", "bin", "python3")
|
||||
if _, err := os.Stat(cand); err == nil {
|
||||
return cand
|
||||
}
|
||||
return "python3"
|
||||
}
|
||||
|
||||
func upsertEnv(env []string, key, val string) []string {
|
||||
prefix := key + "="
|
||||
for i, e := range env {
|
||||
if strings.HasPrefix(e, prefix) {
|
||||
env[i] = prefix + val
|
||||
return env
|
||||
}
|
||||
}
|
||||
return append(env, prefix+val)
|
||||
}
|
||||
|
||||
func launchEnv(base []string) []string {
|
||||
env := base
|
||||
if os.Getenv("RUNE_HOME") == "" {
|
||||
env = upsertEnv(env, "RUNE_HOME", dataHome)
|
||||
}
|
||||
if os.Getenv("RUNE_MEMORY_DIR") == "" {
|
||||
env = upsertEnv(env, "RUNE_MEMORY_DIR", defaultMemDir)
|
||||
}
|
||||
env = upsertEnv(env, "MIMO_PROBE_ENABLED", "1")
|
||||
return env
|
||||
}
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
// H2: the front-door subcommand owns the /v1 surface (no Python handoff).
|
||||
if len(args) > 0 && args[0] == "serve" {
|
||||
os.Exit(runServe(args[1:]))
|
||||
}
|
||||
// R2 Wave 1: the scaffolding initiation surface (Go starts, Rust executes).
|
||||
if len(args) > 0 && args[0] == "scaffold" {
|
||||
os.Exit(runScaffold(args[1:]))
|
||||
}
|
||||
srcDir := deployedSrc()
|
||||
for _, a := range args {
|
||||
switch a {
|
||||
case "--help", "-h":
|
||||
fmt.Print(usage)
|
||||
return
|
||||
case "--version":
|
||||
fmt.Printf("rune %s\n", version(srcDir))
|
||||
return
|
||||
case "--python-only":
|
||||
fmt.Printf("(cd %s && %s -m rune %s)\n", srcDir, pythonBin(), strings.Join(args[1:], " "))
|
||||
return
|
||||
default:
|
||||
if strings.HasPrefix(a, "-") {
|
||||
fmt.Fprintf(os.Stderr, "rune: unknown flag %q\n\n%s", a, usage)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
argv := append([]string{pythonBin(), "-m", "rune"}, args...)
|
||||
bin, err := exec.LookPath(argv[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rune: cannot find python runtime %q: %v\n", argv[0], err)
|
||||
os.Exit(127)
|
||||
}
|
||||
env := launchEnv(os.Environ())
|
||||
// chdir before exec: the launcher's cwd contract (the deployed src dir
|
||||
// makes the rune package importable for `-m rune`).
|
||||
if err := syscall.Chdir(srcDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rune: cannot enter %s: %v\n", srcDir, err)
|
||||
os.Exit(126)
|
||||
}
|
||||
if err := syscall.Exec(bin, argv, env); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rune: handoff failed: %v\n", err)
|
||||
os.Exit(126)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestVersionReadsPyprojectBesideSrc(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "src", "rune"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "pyproject.toml"),
|
||||
[]byte("[project]\nversion = \"0.99.1\"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := version(filepath.Join(root, "src")); got != "0.99.1" {
|
||||
t.Fatalf("want 0.99.1, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionUnknownWhenMissing(t *testing.T) {
|
||||
if got := version("/nonexistent-src"); got != "unknown" {
|
||||
t.Fatalf("missing root -> unknown, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertEnv(t *testing.T) {
|
||||
env := []string{"A=1", "RUNE_HOME=/old"}
|
||||
got := upsertEnv(env, "RUNE_HOME", "/new")
|
||||
if got[1] != "RUNE_HOME=/new" {
|
||||
t.Fatalf("existing key replaced, got %v", got)
|
||||
}
|
||||
got = upsertEnv([]string{"A=1"}, "B", "2")
|
||||
if got[len(got)-1] != "B=2" || len(got) != 2 {
|
||||
t.Fatalf("new key appended, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchEnvContract(t *testing.T) {
|
||||
t.Setenv("RUNE_HOME", "")
|
||||
t.Setenv("RUNE_MEMORY_DIR", "")
|
||||
env := launchEnv(nil)
|
||||
joined := strings.Join(env, "\n")
|
||||
for _, want := range []string{
|
||||
"RUNE_HOME=/mnt/workdrive/rune",
|
||||
"RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory",
|
||||
"MIMO_PROBE_ENABLED=1",
|
||||
} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Fatalf("launch env missing %q\ngot:\n%s", want, joined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchEnvHonorsExplicitOverrides(t *testing.T) {
|
||||
t.Setenv("RUNE_HOME", "/tmp/custom-home")
|
||||
env := launchEnv([]string{"RUNE_HOME=/tmp/custom-home"})
|
||||
joined := strings.Join(env, "\n")
|
||||
if !strings.Contains(joined, "RUNE_HOME=/tmp/custom-home") {
|
||||
t.Fatalf("explicit RUNE_HOME must win, got:\n%s", joined)
|
||||
}
|
||||
if strings.Contains(joined, "RUNE_HOME=/mnt/workdrive/rune") {
|
||||
t.Fatalf("default must NOT override explicit env, got:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPythonBinFallsBack(t *testing.T) {
|
||||
t.Setenv("HOME", "/nonexistent-home")
|
||||
if got := pythonBin(); got != "python3" {
|
||||
t.Fatalf("missing venv -> python3 on PATH, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployedSrcPrefersLiveCurrent(t *testing.T) {
|
||||
// The deployed current revision exists on this host (the launcher
|
||||
// contract) — the shim must resolve to it, not to a dev checkout.
|
||||
got := deployedSrc()
|
||||
if !strings.HasPrefix(got, "/mnt/workdrive/rune-code/current") &&
|
||||
!strings.Contains(got, "/src") {
|
||||
t.Fatalf("deployed current must win, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ── H2 front door (serve.go) ────────────────────────────────────────────────
|
||||
|
||||
func TestServeHelpExitsZero(t *testing.T) {
|
||||
if rc := runServe([]string{"--help"}); rc != 0 {
|
||||
t.Fatalf("serve --help must exit 0, got %d", rc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeUnknownFlagExitsTwo(t *testing.T) {
|
||||
if rc := runServe([]string{"--bogus"}); rc != 2 {
|
||||
t.Fatalf("unknown serve flag must exit 2, got %d", rc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontDoorNotFoundEnvelope(t *testing.T) {
|
||||
f := &frontDoor{core: "http://127.0.0.1:1"}
|
||||
req := httptest.NewRequest("GET", "/v1/nope", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
f.mux().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("unknown route must 404, got %d", rec.Code)
|
||||
}
|
||||
var body map[string]map[string]string
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("404 body must be the error envelope: %v", err)
|
||||
}
|
||||
if body["error"]["code"] != "not_found" {
|
||||
t.Fatalf("404 code must be not_found, got %q", body["error"]["code"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontDoorCoreDownEnvelope(t *testing.T) {
|
||||
// Nothing listening on port 1 — the proxy must answer 502
|
||||
// core_unavailable, not hang and not 500.
|
||||
f := &frontDoor{core: "http://127.0.0.1:1",
|
||||
client: &http.Client{Timeout: 2 * time.Second}}
|
||||
req := httptest.NewRequest("POST", "/v1/chat/completions",
|
||||
strings.NewReader(`{"messages":[{"role":"user","content":"x"}]}`))
|
||||
rec := httptest.NewRecorder()
|
||||
f.mux().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadGateway {
|
||||
t.Fatalf("core down must 502, got %d", rec.Code)
|
||||
}
|
||||
var body map[string]map[string]string
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("502 body must be the error envelope: %v", err)
|
||||
}
|
||||
if body["error"]["code"] != "core_unavailable" {
|
||||
t.Fatalf("502 code must be core_unavailable, got %q", body["error"]["code"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontDoorProxiesByteForByte(t *testing.T) {
|
||||
// A canned upstream: the front door must pass status + body through
|
||||
// verbatim (the H2 parity law in its strongest form).
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"object":"list","data":[]}`)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
f := &frontDoor{core: upstream.URL, client: upstream.Client()}
|
||||
req := httptest.NewRequest("GET", "/v1/models", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
f.mux().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("proxy must pass status through, got %d", rec.Code)
|
||||
}
|
||||
if rec.Body.String() != `{"object":"list","data":[]}` {
|
||||
t.Fatalf("proxy must pass the body byte-for-byte, got %q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ── R2 Wave 1: scaffold initiation surface ──────────────────────────────────
|
||||
|
||||
func TestScaffoldHelpExitsZero(t *testing.T) {
|
||||
if rc := runScaffold([]string{"--help"}); rc != 0 {
|
||||
t.Fatalf("scaffold --help must exit 0, got %d", rc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaffoldUnknownFlagExitsTwo(t *testing.T) {
|
||||
if rc := runScaffold([]string{"--bogus"}); rc != 2 {
|
||||
t.Fatalf("unknown scaffold flag must exit 2, got %d", rc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaffoldCoreCallEnvelope(t *testing.T) {
|
||||
// The membrane junction with a stub binary emitting a canned envelope.
|
||||
dir := t.TempDir()
|
||||
stub := filepath.Join(dir, "scaffold_core")
|
||||
script := "#!/bin/sh\ncat >/dev/null\nprintf '%s\n' '{\"ok\":true,\"value\":{\"legal\":true}}'\n"
|
||||
if err := os.WriteFile(stub, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env, err := scaffoldCoreCall(stub, "check_transition",
|
||||
map[string]string{"current": "draft", "target": "approved"})
|
||||
if err != nil {
|
||||
t.Fatalf("stub junction failed: %v", err)
|
||||
}
|
||||
if env["ok"] != true {
|
||||
t.Fatalf("envelope ok must be true, got %v", env)
|
||||
}
|
||||
value, _ := env["value"].(map[string]any)
|
||||
if value["legal"] != true {
|
||||
t.Fatalf("value.legal must be true, got %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaffoldCoreCallFailureSurfaces(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
stub := filepath.Join(dir, "scaffold_core")
|
||||
script := "#!/bin/sh\nexit 3\n"
|
||||
if err := os.WriteFile(stub, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := scaffoldCoreCall(stub, "check_transition", map[string]string{}); err == nil {
|
||||
t.Fatal("a failing core binary must surface an error, not a zero value")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// scaffold.go — the scaffolding initiation surface (R2 Wave 1, contract
|
||||
// BUILDER-2026-10-04T15:25:33Z-cli). Go STARTS, Rust EXECUTES: pure
|
||||
// operations (check-transition, close-quality) go straight to the
|
||||
// scaffold_core binary over stdio NDJSON (the hybrid membrane's first
|
||||
// contracted junction); store-backed operations proxy to the Python engine
|
||||
// (whose own core is now Rust-delegated) until Wave 1's later slices port
|
||||
// the contract store.
|
||||
//
|
||||
// Stdlib only. The binary path resolves from SC_RUST_BIN, else the repo
|
||||
// crate target, else PATH.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const scaffoldUsage = `rune scaffold — the construction protocol initiation surface (R2 Wave 1)
|
||||
|
||||
usage:
|
||||
rune scaffold check-transition <current> <target> legality verdict (Rust core, direct)
|
||||
rune scaffold close-quality <inputs.json> closure buckets (Rust core, direct)
|
||||
rune scaffold <op> [args...] store-backed ops proxy to the engine:
|
||||
submit-need, inspect-decision, scope,
|
||||
obligations, observe, verify, transition, close
|
||||
|
||||
Go initiates; Rust executes the deterministic core; the Python engine
|
||||
carries store-backed operations during the transition (strangler law).
|
||||
`
|
||||
|
||||
// scaffoldCoreBin resolves the scaffold_core CLI binary.
|
||||
func scaffoldCoreBin() string {
|
||||
if p := os.Getenv("SC_RUST_BIN"); p != "" {
|
||||
return p
|
||||
}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
dir := filepath.Dir(exe)
|
||||
for i := 0; i < 8; i++ {
|
||||
cand := filepath.Join(dir, "src", "rust", "scaffold_core", "target", "release", "scaffold_core")
|
||||
if st, err := os.Stat(cand); err == nil && !st.IsDir() {
|
||||
return cand
|
||||
}
|
||||
dir = filepath.Dir(dir)
|
||||
}
|
||||
}
|
||||
if p, err := exec.LookPath("scaffold_core"); err == nil {
|
||||
return p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// scaffoldCoreCall runs one pure op through the Rust binary (NDJSON stdin
|
||||
// -> envelope stdout). The envelope contract is the doc-envelope law.
|
||||
func scaffoldCoreCall(bin, op string, arg any) (map[string]any, error) {
|
||||
payload, err := json.Marshal(map[string]any{"op": op, "arg": arg})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.Command(bin)
|
||||
cmd.Stdin = bytes.NewReader(payload)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("scaffold_core %s failed: %w", op, err)
|
||||
}
|
||||
var env map[string]any
|
||||
if err := json.Unmarshal(out.Bytes(), &env); err != nil {
|
||||
return nil, fmt.Errorf("scaffold_core %s returned non-JSON: %w", op, err)
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// scaffoldProxy hands a store-backed op to the Python engine (the
|
||||
// transitional membrane — the engine's core is Rust-backed already).
|
||||
func scaffoldProxy(args []string) int {
|
||||
py := pythonBin()
|
||||
argv := append([]string{py, "-m", "rune.construction_interface"}, args...)
|
||||
cmd := exec.Command(argv[0], argv[1:]...)
|
||||
cmd.Dir = deployedSrc()
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
env := launchEnv(os.Environ())
|
||||
cmd.Env = env
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rune scaffold: engine call failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// runScaffold implements `rune scaffold`. Returns the process exit code.
|
||||
func runScaffold(args []string) int {
|
||||
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
|
||||
fmt.Print(scaffoldUsage)
|
||||
return 0
|
||||
}
|
||||
switch args[0] {
|
||||
case "check-transition":
|
||||
if len(args) != 3 {
|
||||
fmt.Fprintln(os.Stderr, "usage: rune scaffold check-transition <current> <target>")
|
||||
return 2
|
||||
}
|
||||
bin := scaffoldCoreBin()
|
||||
if bin == "" {
|
||||
fmt.Fprintln(os.Stderr, "rune scaffold: scaffold_core binary not found (build the crate)")
|
||||
return 127
|
||||
}
|
||||
env, err := scaffoldCoreCall(bin, "check_transition",
|
||||
map[string]string{"current": args[1], "target": args[2]})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "rune scaffold:", err)
|
||||
return 1
|
||||
}
|
||||
out, _ := json.Marshal(env)
|
||||
fmt.Println(string(out))
|
||||
return 0
|
||||
case "close-quality":
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: rune scaffold close-quality <inputs.json>")
|
||||
return 2
|
||||
}
|
||||
raw, err := os.ReadFile(args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "rune scaffold: cannot read inputs:", err)
|
||||
return 2
|
||||
}
|
||||
var inputs any
|
||||
if err := json.Unmarshal(raw, &inputs); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "rune scaffold: inputs are not JSON:", err)
|
||||
return 2
|
||||
}
|
||||
bin := scaffoldCoreBin()
|
||||
if bin == "" {
|
||||
fmt.Fprintln(os.Stderr, "rune scaffold: scaffold_core binary not found (build the crate)")
|
||||
return 127
|
||||
}
|
||||
env, err := scaffoldCoreCall(bin, "close_quality", inputs)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "rune scaffold:", err)
|
||||
return 1
|
||||
}
|
||||
out, _ := json.Marshal(env)
|
||||
fmt.Println(string(out))
|
||||
return 0
|
||||
default:
|
||||
if strings.HasPrefix(args[0], "-") {
|
||||
fmt.Fprintf(os.Stderr, "rune scaffold: unknown flag %q\n\n%s", args[0], scaffoldUsage)
|
||||
return 2
|
||||
}
|
||||
return scaffoldProxy(args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// serve.go — H2 front door (T4.3, contract BUILDER-2026-10-04T08:32:43Z-cli).
|
||||
//
|
||||
// Go owns the front end (USER-LOCKED directive): this subcommand serves the
|
||||
// OpenAI-compatible /v1 surface on 127.0.0.1 and proxies to the Python core
|
||||
// sidecar (core_server.py) during the transition. Parity law: response
|
||||
// bodies pass through byte-for-byte (the canary compares against the Python
|
||||
// server modulo volatile id/created), and locally-generated errors use the
|
||||
// same envelope the core emits: {"error": {"code", "message"}}.
|
||||
//
|
||||
// Stdlib only (net/http, io) — the plan's "stdlib net/http + NDJSON" line.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const serveUsage = `rune serve — H2 front door (OpenAI-compatible /v1 surface)
|
||||
|
||||
usage:
|
||||
rune serve [--port N] [--core URL]
|
||||
|
||||
flags:
|
||||
--port N listen port on 127.0.0.1 (default 8377)
|
||||
--core URL Python core sidecar base URL (default http://127.0.0.1:8376)
|
||||
|
||||
Endpoints (identical shapes to the core — the parity battery enforces it):
|
||||
GET /v1/models
|
||||
GET /v1/status
|
||||
POST /v1/chat/completions stream=true → NDJSON chunk lines
|
||||
`
|
||||
|
||||
type frontDoor struct {
|
||||
core string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// errorEnvelope mirrors core_server._envelope_error exactly.
|
||||
func errorEnvelope(w http.ResponseWriter, status int, code, message string) {
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"error": map[string]string{"code": code, "message": message},
|
||||
})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
// proxy forwards one request to the core and streams the response back
|
||||
// verbatim — byte-parity with the Python server is the H2 contract, and a
|
||||
// pure pass-through is the strongest form of it.
|
||||
func (f *frontDoor) proxy(w http.ResponseWriter, r *http.Request) {
|
||||
var body io.Reader
|
||||
if r.Body != nil {
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errorEnvelope(w, http.StatusBadRequest, "bad_request", "unreadable body")
|
||||
return
|
||||
}
|
||||
body = bytes.NewReader(raw)
|
||||
}
|
||||
req, err := http.NewRequest(r.Method, f.core+r.URL.Path, body)
|
||||
if err != nil {
|
||||
errorEnvelope(w, http.StatusBadRequest, "bad_request", err.Error())
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
|
||||
resp, err := f.client.Do(req)
|
||||
if err != nil {
|
||||
errorEnvelope(w, http.StatusBadGateway, "core_unavailable",
|
||||
"core sidecar unreachable: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for k, vs := range resp.Header {
|
||||
if strings.EqualFold(k, "Content-Type") {
|
||||
w.Header().Set(k, vs[0])
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
// NDJSON streaming: flush per write so chunk lines reach the caller as
|
||||
// the core emits them (the framing is the contract).
|
||||
flusher, canFlush := w.(http.Flusher)
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, readErr := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
if _, werr := w.Write(buf[:n]); werr != nil {
|
||||
return
|
||||
}
|
||||
if canFlush {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontDoor) mux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
handle := func(path string) {
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != path {
|
||||
errorEnvelope(w, http.StatusNotFound, "not_found",
|
||||
"no route "+r.URL.Path)
|
||||
return
|
||||
}
|
||||
f.proxy(w, r)
|
||||
})
|
||||
}
|
||||
handle("/v1/models")
|
||||
handle("/v1/status")
|
||||
handle("/v1/chat/completions")
|
||||
// H4 slice 1: the TUI endpoints (same pass-through law).
|
||||
handle("/v1/conversation")
|
||||
handle("/v1/approvals/resolve")
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
errorEnvelope(w, http.StatusNotFound, "not_found", "no route "+r.URL.Path)
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
// runServe implements `rune serve`. Returns the process exit code.
|
||||
func runServe(args []string) int {
|
||||
port := 8377
|
||||
core := "http://127.0.0.1:8376"
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--help", "-h":
|
||||
fmt.Print(serveUsage)
|
||||
return 0
|
||||
case "--port":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "rune serve: --port needs a value")
|
||||
return 2
|
||||
}
|
||||
i++
|
||||
if _, err := fmt.Sscanf(args[i], "%d", &port); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rune serve: bad --port %q\n", args[i])
|
||||
return 2
|
||||
}
|
||||
case "--core":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "rune serve: --core needs a value")
|
||||
return 2
|
||||
}
|
||||
i++
|
||||
core = strings.TrimSuffix(args[i], "/")
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "rune serve: unknown flag %q\n\n%s", args[i], serveUsage)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
f := &frontDoor{
|
||||
core: core,
|
||||
client: &http.Client{Timeout: 130 * time.Second},
|
||||
}
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
fmt.Printf("rune front door on %s (core %s)\n", addr, core)
|
||||
if err := http.ListenAndServe(addr, f.mux()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "rune serve: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.86.0"
|
||||
__version__ = "0.99.1"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
+62
-2
@@ -43,6 +43,42 @@ LOCAL_MODEL = "qwen2.5:3b"
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
|
||||
|
||||
_ABSTRACTION_STOPWORDS_FROZEN = frozenset(
|
||||
"the a an is are was were be been being have has had do does did will "
|
||||
"would could should may might shall can to of in for on with at by from "
|
||||
"as into through during before after this that these those it its what "
|
||||
"which who how when where why and or but not no".split()
|
||||
)
|
||||
|
||||
|
||||
def _ABSTRACTION_STOPWORDS() -> list:
|
||||
"""Gather seam (W3.4): the fact-extraction stopword table as a sorted
|
||||
list — data received by the kernel; identical filter on both engines."""
|
||||
return sorted(_ABSTRACTION_STOPWORDS_FROZEN)
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust cognition_core when available (strangler: Python fallback
|
||||
load-bearing). RUNE_COGNITION_CORE=python pins the fallback."""
|
||||
return None if cc_loader_pin() else _cc_probe()
|
||||
|
||||
|
||||
def _cc_probe():
|
||||
from rune import cc_loader as _cl
|
||||
try:
|
||||
return _cl if _cl.available() else None
|
||||
except Exception: # noqa: BLE001, S110 — loader probe never crashes the fallback
|
||||
return None
|
||||
|
||||
|
||||
def cc_loader_pin() -> bool:
|
||||
"""The env pin: RUNE_COGNITION_CORE=python → kernels OFF (shared probe).
|
||||
"""
|
||||
import os
|
||||
|
||||
return os.environ.get("RUNE_COGNITION_CORE", "auto") == "python"
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
@@ -241,17 +277,41 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
response = llm_call(prompt, max_tokens=800)
|
||||
|
||||
# Parse response
|
||||
facts: list = []
|
||||
try:
|
||||
# Try to extract JSON from response
|
||||
start = response.find("[")
|
||||
end = response.rfind("]") + 1
|
||||
if start >= 0 and end > start:
|
||||
facts = json.loads(response[start:end])
|
||||
else:
|
||||
facts = []
|
||||
except json.JSONDecodeError:
|
||||
facts = []
|
||||
|
||||
# LLM unavailable / unparseable → DETERMINISTIC KERNEL FALLBACK
|
||||
# (W3.4: cc_abstraction_extract_facts — sentence split, stopwords
|
||||
# filtered, min-length gate; the same facts schema it already emits:
|
||||
# text/words/content_words mapped onto what= text with empty dims).
|
||||
if not facts:
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
raw = _cl.abstraction_extract_facts(
|
||||
episode_text, _ABSTRACTION_STOPWORDS(), 10)
|
||||
for item in raw or []:
|
||||
text_frag = str(item.get("text", "")).strip()
|
||||
if not text_frag:
|
||||
continue
|
||||
facts.append({
|
||||
"what": text_frag, "when": "", "where": "", "who": "",
|
||||
"why": "", "type": "experience", "entities": [],
|
||||
"kernel": True,
|
||||
"content_words": item.get("content_words"),
|
||||
})
|
||||
except Exception: # noqa: BLE001, S110 — kernel failure degrades to empty, intentional silence
|
||||
pass
|
||||
|
||||
if not facts:
|
||||
return {"extracted": 0, "error": "LLM did not return valid JSON", "raw": response[:200]}
|
||||
|
||||
|
||||
+29
-33
@@ -15,6 +15,17 @@ Usage:
|
||||
behavior.py reinforce <id> [--outcome success|failure]
|
||||
behavior.py list [--type correction|habit|all]
|
||||
behavior.py stats
|
||||
|
||||
Phase G strangler slice 2 (port 5): the decision math (reinforce
|
||||
counting/confidence/promotion/state formulas) is served IN-PROCESS by the
|
||||
Rust `evidence_provenance` cdylib through ep_loader.py. STORAGE STAYS
|
||||
PYTHON — SQLite I/O and the regex matching layer remain here (the crate is
|
||||
zero-dep by design; hand-rolling SQLite/regex would be hubris, not
|
||||
hardening). Vintage upgrade per the frozen contract §2: this module is the
|
||||
src/rune authority vintage; the deployed ~/.config/opencode/scripts copy
|
||||
is superseded by the same shim shape pointing at the live db path.
|
||||
Timestamps are pinned by the shim (`now`) — the core is deterministic.
|
||||
API, CLI, and PLUGIN shapes unchanged (git history holds the original).
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -24,6 +35,12 @@ import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# ── Rust core wiring (slice-2 port-5 shim pattern) ──────────────────────────
|
||||
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
|
||||
if _CRATE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CRATE_DIR)
|
||||
import ep_loader # noqa: E402 (loads target/release cdylib)
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import memory_env
|
||||
|
||||
@@ -194,7 +211,10 @@ def match_context(context, match_type=None):
|
||||
|
||||
|
||||
def reinforce(item_id, outcome, item_type="habit"):
|
||||
"""Reinforce a correction or habit based on outcome."""
|
||||
"""Reinforce a correction or habit based on outcome.
|
||||
|
||||
Decision math (counting, confidence formula, promotion, state) served
|
||||
by the Rust core; SQL stays here."""
|
||||
db = connect()
|
||||
try:
|
||||
ts = _now()
|
||||
@@ -204,28 +224,14 @@ def reinforce(item_id, outcome, item_type="habit"):
|
||||
if not habit:
|
||||
return {"error": f"Habit {item_id} not found"}
|
||||
|
||||
new_count = habit["activation_count"] + 1
|
||||
new_success = habit["success_count"] + (1 if outcome == "success" else 0)
|
||||
new_failure = habit["failure_count"] + (1 if outcome == "failure" else 0)
|
||||
|
||||
# Update confidence based on success rate
|
||||
if new_count > 0:
|
||||
success_rate = new_success / new_count
|
||||
new_confidence = min(1.0, 0.5 + success_rate * 0.5)
|
||||
else:
|
||||
new_confidence = habit["confidence"]
|
||||
|
||||
# Promote from candidate to active if enough activations
|
||||
new_state = habit["state"]
|
||||
if habit["state"] == "candidate" and new_count >= habit["min_activations"]:
|
||||
new_state = "active"
|
||||
|
||||
upd = ep_loader.habit_reinforce(dict(habit), outcome)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE habits SET activation_count=?, success_count=?, failure_count=?,
|
||||
confidence=?, state=?, last_activated=? WHERE id=?
|
||||
""",
|
||||
(new_count, new_success, new_failure, new_confidence, new_state, ts, item_id),
|
||||
(upd["activation_count"], upd["success_count"], upd["failure_count"],
|
||||
upd["confidence"], upd["state"], ts, item_id),
|
||||
)
|
||||
|
||||
# Log activation
|
||||
@@ -238,33 +244,23 @@ def reinforce(item_id, outcome, item_type="habit"):
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
|
||||
return {"reinforced": item_id, "confidence": upd["confidence"], "state": upd["state"]}
|
||||
|
||||
else: # correction
|
||||
correction = db.execute("SELECT * FROM corrections WHERE id=?", (item_id,)).fetchone()
|
||||
if not correction:
|
||||
return {"error": f"Correction {item_id} not found"}
|
||||
|
||||
if outcome == "success":
|
||||
new_confidence = min(1.0, correction["confidence"] + 0.1)
|
||||
new_count = correction["evidence_count"] + 1
|
||||
elif outcome == "failure":
|
||||
new_confidence = max(0.0, correction["confidence"] - 0.3)
|
||||
new_count = correction["evidence_count"]
|
||||
else:
|
||||
new_confidence = correction["confidence"]
|
||||
new_count = correction["evidence_count"]
|
||||
|
||||
new_state = "active" if new_confidence > 0.3 else "archived"
|
||||
upd = ep_loader.correction_reinforce(dict(correction), outcome)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE corrections SET confidence=?, evidence_count=?, state=?, last_reinforced=?
|
||||
WHERE id=?
|
||||
""",
|
||||
(new_confidence, new_count, new_state, ts, item_id),
|
||||
(upd["confidence"], upd["evidence_count"], upd["state"], ts, item_id),
|
||||
)
|
||||
db.commit()
|
||||
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
|
||||
return {"reinforced": item_id, "confidence": upd["confidence"], "state": upd["state"]}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -408,4 +404,4 @@ PLUGIN = {
|
||||
"provides": ["behavior"],
|
||||
"priority": 50,
|
||||
"auto_activate": True,
|
||||
}
|
||||
}
|
||||
+48
-126
@@ -16,12 +16,29 @@ dependencies changed are invalidated; every invalidation carries a
|
||||
reason; unrelated hardened confirmations are preserved.
|
||||
|
||||
Storage: <project_store>/behaviour_ledger.json (versioned document).
|
||||
|
||||
Phase G strangler slice 2 (port 4): the state machine (define / get /
|
||||
transitions / selective invalidation / queues / summary) is served
|
||||
IN-PROCESS by the Rust `evidence_provenance` cdylib through ep_loader.py
|
||||
— parity 13/13 proven vs this module's Python vintage (full-scenario
|
||||
doc parity, byte-equal error messages, save render indent=1 sort_keys).
|
||||
File glue (load/save) stays Python; timestamps are pinned by this shim
|
||||
(`now`) so the core stays deterministic. State constants and class API
|
||||
unchanged (git history holds the original).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# ── Rust core wiring (slice-2 port-4 shim pattern) ──────────────────────────
|
||||
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
|
||||
if _CRATE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CRATE_DIR)
|
||||
import ep_loader # noqa: E402 (loads target/release cdylib)
|
||||
|
||||
LEDGER_SCHEMA_VERSION = 1
|
||||
|
||||
# hardening lifecycle (rule 19G semantics)
|
||||
@@ -56,13 +73,11 @@ class BehaviourLedger:
|
||||
|
||||
def _load(self) -> dict:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
return {"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"behaviours": {}, "updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.doc["updated_at"] = _now()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
|
||||
@@ -77,66 +92,34 @@ class BehaviourLedger:
|
||||
dependencies: list[str] | None = None,
|
||||
automated_ref: str = "",
|
||||
environment: str = "") -> dict:
|
||||
bid = (behaviour_id if behaviour_id.startswith("behaviour:")
|
||||
else f"behaviour:{behaviour_id}")
|
||||
if bid in self.doc["behaviours"]:
|
||||
raise ValueError(f"behaviour exists: {bid}")
|
||||
rec = {
|
||||
"behaviour_id": bid,
|
||||
"owning_project": owning_project,
|
||||
"intended_behaviour": intended_behaviour,
|
||||
"origin_intent": origin_intent,
|
||||
"pass_criteria": pass_criteria,
|
||||
"fail_criteria": fail_criteria,
|
||||
"procedure": procedure,
|
||||
"owner_component": owner_component, # structural entity id
|
||||
"dependencies": list(dependencies or []), # structural ids
|
||||
"automated_ref": automated_ref,
|
||||
"environment": environment,
|
||||
"state": UNVALIDATED,
|
||||
"evidence": [], # [{at, generation, kind, detail}]
|
||||
"history": [], # every state transition, never lost
|
||||
"invalidations": [], # [{at, generation, because}]
|
||||
"limitations": [],
|
||||
"created_at": _now(),
|
||||
"updated_at": _now(),
|
||||
}
|
||||
self.doc["behaviours"][bid] = rec
|
||||
self.doc, rec = ep_loader.bl_define(
|
||||
self.doc, behaviour_id=behaviour_id,
|
||||
owning_project=owning_project,
|
||||
intended_behaviour=intended_behaviour,
|
||||
origin_intent=origin_intent,
|
||||
pass_criteria=pass_criteria,
|
||||
fail_criteria=fail_criteria,
|
||||
procedure=procedure,
|
||||
owner_component=owner_component,
|
||||
dependencies=dependencies,
|
||||
automated_ref=automated_ref,
|
||||
environment=environment,
|
||||
now=_now())
|
||||
return rec
|
||||
|
||||
def get(self, behaviour_id: str) -> dict | None:
|
||||
return self.doc["behaviours"].get(
|
||||
behaviour_id if behaviour_id.startswith("behaviour:")
|
||||
else f"behaviour:{behaviour_id}")
|
||||
return ep_loader.bl_get(self.doc, behaviour_id)
|
||||
|
||||
# -- state transitions (all recorded in history — rule 19G) -------------
|
||||
|
||||
def _transition(self, rec: dict, new_state: str, *, kind: str,
|
||||
detail: str = "", generation: str = "") -> None:
|
||||
if new_state not in STATES:
|
||||
raise ValueError(f"unknown validation state {new_state!r}")
|
||||
rec["history"].append({
|
||||
"at": _now(), "from": rec["state"], "to": new_state,
|
||||
"kind": kind, "detail": detail[:200],
|
||||
"generation": generation})
|
||||
rec["state"] = new_state
|
||||
rec["updated_at"] = _now()
|
||||
|
||||
def record_auto_verification(self, behaviour_id: str, *,
|
||||
passed: bool, detail: str = "",
|
||||
generation: str = "") -> dict:
|
||||
"""Automated verification. GREEN NEVER IMPLIES USER_CONFIRMED
|
||||
(rule 19W-22): the ceiling here is AUTO_VERIFIED."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["evidence"].append({"at": _now(), "kind": "automated",
|
||||
"passed": passed,
|
||||
"detail": detail[:200],
|
||||
"generation": generation})
|
||||
self._transition(rec, AUTO_VERIFIED if passed else TEST_FAILED,
|
||||
kind="automated_verification", detail=detail,
|
||||
generation=generation)
|
||||
self.doc, rec = ep_loader.bl_record_auto(
|
||||
self.doc, behaviour_id, passed=passed, detail=detail,
|
||||
generation=generation, now=_now())
|
||||
return rec
|
||||
|
||||
def record_real_world(self, behaviour_id: str, *, passed: bool,
|
||||
@@ -144,32 +127,16 @@ class BehaviourLedger:
|
||||
confirmed_by: str = "user") -> dict:
|
||||
"""Real-world/manual test result (testing mode). USER_CONFIRMED
|
||||
requires an explicit confirmed_by='user' pass (rule 19M)."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["evidence"].append({"at": _now(), "kind": "real_world",
|
||||
"passed": passed,
|
||||
"detail": detail[:200],
|
||||
"generation": generation,
|
||||
"by": confirmed_by})
|
||||
if passed:
|
||||
state = (USER_CONFIRMED if confirmed_by == "user"
|
||||
else REAL_WORLD_PASSED)
|
||||
else:
|
||||
state = TEST_FAILED
|
||||
self._transition(rec, state, kind="real_world", detail=detail,
|
||||
generation=generation)
|
||||
self.doc, rec = ep_loader.bl_record_real_world(
|
||||
self.doc, behaviour_id, passed=passed, detail=detail,
|
||||
generation=generation, confirmed_by=confirmed_by, now=_now())
|
||||
return rec
|
||||
|
||||
def accept_limitation(self, behaviour_id: str, *, note: str,
|
||||
approved_by: str = "user") -> dict:
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["limitations"].append({"at": _now(), "note": note[:200],
|
||||
"approved_by": approved_by})
|
||||
self._transition(rec, ACCEPTED_LIMITATION, kind="accept_limitation",
|
||||
detail=note, )
|
||||
self.doc, rec = ep_loader.bl_accept_limitation(
|
||||
self.doc, behaviour_id, note=note,
|
||||
approved_by=approved_by, now=_now())
|
||||
return rec
|
||||
|
||||
# -- selective invalidation (rule 19I) -----------------------------------
|
||||
@@ -182,44 +149,15 @@ class BehaviourLedger:
|
||||
structural dependencies intersect the changed closure. Every
|
||||
invalidation carries a reason; unrelated confirmations stay.
|
||||
Returns the invalidated records."""
|
||||
reasons = reasons or {}
|
||||
invalidated = []
|
||||
for rec in self.doc["behaviours"].values():
|
||||
if rec["state"] not in CONFIRMED_STATES:
|
||||
continue
|
||||
overlap = set(rec.get("dependencies") or []) & changed_entities
|
||||
if rec.get("owner_component") in changed_entities:
|
||||
overlap.add(rec["owner_component"])
|
||||
if not overlap:
|
||||
continue # unrelated hardened behaviour: PRESERVED (19S-5)
|
||||
because = reasons.get(
|
||||
rec["behaviour_id"],
|
||||
f"structural change touched: {sorted(overlap)}")
|
||||
rec["invalidations"].append({
|
||||
"at": _now(), "generation": generation,
|
||||
"because": because[:200],
|
||||
"was": rec["state"],
|
||||
"changed_entities": sorted(overlap)})
|
||||
# HISTORY IS PRESERVED — prior confirmation stays queryable
|
||||
self._transition(rec, RETEST_REQUIRED,
|
||||
kind="selective_invalidation",
|
||||
detail=f"was {rec['state']}; {because}",
|
||||
generation=generation)
|
||||
invalidated.append(rec)
|
||||
return invalidated
|
||||
self.doc, recs = ep_loader.bl_invalidate(
|
||||
self.doc, changed_entities=sorted(changed_entities),
|
||||
generation=generation, reasons=reasons, now=_now())
|
||||
return recs
|
||||
|
||||
def retest_history(self, behaviour_id: str) -> list[dict]:
|
||||
"""Generation-aware history: all confirmations + invalidations
|
||||
for one behaviour, oldest first (rule 19S-12)."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
return []
|
||||
events = []
|
||||
for h in rec["history"]:
|
||||
events.append({"at": h["at"], "generation": h.get("generation"),
|
||||
"event": f"{h['from']}→{h['to']}",
|
||||
"kind": h["kind"], "detail": h.get("detail", "")})
|
||||
return events
|
||||
return ep_loader.bl_retest_history(self.doc, behaviour_id)
|
||||
|
||||
# -- queue (testing mode, 19L/M) ------------------------------------------
|
||||
|
||||
@@ -228,24 +166,8 @@ class BehaviourLedger:
|
||||
"""Eligible testing items: anything not confirmed/accepted/n.a.
|
||||
Dynamic — the queue re-derives from states after every fix
|
||||
(rule 19M)."""
|
||||
out = []
|
||||
for rec in self.doc["behaviours"].values():
|
||||
if rec["state"] in (USER_CONFIRMED, ACCEPTED_LIMITATION,
|
||||
NOT_APPLICABLE):
|
||||
continue
|
||||
if project and rec.get("owning_project") != project:
|
||||
continue
|
||||
if component and rec.get("owner_component") != component:
|
||||
continue
|
||||
out.append(rec)
|
||||
out.sort(key=lambda r: (0 if r["state"] == TEST_FAILED else 1,
|
||||
r["behaviour_id"]))
|
||||
return out
|
||||
return ep_loader.bl_outstanding(
|
||||
self.doc, component=component or "", project=project or "")
|
||||
|
||||
def summary(self) -> dict:
|
||||
counts: dict[str, int] = {}
|
||||
for rec in self.doc["behaviours"].values():
|
||||
counts[rec["state"]] = counts.get(rec["state"], 0) + 1
|
||||
return {"total": len(self.doc["behaviours"]),
|
||||
"states": counts,
|
||||
"outstanding": len(self.outstanding())}
|
||||
return ep_loader.bl_summary(self.doc)
|
||||
@@ -64,6 +64,7 @@ class BuilderCapsule:
|
||||
memory_lines: list = field(default_factory=list)
|
||||
lesson_lines: list = field(default_factory=list)
|
||||
constraints: list = field(default_factory=list)
|
||||
construction_lines: list = field(default_factory=list)
|
||||
generated_at: str = ""
|
||||
core_version: str = "rune-core/operator-path"
|
||||
|
||||
@@ -75,6 +76,7 @@ class BuilderCapsule:
|
||||
"memory_lines": self.memory_lines[:MAX_MEMORY_LINES],
|
||||
"lesson_lines": self.lesson_lines[:MAX_LESSON_LINES],
|
||||
"constraints": self.constraints,
|
||||
"construction_lines": self.construction_lines,
|
||||
"generated_at": self.generated_at,
|
||||
"core_version": self.core_version,
|
||||
}
|
||||
@@ -87,10 +89,14 @@ class BuilderCapsule:
|
||||
and not d["lesson_lines"]
|
||||
and not d["unknowns"]
|
||||
and not d["constraints"]
|
||||
and not d["construction_lines"]
|
||||
and d["task_route"] in ("LOCAL_FIX", "STANDARD_CHANGE")
|
||||
):
|
||||
return "" # nothing useful — inject nothing (handoff §5)
|
||||
lines = [f"### Rune Core preflight ({d['task_route']})"]
|
||||
if d["construction_lines"]:
|
||||
lines.append("**Active Scaffolding construction (read-only projection):**")
|
||||
lines += [f"- {c}" for c in d["construction_lines"]]
|
||||
if d["memory_lines"]:
|
||||
lines.append("**Relevant Rune memory:**")
|
||||
lines += [f"- {m}" for m in d["memory_lines"]]
|
||||
@@ -107,6 +113,56 @@ class BuilderCapsule:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _construction_store() -> str:
|
||||
"""The SAME construction store the operator path uses (env override
|
||||
honoured; default derives from the repo root, never a bare relative
|
||||
path). Kept identical to rune_operator._construction_store."""
|
||||
import os as _os
|
||||
env = _os.environ.get("RUNE_CONSTRUCTION_STORE")
|
||||
if env:
|
||||
return env
|
||||
from pathlib import Path as _P
|
||||
return str(_P(__file__).resolve().parent.parent.parent / "store" / "scaffold")
|
||||
|
||||
|
||||
def _active_construction_lines() -> list:
|
||||
"""Read-only projection of the ACTIVE Scaffolding construction into
|
||||
builder-consumable lines. This is the MiMo edge of the shared
|
||||
Construction Context (§15) — it surfaces canonical state; it is NOT a
|
||||
second construction authority and never mutates. Degrade-safe: any
|
||||
failure returns [] (the bridge must never block the builder)."""
|
||||
try:
|
||||
from .construction_interface import ConstructionInterface
|
||||
env = ConstructionInterface(builder="mimo-bridge",
|
||||
project_store=_construction_store(),
|
||||
project_id="issue161-harness"
|
||||
).construction_context()
|
||||
st = env.get("state") or {}
|
||||
if not st.get("active"):
|
||||
return []
|
||||
out = [f"contract {st.get('contract_id')} · phase {st.get('phase')}"
|
||||
f" · status {st.get('status')}"
|
||||
f" · mode {st.get('construction_mode')}"]
|
||||
if st.get("project"):
|
||||
out.append(f"project: {st['project']}")
|
||||
if st.get("need"):
|
||||
out.append(f"need: {st['need']}")
|
||||
scope = st.get("scope") or []
|
||||
if scope:
|
||||
out.append("scope: " + ", ".join(str(x) for x in scope[:8]))
|
||||
obl = st.get("obligations") or []
|
||||
if obl:
|
||||
out.append(f"outstanding obligations: {len(obl)}")
|
||||
for o in obl[:3]:
|
||||
txt = o.get("obligation") or o.get("kind") or o
|
||||
out.append(f" - {str(txt)[:120]}")
|
||||
if st.get("next_action"):
|
||||
out.append(f"next action: {st['next_action']}")
|
||||
return out
|
||||
except Exception: # noqa: BLE001 — projection only; never block the builder
|
||||
return []
|
||||
|
||||
|
||||
def _default_selector():
|
||||
"""The SAME MemorySelector the operator path uses (lazy, degrade-safe)."""
|
||||
try:
|
||||
@@ -166,6 +222,11 @@ def preflight(task_text: str, memory_selector=None) -> BuilderCapsule:
|
||||
# design (handoff §9: raw SEED records are outcome telemetry, not lessons).
|
||||
capsule.lesson_lines = _validated_lessons_for(task_text or "")
|
||||
|
||||
# Active Scaffolding construction state (read-only projection of the
|
||||
# shared Construction Context). Empty when no contract is active, so
|
||||
# the inject-nothing policy is preserved for ordinary turns.
|
||||
capsule.construction_lines = _active_construction_lines()
|
||||
|
||||
return capsule
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_audit_ledger.py — Python canary for slice-2 ports 1-6.
|
||||
|
||||
Slice-1 PASS/FAIL discipline (counter harness, exit 1 on any FAIL), pinned to
|
||||
the FROZEN CONTRACT /mnt/workdrive/rune/plans/2026-10-03-phase-g-evidence-provenance-contract.md:
|
||||
§5 requires the canary pair to carry per-port sections for ALL SIX ports.
|
||||
|
||||
DUAL-MODE LAW (added 2026-10-03 after run 18141 went red): sections 2/3/4/5
|
||||
pin the LIVE promotion ledger when it exists in this process's HOME (desktop
|
||||
runs — the drift-key pin is the §3 linchpin). Under CI's sandboxed HOME the
|
||||
ledger file does not exist, so the same sections run an ISOLATED battery
|
||||
against the sandboxed ledger with count expectations re-derived from the
|
||||
seeded entries — identical test count, identical shapes, honest MODE print.
|
||||
The artifact coupling is disclosed, not hidden: the Rust binary + cdylib
|
||||
resolve to the checkout's own target/release/ (CI's host-executor topology
|
||||
makes the desktop build visible; if they ever go missing, ep_loader raises
|
||||
and this canary FAILS LOUDLY rather than testing nothing).
|
||||
|
||||
Canary expectations are OBSERVED, not invented:
|
||||
- Deployed shim head + verify_chain() on the live 23-entry ledger:
|
||||
{'valid': True, 'entries': 23, 'reason': 'chain intact'} (observed 2026-10-03).
|
||||
- verify_chain() return shape is a 3-key dict, no tuple unpacking.
|
||||
- Deployed SECRET_KEY default: 'rune-drift-integrity-2024' (the live-chain key).
|
||||
- Empty-ledger contract: missing ledger file -> valid-and-empty dict.
|
||||
- Shadow battery (18/18 GREEN, banked): read/write/CLI-error shapes all match
|
||||
the old vintage at the shim.
|
||||
|
||||
Run under ~/.venvs/memory/bin/python3.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/home/nick/.config/opencode/scripts")
|
||||
|
||||
# Repo-derived roots (no /mnt/workdrive hardcodes): this file lives in
|
||||
# src/rune/, so the crate is two parents up plus src/rust/evidence_provenance.
|
||||
_REPO = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
_CRATE_DIR = os.path.join(_REPO, "src", "rust", "evidence_provenance")
|
||||
if os.environ.get("EP_RUST_LIB"):
|
||||
# CI: the build step emits to a runner-owned /tmp target dir and the job
|
||||
# points EP_RUST_LIB at the cdylib FILE (that's the form ep_loader's
|
||||
# override takes). The binary lives next to it.
|
||||
_BIN = os.path.join(os.path.dirname(os.environ["EP_RUST_LIB"]),
|
||||
"evidence_provenance")
|
||||
else:
|
||||
_BIN = os.path.join(_CRATE_DIR, "target", "release", "evidence_provenance")
|
||||
|
||||
# Dual-mode switch (DUAL-MODE LAW in the docstring): LIVE when this process's
|
||||
# HOME carries the real promotion ledger AND we are not inside the CI sandbox.
|
||||
# The discriminator is the repo's own CI-sandbox convention — the workflow sets
|
||||
# RUNE_HOME=/tmp/rune-ci-<run_id> and its cleanup guard matches /tmp/rune-ci-*
|
||||
# exactly. File EXISTENCE is NOT a valid discriminator: run 18141's shim writes
|
||||
# left a zombie 1-entry ledger in the runner's persistent HOME, which would
|
||||
# flip a naive isfile() gate to LIVE with the wrong data (proven 2026-10-03
|
||||
# on run 18161: "entries >= 23 — got 1").
|
||||
_LIVE_LEDGER = os.path.expanduser("~/.config/opencode/memory/promotion_ledger.jsonl")
|
||||
_IN_CI_SANDBOX = os.environ.get("RUNE_HOME", "").startswith("/tmp/rune-ci-")
|
||||
LIVE = os.path.isfile(_LIVE_LEDGER) and not _IN_CI_SANDBOX
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
|
||||
def test(name, condition, detail=""):
|
||||
global PASS, FAIL
|
||||
if condition:
|
||||
PASS += 1
|
||||
print(f" ✓ {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ✗ {name} — {detail}")
|
||||
|
||||
|
||||
def main():
|
||||
global PASS, FAIL
|
||||
|
||||
print("=== AUDIT LEDGER CANARY (slice-2 ports 1-6) ===")
|
||||
print(f"MODE: {'LIVE (deployed surface + live ledger)' if LIVE else 'ISOLATED (sandboxed ledger — CI mode)'}\n")
|
||||
|
||||
bin_home = None # binary HOME override for §5 in ISOLATED mode
|
||||
|
||||
# --- 1. Deployed module surface ---
|
||||
print("--- 1. Deployed module surface ---")
|
||||
try:
|
||||
import audit_ledger
|
||||
|
||||
test("audit_ledger imports (deployed shim)", True)
|
||||
for name in (
|
||||
"_compute_hash",
|
||||
"append_entry",
|
||||
"verify_chain",
|
||||
"get_entries",
|
||||
"log_promotion",
|
||||
"log_gate_result",
|
||||
"log_drift_alert",
|
||||
"main",
|
||||
):
|
||||
test(f"has {name}", hasattr(audit_ledger, name))
|
||||
test("LEDGER_FILE is the live ledger path",
|
||||
audit_ledger.LEDGER_FILE ==
|
||||
os.path.expanduser("~/.config/opencode/memory/promotion_ledger.jsonl"),
|
||||
f"got {audit_ledger.LEDGER_FILE}")
|
||||
test("SECRET_KEY default is the drift key (live-chain key)",
|
||||
audit_ledger.SECRET_KEY == "rune-drift-integrity-2024",
|
||||
f"got {audit_ledger.SECRET_KEY}")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("audit_ledger module", False, str(e))
|
||||
print(f"CANARY ABORT: import failed — {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- 2. Ledger verification through the shim (live on desktop / isolated in CI) ---
|
||||
print("\n--- 2. Ledger verification ---")
|
||||
if not LIVE:
|
||||
# Seed a PER-RUN isolated ledger (never the runner's persistent HOME —
|
||||
# leftover writes there are what poisoned run 18161's mode detection):
|
||||
# point the shim's module-level LEDGER_FILE at a temp path; all §2/§3/§4
|
||||
# reads/writes then land in the temp file. Binary §5 gets HOME=<temp>.
|
||||
# Placed AFTER §1 so the surface pins observe the shim's DEFAULT
|
||||
# resolution before the override.
|
||||
import tempfile as _tf
|
||||
|
||||
bin_home = _tf.mkdtemp(prefix="alcanary_home_")
|
||||
iso_ledger = os.path.join(bin_home, ".config", "opencode", "memory",
|
||||
"promotion_ledger.jsonl")
|
||||
os.makedirs(os.path.dirname(iso_ledger), exist_ok=True)
|
||||
audit_ledger.LEDGER_FILE = iso_ledger
|
||||
for seed_id in (9001, 9002):
|
||||
audit_ledger.append_entry("promotion", seed_id,
|
||||
{"reason": "canary-ci-seed", "old_status": "a",
|
||||
"new_status": "b", "wash_count": 1})
|
||||
try:
|
||||
result = audit_ledger.verify_chain()
|
||||
test("verify_chain() returns dict", isinstance(result, dict), f"got {type(result)}") # type: ignore[reportUnnecessaryIsInstance]
|
||||
test("valid is True on the chain", result.get("valid") is True,
|
||||
f"got {result.get('valid')}")
|
||||
if LIVE:
|
||||
test("entries >= 23 (ledger grew through prior canaries)",
|
||||
result.get("entries", 0) >= 23, f"got {result.get('entries')}")
|
||||
else:
|
||||
test("entries == 2 (the §2 seeds this process wrote)",
|
||||
result.get("entries") == 2, f"got {result.get('entries')}")
|
||||
test("reason is 'chain intact'", result.get("reason") == "chain intact",
|
||||
f"got {result.get('reason')}")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("live verify_chain()", False, str(e))
|
||||
|
||||
# --- 3. Read path: get_entries ---
|
||||
print("\n--- 3. Read path ---")
|
||||
try:
|
||||
entries = audit_ledger.get_entries(limit=5)
|
||||
test("get_entries(5) returns a list", isinstance(entries, list)) # type: ignore[reportUnnecessaryIsInstance]
|
||||
if LIVE:
|
||||
test("get_entries(5) returns 5 entries on the live ledger", len(entries) == 5,
|
||||
f"got {len(entries)}")
|
||||
else:
|
||||
test("get_entries(5) returns the 2 seeded entries (isolated)", len(entries) == 2,
|
||||
f"got {len(entries)}")
|
||||
first = entries[0] if entries else {}
|
||||
test("entry carries hash chain fields",
|
||||
"hash" in first and "prev_hash" in first,
|
||||
f"keys: {sorted(first.keys())}")
|
||||
# Observed deployed-vintage serving order: ASCENDING (oldest-first
|
||||
# within the limit window) — verified against the live ledger 2026-10-03.
|
||||
test("entries served in ascending timestamp order (deployed-vintage shape)",
|
||||
all(entries[i]["timestamp"] <= entries[i + 1]["timestamp"]
|
||||
for i in range(len(entries) - 1)),
|
||||
"timestamps not monotonic-ascending")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("get_entries", False, str(e))
|
||||
|
||||
# --- 4. Write path: append through the deployed shim (LIVE ledger,
|
||||
# the established canary pattern — entries 89/90 are this shape) ---
|
||||
print("\n--- 4. Write path (canary append through the shim) ---")
|
||||
try:
|
||||
e1 = audit_ledger.append_entry("promotion", 4242, {"old_status": "established",
|
||||
"new_status": "rock",
|
||||
"wash_count": 20,
|
||||
"reason": "canary"})
|
||||
test("append_entry returns dict", isinstance(e1, dict), f"got {type(e1)}") # type: ignore[reportUnnecessaryIsInstance]
|
||||
test("entry has prev_hash + hash", "prev_hash" in e1 and "hash" in e1)
|
||||
test("hash is 64-hex", isinstance(e1.get("hash"), str) and len(e1["hash"]) == 64
|
||||
and all(c in "0123456789abcdef" for c in e1["hash"]))
|
||||
test("append flowed through the shim: live chain still intact",
|
||||
audit_ledger.verify_chain()["valid"] is True,
|
||||
"live chain invalid after append")
|
||||
tail = audit_ledger.get_entries(limit=1)
|
||||
test("newest live entry is the canary append (entry_id 4242)",
|
||||
tail and tail[0].get("entry_id") == 4242,
|
||||
f"got {tail[0].get('entry_id') if tail else 'empty'}")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("append_entry write path", False, str(e))
|
||||
|
||||
# --- 5. Rust binary CLI parity (the compiled candidate) ---
|
||||
print("\n--- 5. Rust binary CLI parity ---")
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
BIN = _BIN
|
||||
tmp2 = tempfile.mkdtemp(prefix="alcanary_cli_")
|
||||
# The binary honors RUNE_MEMORY_DIR/RUNE_HOME first (main.rs ledger_path);
|
||||
# the deployed shim resolves expanduser($HOME). Strip the RUNE_* overrides
|
||||
# from binary envs so both surfaces resolve the SAME ledger — without this,
|
||||
# CI's RUNE_MEMORY_DIR silently pointed the binary at an empty ledger while
|
||||
# the shim wrote the sandboxed-HOME one (run 18141's ghost divergence).
|
||||
clean_env = {k: v for k, v in os.environ.items()
|
||||
if k not in ("RUNE_MEMORY_DIR", "RUNE_HOME")}
|
||||
if not os.path.exists(BIN):
|
||||
# FAIL loud, never crash: run 18164 died on an uncaught Popen
|
||||
# FileNotFoundError when the artifact was absent from the checkout.
|
||||
# CI now builds the crate (canary job); a missing binary here means
|
||||
# the build step regressed and this canary must go red, not skip.
|
||||
test("release binary exists in the checkout (CI builds it)",
|
||||
False, f"missing: {BIN}")
|
||||
else:
|
||||
try:
|
||||
env = dict(clean_env, HOME=tmp2)
|
||||
r = subprocess.run([BIN, "verify"], capture_output=True, text=True, env=env)
|
||||
test("binary verify on missing ledger -> valid-empty",
|
||||
r.returncode == 0 and "Chain valid: True" in r.stdout
|
||||
and "Entries: 0" in r.stdout and "empty ledger" in r.stdout,
|
||||
f"rc={r.returncode} out={r.stdout!r} err={r.stderr!r}")
|
||||
r = subprocess.run([BIN], capture_output=True, text=True, env=env)
|
||||
test("binary no-args -> help + exit 1", r.returncode == 1
|
||||
and "usage:" in r.stdout, f"rc={r.returncode}")
|
||||
r = subprocess.run([BIN, "frobnicate"], capture_output=True, text=True, env=env)
|
||||
test("binary invalid choice -> exit 2", r.returncode == 2
|
||||
and "invalid choice" in r.stderr, f"rc={r.returncode} err={r.stderr!r}")
|
||||
r = subprocess.run([BIN, "list", "--limit", "3"], capture_output=True, text=True,
|
||||
env=dict(clean_env, HOME=bin_home) if bin_home else clean_env)
|
||||
test("binary list --limit 3 on the shim's ledger renders 3 entries",
|
||||
r.returncode == 0 and "Recent ledger entries (3):" in r.stdout,
|
||||
f"rc={r.returncode} out={r.stdout[:120]!r}")
|
||||
finally:
|
||||
import shutil
|
||||
shutil.rmtree(tmp2, ignore_errors=True)
|
||||
|
||||
# --- 6. Port 2 (verification_layer) through the same cdylib ---
|
||||
print("\n--- 6. Port 2: verification_layer via ep_loader ---")
|
||||
import json
|
||||
if _CRATE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CRATE_DIR)
|
||||
try:
|
||||
import ep_loader # type: ignore[import-not-found] (ctypes cdylib wrapper — runtime import by design)
|
||||
v = ep_loader.verify('{"exit_code": 0}', "exit_code")
|
||||
out = json.loads(v)
|
||||
test("ep_verify exit_code=0 -> passed True", out.get("passed") is True)
|
||||
v = ep_loader.verify('{"exit_code": false}', "exit_code")
|
||||
out = json.loads(v)
|
||||
test("ep_verify bool-false renders str() shape (False, not 0)",
|
||||
out.get("passed") is True and out.get("evidence_refs") == ["exit_code=False"],
|
||||
f"got {out}")
|
||||
v = ep_loader.verify('{}', "frobnicate")
|
||||
out = json.loads(v)
|
||||
test("ep_verify unknown verifier -> passed False retryable False",
|
||||
out.get("passed") is False and out.get("retryable") is False
|
||||
and out.get("escalation_reason") == "unknown_verifier",
|
||||
f"got {out}")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("port-2 ep_verify FFI", False, str(e))
|
||||
|
||||
# --- 7. src/rune shim module ---
|
||||
print("\n--- 7. src/rune verification_layer shim ---")
|
||||
try:
|
||||
sys.path.insert(0, os.path.join(_REPO, "src", "rune"))
|
||||
import verification_layer as vl
|
||||
r = vl.verify({"exit_code": 1}, "exit_code")
|
||||
test("shim verify returns VerificationResult dataclass",
|
||||
type(r).__name__ == "VerificationResult")
|
||||
test("shim CLI parity: fail case escalation_reason=exit_code_nonzero",
|
||||
r.to_dict()["escalation_reason"] == "exit_code_nonzero")
|
||||
test("shim PLUGIN name preserved", vl.PLUGIN["name"] == "verification_layer")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("src/rune verification_layer shim", False, str(e))
|
||||
|
||||
# --- 8. Port 3 (evidence_probe): schema gate + budget count ---
|
||||
# Expectations probed against the serving shim 2026-10-03 (zero-dep split:
|
||||
# schema gate + count Rust; orchestration Python).
|
||||
print("\n--- 8. Port 3: evidence_probe shim ---")
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
import evidence_probe as epr
|
||||
|
||||
errs = epr.validate_probe({})
|
||||
test("validate_probe({}) -> 5 missing-field errors in field order",
|
||||
errs == ["missing required field: question",
|
||||
"missing required field: source",
|
||||
"missing required field: answer",
|
||||
"missing required field: implication",
|
||||
"missing required field: status"], f"got {errs}")
|
||||
good = {"question": "q", "source": "s", "answer": "a",
|
||||
"implication": "i", "status": "STILL_UNKNOWN"}
|
||||
test("valid STILL_UNKNOWN probe -> no errors",
|
||||
epr.validate_probe(good) == [], f"got {epr.validate_probe(good)}")
|
||||
bad_status = dict(good, status="PASS")
|
||||
test("status 'PASS' -> membership error with Python repr rendering",
|
||||
epr.validate_probe(bad_status) ==
|
||||
["status must be one of ['CONTRADICTED', 'GROUNDED', 'STILL_UNKNOWN'], got 'PASS'"],
|
||||
f"got {epr.validate_probe(bad_status)}")
|
||||
int_status = dict(good, status=7)
|
||||
test("non-string status -> field error AND membership error (Python law)",
|
||||
epr.validate_probe(int_status) ==
|
||||
["field status must be a string",
|
||||
"status must be one of ['CONTRADICTED', 'GROUNDED', 'STILL_UNKNOWN'], got 7"],
|
||||
f"got {epr.validate_probe(int_status)}")
|
||||
extra = dict(good, extra=1)
|
||||
test("extra field -> unexpected-fields error appended last",
|
||||
epr.validate_probe(extra)[-1] == "unexpected extra fields: ['extra']",
|
||||
f"got {epr.validate_probe(extra)}")
|
||||
grounded_blank = dict(good, question="", status="GROUNDED")
|
||||
test("blank question + GROUNDED -> both errors (citation gate)",
|
||||
epr.validate_probe(grounded_blank) ==
|
||||
["field question must be a non-empty string",
|
||||
"GROUNDED probe requires a concrete source citation"],
|
||||
f"got {epr.validate_probe(grounded_blank)}")
|
||||
|
||||
probe_dir = tempfile.mkdtemp(prefix="eprcanary_")
|
||||
try:
|
||||
with open(os.path.join(probe_dir, "probe.jsonl"), "w") as f:
|
||||
f.write('{"q": 1}\n\n{"r": 2}\n\n\n{"s": 3}\n')
|
||||
test("count_probes counts non-empty lines only",
|
||||
epr.count_probes(probe_dir) == 3,
|
||||
f"got {epr.count_probes(probe_dir)}")
|
||||
test("count_probes on missing dir -> 0",
|
||||
epr.count_probes(probe_dir + "/nope") == 0)
|
||||
test("MAX_PROBES_PER_TASK is 2 (the no-recursion budget)",
|
||||
epr.MAX_PROBES_PER_TASK == 2)
|
||||
try:
|
||||
epr.check_budget(probe_dir)
|
||||
test("check_budget raises at the budget", False, "no raise")
|
||||
except epr.ProbeBudgetExceeded as e:
|
||||
test("check_budget raises ProbeBudgetExceeded at 2 probes",
|
||||
str(e).startswith("probe budget exhausted (2)"), f"got {e}")
|
||||
finally:
|
||||
import shutil
|
||||
shutil.rmtree(probe_dir, ignore_errors=True)
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("port-3 evidence_probe shim", False, str(e))
|
||||
|
||||
# --- 9. Port 4 (behaviour_ledger): doc-envelope state machine ---
|
||||
# Expectations probed against ep_loader 2026-10-03: doc-in/doc-out,
|
||||
# {ok, err} envelope, CONFIRMED_STATES-only invalidation, TEST_FAILED-first
|
||||
# queue ordering.
|
||||
print("\n--- 9. Port 4: behaviour_ledger via ep_loader ---")
|
||||
try:
|
||||
import ep_loader # type: ignore[import-not-found] (already imported in §6; re-import is a cache hit)
|
||||
|
||||
doc = {"behaviours": {}}
|
||||
doc, rec = ep_loader.bl_define(
|
||||
doc, behaviour_id="alpha", owning_project="proj",
|
||||
intended_behaviour="ib", origin_intent="oi", pass_criteria="pc",
|
||||
fail_criteria="fc", procedure="proc", owner_component="comp:alpha",
|
||||
dependencies=["dep:one"], automated_ref="ar", environment="env",
|
||||
now="2026-10-03T23:00:00Z")
|
||||
test("define returns (doc, rec) with normalized id + UNVALIDATED",
|
||||
rec.get("behaviour_id") == "behaviour:alpha"
|
||||
and rec.get("state") == "UNVALIDATED",
|
||||
f"got id={rec.get('behaviour_id')} state={rec.get('state')}")
|
||||
test("define record carries the 18-key shape",
|
||||
set(rec.keys()) == {"automated_ref", "behaviour_id", "created_at",
|
||||
"dependencies", "environment", "evidence",
|
||||
"fail_criteria", "history", "intended_behaviour",
|
||||
"invalidations", "limitations", "origin_intent",
|
||||
"owner_component", "owning_project", "pass_criteria",
|
||||
"procedure", "state", "updated_at"},
|
||||
f"got {sorted(rec.keys())}")
|
||||
|
||||
doc, _ = ep_loader.bl_record_auto(doc, "alpha", passed=True,
|
||||
detail="tests green", generation="g1",
|
||||
now="2026-10-03T23:01:00Z")
|
||||
rec_a = ep_loader.bl_get(doc, "alpha")
|
||||
test("record_auto pass -> AUTO_VERIFIED (a CONFIRMED state)",
|
||||
rec_a is not None and rec_a["state"] == "AUTO_VERIFIED")
|
||||
|
||||
doc, _ = ep_loader.bl_define(doc, behaviour_id="beta", owning_project="proj",
|
||||
intended_behaviour="ib2", now="2026-10-03T23:02:00Z")
|
||||
doc, _ = ep_loader.bl_record_auto(doc, "beta", passed=False, detail="flaky",
|
||||
now="2026-10-03T23:03:00Z")
|
||||
rec_b = ep_loader.bl_get(doc, "beta")
|
||||
test("record_auto fail -> TEST_FAILED",
|
||||
rec_b is not None and rec_b["state"] == "TEST_FAILED")
|
||||
|
||||
# Selective invalidation (rule 19I): only CONFIRMED_STATES with
|
||||
# dep/owner overlap reopen; unrelated change preserves.
|
||||
doc, untouched = ep_loader.bl_invalidate(
|
||||
doc, changed_entities=["dep:unrelated"], now="2026-10-03T23:04:00Z")
|
||||
rec_a = ep_loader.bl_get(doc, "alpha")
|
||||
test("unrelated change invalidates nothing; AUTO_VERIFIED preserved",
|
||||
untouched == [] and rec_a is not None and rec_a["state"] == "AUTO_VERIFIED")
|
||||
doc, hit = ep_loader.bl_invalidate(
|
||||
doc, changed_entities=["dep:one"], generation="g2",
|
||||
reasons={"behaviour:alpha": "dep one changed"}, now="2026-10-03T23:05:00Z")
|
||||
test("dependency hit invalidates exactly the confirmed behaviour",
|
||||
len(hit) == 1 and hit[0]["state"] == "RETEST_REQUIRED",
|
||||
f"got {[r['state'] for r in hit]}")
|
||||
inv_entry = hit[0]["invalidations"][0]
|
||||
test("invalidation entry carries because/generation/was/changed_entities",
|
||||
inv_entry.get("because") == "dep one changed"
|
||||
and inv_entry.get("generation") == "g2"
|
||||
and inv_entry.get("was") == "AUTO_VERIFIED"
|
||||
and inv_entry.get("changed_entities") == ["dep:one"],
|
||||
f"got {inv_entry}")
|
||||
doc, rehit = ep_loader.bl_invalidate(
|
||||
doc, changed_entities=["dep:one"], now="2026-10-03T23:06:00Z")
|
||||
rec_a = ep_loader.bl_get(doc, "alpha")
|
||||
test("re-invalidating a non-confirmed record is a no-op",
|
||||
rehit == [] and rec_a is not None and rec_a["state"] == "RETEST_REQUIRED")
|
||||
|
||||
out = ep_loader.bl_outstanding(doc)
|
||||
test("outstanding() puts TEST_FAILED first (queue ordering)",
|
||||
out and out[0]["state"] == "TEST_FAILED",
|
||||
f"got {[(r['behaviour_id'], r['state']) for r in out]}")
|
||||
summ = ep_loader.bl_summary(doc)
|
||||
test("summary() shape {total, states, outstanding}",
|
||||
summ.get("total") == 2 and isinstance(summ.get("states"), dict)
|
||||
and "outstanding" in summ, f"got {summ}")
|
||||
hist = ep_loader.bl_retest_history(doc, "alpha")
|
||||
test("retest_history(alpha) -> [automated_verification, selective_invalidation]",
|
||||
[e.get("kind") for e in hist] == ["automated_verification", "selective_invalidation"],
|
||||
f"got {[e.get('kind') for e in hist]}")
|
||||
|
||||
try:
|
||||
ep_loader.bl_define(doc, behaviour_id="alpha", owning_project="x",
|
||||
intended_behaviour="y", now="2026-10-03T23:07:00Z")
|
||||
test("duplicate define raises ValueError", False, "no raise")
|
||||
except ValueError as e:
|
||||
test("duplicate define error message byte-exact",
|
||||
str(e) == "behaviour exists: behaviour:alpha", f"got {e}")
|
||||
try:
|
||||
ep_loader.bl_record_auto(doc, "ghost", passed=True, now="2026-10-03T23:08:00Z")
|
||||
test("unknown behaviour record raises ValueError", False, "no raise")
|
||||
except ValueError as e:
|
||||
test("unknown behaviour error message byte-exact",
|
||||
str(e) == "unknown behaviour 'ghost'", f"got {e}")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("port-4 behaviour_ledger FFI", False, str(e))
|
||||
|
||||
# --- 10. Port 5 (behavior): decision math through the shim on an
|
||||
# isolated db (RUNE_HOME + RUNE_MEMORY_DIR pinned BEFORE import —
|
||||
# memory_dir() honors RUNE_MEMORY_DIR first, and DB_PATH resolves at
|
||||
# import time). SQLite stays Python; the math is Rust.
|
||||
print("\n--- 10. Port 5: behavior shim (isolated db) ---")
|
||||
_iso_home = tempfile.mkdtemp(prefix="alcanary_runehome_")
|
||||
os.environ["RUNE_HOME"] = _iso_home
|
||||
os.environ["RUNE_MEMORY_DIR"] = os.path.join(_iso_home, "memory")
|
||||
try:
|
||||
import behavior as bh
|
||||
|
||||
test("behavior DB resolved under the isolated RUNE_HOME",
|
||||
bh.DB_PATH.startswith(os.environ["RUNE_HOME"]), f"got {bh.DB_PATH}")
|
||||
cid = bh.add_correction("say X", "say Y", domain="canary")["added"]
|
||||
r = bh.reinforce(cid, "success", item_type="correction")
|
||||
test("correction reinforce success: confidence 0.5 -> 0.6, stays active",
|
||||
r == {"reinforced": cid, "confidence": 0.6, "state": "active"},
|
||||
f"got {r}")
|
||||
r = bh.reinforce(cid, "failure", item_type="correction")
|
||||
test("correction reinforce failure: 0.6 -> 0.3, archives at the boundary",
|
||||
r == {"reinforced": cid, "confidence": 0.3, "state": "archived"},
|
||||
f"got {r}")
|
||||
hid = bh.add_habit("trigger t", "action a", name="canary habit")["added"]
|
||||
for _ in range(3):
|
||||
r = bh.reinforce(hid, "success")
|
||||
test("habit promotion: candidate -> active at min_activations (3)",
|
||||
r["state"] == "active" and r["confidence"] == 1,
|
||||
f"got {r}")
|
||||
test("habit not-found returns error dict (not raise)",
|
||||
bh.reinforce(99999, "success") == {"error": "Habit 99999 not found"})
|
||||
test("correction not-found returns error dict (not raise)",
|
||||
bh.reinforce(99999, "success", item_type="correction")
|
||||
== {"error": "Correction 99999 not found"})
|
||||
stats = bh.behavior_stats()
|
||||
test("behavior_stats carries the five-key shape",
|
||||
set(stats.keys()) == {"corrections_active", "corrections_archived",
|
||||
"habits_active", "habits_candidate",
|
||||
"total_activations"}, f"got {sorted(stats.keys())}")
|
||||
test("stats reflect the battery: 1 archived correction, 1 active habit",
|
||||
stats["corrections_archived"] == 1 and stats["habits_active"] == 1,
|
||||
f"got {stats}")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("port-5 behavior shim", False, str(e))
|
||||
|
||||
# --- 11. Port 6 (claim_audit): full pipeline through the shim on an
|
||||
# isolated journal (MEM_DIR resolves under the pinned RUNE_HOME).
|
||||
# SYMMETRY GUARD never-fatal: research_lens absence must not crash.
|
||||
print("\n--- 11. Port 6: claim_audit shim (isolated journal) ---")
|
||||
try:
|
||||
import claim_audit as ca
|
||||
|
||||
claims = ca.scan("We exceeded the frontier. Results are flawless.")
|
||||
test("scan flags the comparative + absolute claims",
|
||||
[(c["type"], c["match"]) for c in claims]
|
||||
== [("comparative", "we exceeded"), ("absolute", "flawless")],
|
||||
f"got {[(c['type'], c['match']) for c in claims]}")
|
||||
st = ca.scan_text("We exceeded anything.")
|
||||
test("scan_text returns the repaired {claims, clean, degraded, rewritten} surface",
|
||||
set(st.keys()) == {"claims", "clean", "degraded", "rewritten"},
|
||||
f"got {sorted(st.keys())}")
|
||||
test("scan_text degrades the unsupported comparative (clean False)",
|
||||
st["clean"] is False and len(st["degraded"]) == 1
|
||||
and st["degraded"][0]["verdict"] == "DEGRADE",
|
||||
f"clean={st['clean']} degraded={st['degraded']}")
|
||||
res = ca.audit("We exceeded the frontier. Results are flawless.")
|
||||
test("audit() -> both claims DEGRADE, clean False",
|
||||
res["clean"] is False
|
||||
and [c["verdict"] for c in res["claims"]] == ["DEGRADE", "DEGRADE"],
|
||||
f"got {[(c['verdict']) for c in res['claims']]} clean={res['clean']}")
|
||||
test("audit() rewrote the draft (degrade templates applied)",
|
||||
res["rewritten"] != "We exceeded the frontier. Results are flawless.")
|
||||
with open(ca.JOURNAL) as f:
|
||||
rows = f.readlines()
|
||||
test("journal wrote one row per claim + scan_text row (isolated)",
|
||||
len(rows) == 3, f"got {len(rows)} rows at {ca.JOURNAL}")
|
||||
rep = ca.report()
|
||||
test("report() aggregates through the Rust core: total + by_verdict + by_type",
|
||||
rep.get("total") == 3
|
||||
and rep.get("by_verdict", {}).get("DEGRADE") == 3
|
||||
and rep.get("by_type", {}).get("comparative") == 2
|
||||
and rep.get("by_type", {}).get("absolute") == 1,
|
||||
f"got {rep}")
|
||||
test("verify() on an unsupported plain claim -> ('DEGRADE', 0)",
|
||||
ca.verify("the module renders a menu when opened") == ("DEGRADE", 0))
|
||||
test("evidence-term stoplist through the Rust core (>=4 chars, trio dropped)",
|
||||
ca.ep_loader.ca_evidence_terms(["this", "hash", "canary", "ru"])
|
||||
== ["this", "hash", "canary"],
|
||||
f"got {ca.ep_loader.ca_evidence_terms(['this', 'hash', 'canary', 'ru'])}")
|
||||
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
|
||||
test("port-6 claim_audit shim", False, str(e))
|
||||
|
||||
print(f"\n{'=' * 47}")
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
sys.exit(1 if FAIL else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = os.path.join(sys.path[0], "canary_audit_ledger.py")
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--selfcheck":
|
||||
print("selfcheck ok")
|
||||
sys.exit(0)
|
||||
main()
|
||||
@@ -19,7 +19,8 @@ def has_idempotent_label_removal(workflow: str) -> bool:
|
||||
return bool(
|
||||
re.search(
|
||||
r"for \(const name of staleManaged\) \{\s*"
|
||||
r"try \{\s*await github\.rest\.issues\.removeLabel\(\{.*?"
|
||||
r"try \{.*?await github\.request\('DELETE /repos/\{owner\}/\{repo\}/issues/\{issue_number\}/labels/\{id\}', \{.*?"
|
||||
r"id: currentLabelIds\.get\(name\),\s*"
|
||||
r"\}\);\s*\} catch \(error\) \{\s*"
|
||||
r"if \(error\.status !== 404\) \{\s*throw error;\s*\}\s*"
|
||||
r"console\.log\(`Managed label already absent: \$\{name\}`\);\s*"
|
||||
@@ -64,7 +65,7 @@ def main() -> int:
|
||||
"infrastructure",
|
||||
"life",
|
||||
"custodian",
|
||||
"priority: high",
|
||||
"P0 — critical", "P1 — high", "P2 — medium", "P3 — low",
|
||||
"bug",
|
||||
"feature",
|
||||
)
|
||||
@@ -80,7 +81,8 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"stale managed labels are removed",
|
||||
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label and "removeLabel" in auto_label,
|
||||
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label
|
||||
and "currentLabelIds.get(name)" in auto_label,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -231,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.86.0"' in rune_version,
|
||||
'__version__ = "0.99.1"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_rune_entry.py — H1 canary for the Go first-call shim.
|
||||
|
||||
Same PASS/FAIL discipline as the slice canaries (counter harness, exit 1 on
|
||||
any FAIL). The canary builds the zero-dep Go module into a temp dir at run
|
||||
start (works on the desktop and in CI — system Go is world-readable), then
|
||||
verifies the observed launcher contract:
|
||||
|
||||
--version -> "rune <x.y.z>" matching the deployed pyproject version
|
||||
--python-only -> the exact handoff shape of the deployed bash launcher
|
||||
(cd <deployed>/src && <venv-python> -m rune <args>)
|
||||
--help -> usage text, exit 0
|
||||
unknown flag -> usage on stderr, exit 2
|
||||
go test -> the module's own unit battery green (subprocess)
|
||||
|
||||
Expectations are OBSERVED, not invented: the bash launcher at
|
||||
~/.local/bin/rune (cd + env contract) and the deployed pyproject version.
|
||||
|
||||
Run under ~/.venvs/memory/bin/python3.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
REPO = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
GO_MOD = os.path.join(REPO, "src", "go", "rune-entry")
|
||||
|
||||
|
||||
def test(name, cond, detail=""):
|
||||
global PASS, FAIL
|
||||
mark = "✓" if cond else "✗"
|
||||
print(f" {mark} {name}" + (f" — {detail}" if detail and not cond else ""))
|
||||
if cond:
|
||||
PASS += 1
|
||||
else:
|
||||
FAIL += 1
|
||||
|
||||
|
||||
def deployed_version():
|
||||
for cand in ("/mnt/workdrive/rune-code/current/pyproject.toml",
|
||||
os.path.join(REPO, "pyproject.toml")):
|
||||
try:
|
||||
for line in open(cand):
|
||||
if line.startswith('version = "'):
|
||||
return line.split('"')[1]
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("=== RUNE ENTRY (Go first-call) CANARY (H1) ===\n")
|
||||
|
||||
# --- 0. Build the artifact (zero-dep; system Go) ---
|
||||
print("--- 0. Build ---")
|
||||
tmp = tempfile.mkdtemp(prefix="runeentry_canary_")
|
||||
bin_path = os.path.join(tmp, "rune")
|
||||
r = subprocess.run(["go", "build", "-o", bin_path, "."],
|
||||
capture_output=True, text=True, cwd=GO_MOD)
|
||||
test("go build succeeds (zero-dep, system toolchain)", r.returncode == 0,
|
||||
f"rc={r.returncode} err={r.stderr[:200]!r}")
|
||||
if r.returncode != 0:
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
sys.exit(1)
|
||||
|
||||
# --- 1. Version surface ---
|
||||
print("\n--- 1. Version ---")
|
||||
want = deployed_version()
|
||||
r = subprocess.run([bin_path, "--version"], capture_output=True, text=True)
|
||||
test("--version exits 0", r.returncode == 0, f"rc={r.returncode}")
|
||||
test("--version prints 'rune <x.y.z>'",
|
||||
r.stdout.startswith("rune ") and r.stdout.split()[-1].count(".") == 2,
|
||||
f"got {r.stdout!r}")
|
||||
test("version matches the deployed pyproject",
|
||||
want is not None and r.stdout.strip() == f"rune {want}",
|
||||
f"got {r.stdout.strip()!r} want 'rune {want}'")
|
||||
|
||||
# --- 2. Handoff shape (the bash launcher's contract) ---
|
||||
print("\n--- 2. Handoff shape ---")
|
||||
r = subprocess.run([bin_path, "--python-only", "status"],
|
||||
capture_output=True, text=True)
|
||||
out = r.stdout.strip()
|
||||
test("--python-only exits 0", r.returncode == 0, f"rc={r.returncode}")
|
||||
test("handoff cd's into the deployed src revision",
|
||||
out.startswith("(cd /mnt/workdrive/rune-code/current/src"),
|
||||
f"got {out!r}")
|
||||
venv_py = os.path.join(os.environ.get("HOME", ""), ".venvs", "memory", "bin", "python3")
|
||||
if os.path.isfile(venv_py):
|
||||
test("handoff execs the memory-venv python with -m rune",
|
||||
".venvs/memory/bin/python3 -m rune" in out, f"got {out!r}")
|
||||
else:
|
||||
test("handoff falls back to PATH python3 when the memory venv is absent "
|
||||
"(bare HOME — CI sandbox)",
|
||||
"python3 -m rune" in out, f"got {out!r}")
|
||||
test("args pass through the handoff",
|
||||
out.endswith("status)"), f"got {out!r}")
|
||||
|
||||
# --- 3. Flag discipline ---
|
||||
print("\n--- 3. Flag discipline ---")
|
||||
r = subprocess.run([bin_path, "--help"], capture_output=True, text=True)
|
||||
test("--help exits 0 with usage text",
|
||||
r.returncode == 0 and "usage:" in r.stdout,
|
||||
f"rc={r.returncode} out={r.stdout[:60]!r}")
|
||||
r = subprocess.run([bin_path, "--frobnicate"], capture_output=True, text=True)
|
||||
test("unknown flag -> exit 2 + usage on stderr",
|
||||
r.returncode == 2 and "usage:" in r.stderr,
|
||||
f"rc={r.returncode} err={r.stderr[:80]!r}")
|
||||
|
||||
# --- 4. The module's own unit battery ---
|
||||
print("\n--- 4. go test ---")
|
||||
r = subprocess.run(["go", "test", "./..."], capture_output=True, text=True,
|
||||
cwd=GO_MOD)
|
||||
test("go test ./... green (the Go unit battery)",
|
||||
r.returncode == 0 and "ok" in r.stdout,
|
||||
f"rc={r.returncode} out={r.stdout[-120:]!r}")
|
||||
|
||||
# --- 5. go vet ---
|
||||
print("\n--- 5. go vet ---")
|
||||
r = subprocess.run(["go", "vet", "./..."], capture_output=True, text=True,
|
||||
cwd=GO_MOD)
|
||||
test("go vet clean", r.returncode == 0,
|
||||
f"rc={r.returncode} err={r.stderr[:120]!r}")
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
print(f"\n{'=' * 47}")
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
sys.exit(1 if FAIL else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_rune_serve.py — H2 parity battery (T4.3, contract
|
||||
BUILDER-2026-10-04T08:32:43Z-cli).
|
||||
|
||||
The H2 contract: the Go front door (`rune serve`) serves IDENTICAL shapes
|
||||
to the Python core server for the same canned conversations — byte-level
|
||||
for metadata (modulo volatile id/created), exact for NDJSON line sequences.
|
||||
|
||||
Determinism law: no live model calls. Both servers run against a stub
|
||||
operator with canned responses (the established _execute_on_model stub
|
||||
class of seam — the server's shape is the contract under test, not the
|
||||
cognition behind it).
|
||||
|
||||
Sections:
|
||||
1. Python core server boots (stdlib, ephemeral port)
|
||||
2. Python shape freeze (models / status / chat / stream / 404)
|
||||
3. Go serve subcommand builds and answers --help
|
||||
4. Cross-language parity (Go front → Python core, same canned set)
|
||||
5. Core-down honesty (502 envelope, both servers agree)
|
||||
|
||||
Run: python3 src/rune/canary_rune_serve.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
|
||||
def test(name: str, ok: bool, detail: str = "") -> None:
|
||||
global PASS, FAIL
|
||||
if ok:
|
||||
PASS += 1
|
||||
print(f" ✓ {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ✗ {name} — {detail}")
|
||||
|
||||
|
||||
# ── stub operator: canned, deterministic ────────────────────────────────────
|
||||
|
||||
CANNED_TASK = {
|
||||
"task_id": "task-canned-001",
|
||||
"status": "completed",
|
||||
"model": "stub-model",
|
||||
"route": "direct",
|
||||
"user_text": "Canned reply: the shape is the contract.",
|
||||
"success": True,
|
||||
"degraded": [],
|
||||
"iterations_used": 1,
|
||||
"iteration_budget": 6,
|
||||
}
|
||||
|
||||
|
||||
class StubOperator:
|
||||
def list_models(self):
|
||||
return [{"name": "stub-model-a", "size_gb": 4.1},
|
||||
{"name": "stub-model-b", "size_gb": 8.2}]
|
||||
|
||||
def get_status(self):
|
||||
return {"identity": "rune", "role": "operator", "current_task": None,
|
||||
"active_model": "stub-model-a", "tasks_pending": 0,
|
||||
"tasks_running": 0, "tasks_completed": 3}
|
||||
|
||||
def send_task(self, description, model=None, channel="operator"):
|
||||
out = dict(CANNED_TASK)
|
||||
out["echo"] = description # proves the request reached the core
|
||||
return out
|
||||
|
||||
def conversation_window(self, channel="operator", n=8):
|
||||
return [{"role": "user", "content": "earlier", "channel_id": channel}][:n]
|
||||
|
||||
def resolve_pending_approval(self, approved):
|
||||
return {"resolved": bool(approved), "approval_id": "appr-1"}
|
||||
|
||||
|
||||
# ── http helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
def http(method: str, url: str, body: dict | None = None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method,
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
raw = resp.read().decode()
|
||||
return resp.status, raw
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()
|
||||
|
||||
|
||||
def normalize(obj):
|
||||
"""Strip volatile fields (id, created) for parity comparison."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: normalize(v) for k, v in obj.items()
|
||||
if k not in ("id", "created")}
|
||||
if isinstance(obj, list):
|
||||
return [normalize(v) for v in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def wait_ready(url: str, timeout: float = 10.0) -> bool:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=1) as r:
|
||||
if r.status == 200:
|
||||
return True
|
||||
except Exception: # noqa: BLE001 — readiness poll: any failure = not yet
|
||||
time.sleep(0.1)
|
||||
return False
|
||||
|
||||
|
||||
CANNED_CHAT = {"model": "stub-model-a",
|
||||
"messages": [{"role": "user", "content": "Say the canned thing."}]}
|
||||
|
||||
|
||||
def fire_shape_set(base: str) -> dict:
|
||||
"""The canned request battery against one server; returns raw results."""
|
||||
out = {}
|
||||
out["models"] = http("GET", f"{base}/v1/models")
|
||||
out["status"] = http("GET", f"{base}/v1/status")
|
||||
out["chat"] = http("POST", f"{base}/v1/chat/completions", CANNED_CHAT)
|
||||
out["stream"] = http("POST", f"{base}/v1/chat/completions",
|
||||
{**CANNED_CHAT, "stream": True})
|
||||
out["conversation"] = http("GET", f"{base}/v1/conversation?channel=operator&n=8")
|
||||
out["approve"] = http("POST", f"{base}/v1/approvals/resolve", {"approved": True})
|
||||
out["missing"] = http("GET", f"{base}/v1/definitely-not-here")
|
||||
return out
|
||||
|
||||
|
||||
def check_shapes(prefix: str, results: dict) -> None:
|
||||
s, raw = results["models"]
|
||||
body = json.loads(raw) if s == 200 else {}
|
||||
test(f"{prefix}: /v1/models 200 + list envelope",
|
||||
s == 200 and body.get("object") == "list"
|
||||
and [m.get("id") for m in body.get("data", [])] == ["stub-model-a", "stub-model-b"]
|
||||
and all(m.get("object") == "model" and m.get("owned_by") == "rune"
|
||||
for m in body.get("data", [])),
|
||||
f"status={s} body={raw[:160]!r}")
|
||||
|
||||
s, raw = results["status"]
|
||||
body = json.loads(raw) if s == 200 else {}
|
||||
test(f"{prefix}: /v1/status pass-through keys",
|
||||
s == 200 and body.get("active_model") == "stub-model-a"
|
||||
and body.get("tasks_completed") == 3,
|
||||
f"status={s} body={raw[:160]!r}")
|
||||
|
||||
s, raw = results["chat"]
|
||||
body = json.loads(raw) if s == 200 else {}
|
||||
choices = body.get("choices", [{}])
|
||||
test(f"{prefix}: chat.completion envelope + rune metadata",
|
||||
s == 200 and body.get("object") == "chat.completion"
|
||||
and choices[0].get("message", {}).get("role") == "assistant"
|
||||
and "Canned reply" in choices[0].get("message", {}).get("content", "")
|
||||
and body.get("rune", {}).get("task_id") == "task-canned-001"
|
||||
and body.get("rune", {}).get("echo") == "Say the canned thing.",
|
||||
f"status={s} body={raw[:200]!r}")
|
||||
|
||||
s, raw = results["stream"]
|
||||
lines = [l for l in raw.strip().splitlines() if l.strip()]
|
||||
parsed = []
|
||||
for l in lines:
|
||||
try:
|
||||
parsed.append(json.loads(l))
|
||||
except json.JSONDecodeError:
|
||||
parsed.append(None)
|
||||
test(f"{prefix}: NDJSON stream — chunk(s) then terminal",
|
||||
s == 200 and len(parsed) >= 2 and None not in parsed
|
||||
and all(p.get("object") == "chat.completion.chunk" for p in parsed)
|
||||
and parsed[-1].get("choices", [{}])[0].get("finish_reason") == "stop"
|
||||
and "Canned reply" in (parsed[0].get("choices", [{}])[0]
|
||||
.get("delta", {}).get("content", "")),
|
||||
f"status={s} lines={len(lines)} raw={raw[:200]!r}")
|
||||
|
||||
s, raw = results["conversation"]
|
||||
body = json.loads(raw) if s == 200 else {}
|
||||
test(f"{prefix}: /v1/conversation window shape",
|
||||
s == 200 and isinstance(body.get("messages"), list)
|
||||
and body["messages"][0]["role"] == "user",
|
||||
f"status={s} body={raw[:160]!r}")
|
||||
|
||||
s, raw = results["approve"]
|
||||
body = json.loads(raw) if s == 200 else {}
|
||||
test(f"{prefix}: /v1/approvals/resolve round-trip",
|
||||
s == 200 and body.get("resolved") is True,
|
||||
f"status={s} body={raw[:160]!r}")
|
||||
|
||||
s, raw = results["missing"]
|
||||
body = json.loads(raw) if raw else {}
|
||||
test(f"{prefix}: 404 error envelope",
|
||||
s == 404 and body.get("error", {}).get("code") == "not_found",
|
||||
f"status={s} body={raw[:160]!r}")
|
||||
|
||||
|
||||
def compare_results(py: dict, go: dict) -> None:
|
||||
for key in ("models", "status", "chat", "conversation", "approve", "missing"):
|
||||
(ps, praw), (gs, graw) = py[key], go[key]
|
||||
same_status = ps == gs
|
||||
try:
|
||||
same_body = normalize(json.loads(praw)) == normalize(json.loads(graw))
|
||||
except json.JSONDecodeError:
|
||||
same_body = praw == graw
|
||||
test(f"parity: {key} identical modulo volatile",
|
||||
same_status and same_body,
|
||||
f"py=({ps}) {praw[:140]!r} go=({gs}) {graw[:140]!r}")
|
||||
# NDJSON: exact line-sequence equality modulo volatile
|
||||
(ps, praw), (gs, graw) = py["stream"], go["stream"]
|
||||
pl = [normalize(json.loads(l)) for l in praw.strip().splitlines() if l.strip()]
|
||||
gl = [normalize(json.loads(l)) for l in graw.strip().splitlines() if l.strip()]
|
||||
test("parity: NDJSON line sequences identical modulo volatile",
|
||||
ps == gs and pl == gl,
|
||||
f"py_lines={len(pl)} go_lines={len(gl)}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 47)
|
||||
print("canary_rune_serve — H2 Go front-door parity battery")
|
||||
print("=" * 47)
|
||||
|
||||
# ── 1. Python core server boots ───────────────────────────────────────
|
||||
print("\n--- 1. Python core server ---")
|
||||
try:
|
||||
from rune import core_server
|
||||
except ImportError as e:
|
||||
test("core_server imports", False, str(e))
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1
|
||||
test("core_server imports", True)
|
||||
|
||||
server = core_server.make_server("127.0.0.1", 0, StubOperator())
|
||||
port = server.server_address[1]
|
||||
import threading
|
||||
t = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
t.start()
|
||||
py_base = f"http://127.0.0.1:{port}"
|
||||
test("core server answers on an ephemeral port",
|
||||
wait_ready(f"{py_base}/v1/status"), py_base)
|
||||
|
||||
# ── 2. Python shape freeze ────────────────────────────────────────────
|
||||
print("\n--- 2. Python shape freeze ---")
|
||||
py_results = fire_shape_set(py_base)
|
||||
check_shapes("python", py_results)
|
||||
|
||||
# ── 3. Go serve subcommand ────────────────────────────────────────────
|
||||
print("\n--- 3. Go serve subcommand ---")
|
||||
go_dir = REPO / "src" / "go" / "rune-entry"
|
||||
env = dict(os.environ)
|
||||
env["GOFLAGS"] = env.get("GOFLAGS", "") + " -mod=mod"
|
||||
# Per-run output path: a fixed /tmp name collides cross-user (nick owns
|
||||
# the local build; the runner cannot overwrite it — run 18322 EACCES).
|
||||
go_bin = f"/tmp/rune-serve-canary-{os.getpid()}"
|
||||
r = subprocess.run(["go", "build", "-o", go_bin, "."],
|
||||
cwd=go_dir, env=env, capture_output=True, text=True, timeout=300)
|
||||
test("go build exits 0", r.returncode == 0,
|
||||
f"rc={r.returncode} err={r.stderr[-200:]!r}")
|
||||
if r.returncode != 0:
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1
|
||||
r = subprocess.run([go_bin, "serve", "--help"],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
test("rune serve --help exits 0 with usage",
|
||||
r.returncode == 0 and "core" in (r.stdout + r.stderr).lower(),
|
||||
f"rc={r.returncode} out={(r.stdout + r.stderr)[:160]!r}")
|
||||
|
||||
# ── 4. Cross-language parity ──────────────────────────────────────────
|
||||
print("\n--- 4. Parity: Go front → Python core ---")
|
||||
go_port = port + 1
|
||||
proc = subprocess.Popen(
|
||||
[go_bin, "serve", "--port", str(go_port),
|
||||
"--core", py_base],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
go_base = f"http://127.0.0.1:{go_port}"
|
||||
try:
|
||||
ready = wait_ready(f"{go_base}/v1/status")
|
||||
test("go front answers on its port", ready, go_base)
|
||||
if ready:
|
||||
go_results = fire_shape_set(go_base)
|
||||
check_shapes("go", go_results)
|
||||
compare_results(py_results, go_results)
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
# ── 5. Core-down honesty ──────────────────────────────────────────────
|
||||
print("\n--- 5. Core-down honesty ---")
|
||||
proc = subprocess.Popen(
|
||||
[go_bin, "serve", "--port", str(go_port),
|
||||
"--core", "http://127.0.0.1:1"], # nothing listening
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
try:
|
||||
time.sleep(0.5) # listener comes up even though the core never will
|
||||
s, raw = http("POST", f"{go_base}/v1/chat/completions", CANNED_CHAT)
|
||||
body = json.loads(raw) if raw else {}
|
||||
test("go 502 envelope when the core is down",
|
||||
s == 502 and body.get("error", {}).get("code") == "core_unavailable",
|
||||
f"status={s} body={raw[:160]!r}")
|
||||
s2, raw2 = http("POST", f"{py_base}/v1/chat/completions", CANNED_CHAT)
|
||||
test("python core still healthy after go core-down probe",
|
||||
s2 == 200, f"status={s2}")
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
server.shutdown()
|
||||
print(f"\n{'=' * 47}")
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1 if FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_spine_meta.py — meta-scaffold canary (H-meta).
|
||||
|
||||
Contract BUILDER-2026-10-04T06:24:56Z-cli (Nick directive 2026-10-04):
|
||||
every part of rune must exist as a modeled entity in rune's spine — the
|
||||
spine is the meta-scaffold. This canary proves the projection models the
|
||||
surface classes the closures have been honestly reporting as unmodeled:
|
||||
|
||||
1. rust-crate entities for both crates (evidence_provenance, query_memory)
|
||||
2. go-surface entity for src/go/rune-entry
|
||||
3. module entities for canary_audit_ledger / canary_rune_entry /
|
||||
canary_evidence_provenance_rust (with obligated_canary edges)
|
||||
4. workflow entities for every .gitea/workflows/*.yml
|
||||
5. config entities for the tranche config files
|
||||
6. determinism: two consecutive builds differ only in generated_at
|
||||
7. open-world law: every pre-existing entity survives the rebuild
|
||||
|
||||
PASS/FAIL counter harness, exit 1 on any FAIL. Expectations observed from
|
||||
the spine's own projection rules (src/rune/spine.py) and the umbrella
|
||||
manifest — never invented.
|
||||
|
||||
Run under ~/.venvs/memory/bin/python3.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
REPO = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
SPINE_STORE = os.path.join(REPO, "store", "spine", "current.json")
|
||||
|
||||
|
||||
def test(name, cond, detail=""):
|
||||
global PASS, FAIL
|
||||
mark = "✓" if cond else "✗"
|
||||
print(f" {mark} {name}" + (f" — {detail}" if detail and not cond else ""))
|
||||
if cond:
|
||||
PASS += 1
|
||||
else:
|
||||
FAIL += 1
|
||||
|
||||
|
||||
def build_spine():
|
||||
r = subprocess.run(
|
||||
[sys.executable, "-m", "rune.spine", "build"],
|
||||
capture_output=True, text=True, timeout=180,
|
||||
env={"PYTHONPATH": os.path.join(REPO, "src"), "PATH": os.environ.get("PATH", ""),
|
||||
"HOME": os.environ.get("HOME", "")},
|
||||
cwd=REPO)
|
||||
return r
|
||||
|
||||
|
||||
def load_spine():
|
||||
return json.load(open(SPINE_STORE))
|
||||
|
||||
|
||||
def main():
|
||||
print("=== SPINE META-SCAFFOLD CANARY (H-meta) ===\n")
|
||||
pre = load_spine() if os.path.isfile(SPINE_STORE) else {"entities": []}
|
||||
pre_ids = {e["id"] for e in pre["entities"]}
|
||||
|
||||
# --- 0. Build ---
|
||||
print("--- 0. Build ---")
|
||||
r = build_spine()
|
||||
test("spine build exits 0", r.returncode == 0, f"rc={r.returncode} err={r.stderr[-200:]!r}")
|
||||
if r.returncode != 0:
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
sys.exit(1)
|
||||
doc = load_spine()
|
||||
ents = {e["id"]: e for e in doc["entities"]}
|
||||
rels = doc.get("relationships", [])
|
||||
|
||||
# --- 1. Rust crates ---
|
||||
print("\n--- 1. Rust crates ---")
|
||||
for crate in ("evidence_provenance", "query_memory"):
|
||||
eid = f"rust-crate:{crate}"
|
||||
test(f"{eid} exists with kind rust-crate",
|
||||
eid in ents and ents[eid]["kind"] == "rust-crate",
|
||||
f"ids matching: {[i for i in ents if crate in i][:4]}")
|
||||
|
||||
# --- 2. Go surface ---
|
||||
print("\n--- 2. Go surface ---")
|
||||
test("go-surface:rune-entry exists with kind go-surface",
|
||||
"go-surface:rune-entry" in ents
|
||||
and ents["go-surface:rune-entry"]["kind"] == "go-surface",
|
||||
f"ids matching: {[i for i in ents if 'rune-entry' in i][:4]}")
|
||||
|
||||
# --- 3. Missing canaries modeled ---
|
||||
print("\n--- 3. Canaries ---")
|
||||
for canary in ("canary_audit_ledger", "canary_rune_entry",
|
||||
"canary_evidence_provenance_rust"):
|
||||
mid = f"module:{canary}"
|
||||
test(f"{mid} entity exists", mid in ents,
|
||||
f"present: {mid in ents}")
|
||||
edge = any(r["type"] == "obligated_canary" and r["to"] == mid for r in rels)
|
||||
test(f"{mid} has an obligated_canary edge from its umbrella", edge)
|
||||
|
||||
# --- 4. Workflows ---
|
||||
print("\n--- 4. Workflows ---")
|
||||
import glob
|
||||
wf_files = sorted(os.path.basename(p)
|
||||
for p in glob.glob(os.path.join(REPO, ".gitea", "workflows", "*.yml")))
|
||||
test("workflow inventory is non-empty", len(wf_files) >= 10, f"found {len(wf_files)}")
|
||||
missing = [w for w in wf_files if f"workflow:{w}" not in ents]
|
||||
test("every workflow file has a workflow entity", not missing,
|
||||
f"missing: {missing[:4]}")
|
||||
|
||||
# --- 5. Configs ---
|
||||
print("\n--- 5. Configs ---")
|
||||
for cfg in ("go-version.txt", "lint-baseline.json", "pyright-baseline.json",
|
||||
".coveragerc"):
|
||||
cid = f"config:{cfg}"
|
||||
test(f"{cid} entity exists with kind config",
|
||||
cid in ents and ents[cid]["kind"] == "config",
|
||||
f"present: {cid in ents}")
|
||||
|
||||
# --- 6. Determinism (steady-state pair) ---
|
||||
# The honest determinism contract: once the store already carries the
|
||||
# projector's facts, two consecutive builds with identical inputs must
|
||||
# differ only in generated_at. A MIGRATION pair (store predating the
|
||||
# projector vs store carrying it) is not a determinism failure — the
|
||||
# first build after any projector change legitimately rewrites state.
|
||||
# The warm-up build makes this check judge steady-state, not migration.
|
||||
print("\n--- 6. Determinism (steady-state) ---")
|
||||
warm = build_spine()
|
||||
test("warm-up build exits 0", warm.returncode == 0,
|
||||
f"rc={warm.returncode} err={warm.stderr[-160:]!r}")
|
||||
r = build_spine()
|
||||
first = load_spine()
|
||||
r = build_spine()
|
||||
second = load_spine()
|
||||
def strip_ts(d):
|
||||
"""Deep-strip time-varying fields: determinism is a STRUCTURAL
|
||||
contract (same entities/relationships/observation content), not a
|
||||
clock contract. observed_at/deployed_at/generated_at tick every
|
||||
build by design — comparing them made this check pass or fail on
|
||||
whether the two builds crossed a one-second boundary."""
|
||||
TIME_KEYS = {"generated_at", "observed_at", "deployed_at", "updated_at",
|
||||
"last_success"}
|
||||
def walk(x):
|
||||
if isinstance(x, dict):
|
||||
return {k: walk(v) for k, v in x.items() if k not in TIME_KEYS}
|
||||
if isinstance(x, list):
|
||||
return [walk(v) for v in x]
|
||||
return x
|
||||
def normalize(d):
|
||||
d = walk(d)
|
||||
# build() appends one deployed_identity observation per pass and
|
||||
# the store dedups on the FULL observation (timestamp included),
|
||||
# so the observations list grows by one identical-modulo-timestamp
|
||||
# entry per build. Structural content = the deduped set.
|
||||
seen, obs = set(), []
|
||||
for o in d.get("observations", []):
|
||||
key = json.dumps(o, sort_keys=True)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
obs.append(o)
|
||||
if "observations" in d:
|
||||
d["observations"] = obs
|
||||
return d
|
||||
return normalize(json.loads(json.dumps(d)))
|
||||
fa, fb = strip_ts(first), strip_ts(second)
|
||||
if fa != fb:
|
||||
# Diagnostic: write the stripped pair so the divergence is inspectable
|
||||
# instead of a bare boolean verdict.
|
||||
with open("/tmp/spine_meta_determinism_first.json", "w") as f:
|
||||
json.dump(fa, f, indent=2, sort_keys=True)
|
||||
with open("/tmp/spine_meta_determinism_second.json", "w") as f:
|
||||
json.dump(fb, f, indent=2, sort_keys=True)
|
||||
test("two consecutive steady-state builds are structurally identical "
|
||||
"(timestamps free to tick)",
|
||||
fa == fb,
|
||||
"rebuild not structurally stable "
|
||||
"(stripped pair dumped to /tmp/spine_meta_determinism_*.json)")
|
||||
|
||||
# --- 7. Open-world law ---
|
||||
print("\n--- 7. Open-world law ---")
|
||||
lost = pre_ids - {e["id"] for e in doc["entities"]}
|
||||
test("every pre-existing entity survives the rebuild", not lost,
|
||||
f"lost: {sorted(lost)[:4]}")
|
||||
|
||||
print(f"\n{'=' * 47}")
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
sys.exit(1 if FAIL else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_tui_transport.py — H4 slice 1 battery (T4.5, contract
|
||||
BUILDER-2026-10-04T10:32:00Z-cli).
|
||||
|
||||
The TUI transport seam contract: InProcessTransport and V1Transport answer
|
||||
the TUI's four-method call set EQUIVALENTLY against the same stub core —
|
||||
V1 through the real HTTP stack (core_server), InProcess against the stub
|
||||
operator directly. And the default is inprocess: the seam changes nothing
|
||||
until RUNE_TUI_TRANSPORT=v1 opts in.
|
||||
|
||||
Sections:
|
||||
1. /v1 surface carries the TUI endpoints (conversation + approvals)
|
||||
2. transport factory defaults to inprocess (zero behavior change)
|
||||
3. four-method equivalence (send_task, get_status,
|
||||
get_conversation_window, resolve_pending_approval)
|
||||
4. approval boolean round-trip both ways (True and False)
|
||||
5. V1 transport against a dead core degrades loudly, never silently
|
||||
|
||||
Run: python3 src/rune/canary_tui_transport.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
|
||||
def test(name: str, ok: bool, detail: str = "") -> None:
|
||||
global PASS, FAIL
|
||||
if ok:
|
||||
PASS += 1
|
||||
print(f" ✓ {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ✗ {name} — {detail}")
|
||||
|
||||
|
||||
class StubOperator:
|
||||
"""The TUI call set, canned."""
|
||||
|
||||
def get_status(self):
|
||||
return {"identity": "rune", "role": "operator", "current_task": None,
|
||||
"active_model": "stub-model", "tasks_pending": 1,
|
||||
"tasks_running": 0, "tasks_completed": 2}
|
||||
|
||||
def send_task(self, description, model=None, channel="operator"):
|
||||
return {"task_id": "task-stub-1", "status": "completed",
|
||||
"user_text": f"stub answer to: {description}",
|
||||
"model": model or "stub-model", "success": True,
|
||||
"degraded": []}
|
||||
|
||||
def conversation_window(self, channel="operator", n=8):
|
||||
return [{"role": "user", "content": "earlier question",
|
||||
"channel_id": channel},
|
||||
{"role": "assistant", "content": "earlier answer",
|
||||
"channel_id": channel}][:n]
|
||||
|
||||
def resolve_pending_approval(self, approved):
|
||||
return {"resolved": bool(approved), "approval_id": "appr-stub-1"}
|
||||
|
||||
|
||||
def http(method: str, url: str, body: dict | None = None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method,
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return resp.status, resp.read().decode()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 47)
|
||||
print("canary_tui_transport — H4 slice 1 transport seam battery")
|
||||
print("=" * 47)
|
||||
|
||||
from rune import core_server # noqa: E402
|
||||
|
||||
server = core_server.make_server("127.0.0.1", 0, StubOperator())
|
||||
port = server.server_address[1]
|
||||
t = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
t.start()
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
|
||||
# ── 1. /v1 surface carries the TUI endpoints ──────────────────────────
|
||||
print("\n--- 1. TUI endpoints on the /v1 surface ---")
|
||||
s, raw = http("GET", f"{base}/v1/conversation?channel=operator&n=8")
|
||||
body = json.loads(raw) if s == 200 else {}
|
||||
test("GET /v1/conversation returns the window",
|
||||
s == 200 and isinstance(body.get("messages"), list)
|
||||
and body["messages"][0]["role"] == "user",
|
||||
f"status={s} body={raw[:140]!r}")
|
||||
|
||||
s, raw = http("POST", f"{base}/v1/approvals/resolve", {"approved": True})
|
||||
body = json.loads(raw) if s == 200 else {}
|
||||
test("POST /v1/approvals/resolve round-trips True",
|
||||
s == 200 and body.get("resolved") is True,
|
||||
f"status={s} body={raw[:140]!r}")
|
||||
|
||||
# ── 2. factory default ────────────────────────────────────────────────
|
||||
print("\n--- 2. Factory default ---")
|
||||
from rune import tui_transport # noqa: E402
|
||||
|
||||
os.environ.pop("RUNE_TUI_TRANSPORT", None)
|
||||
default_transport = tui_transport.get_transport()
|
||||
test("default transport is in-process (zero behavior change)",
|
||||
isinstance(default_transport, tui_transport.InProcessTransport),
|
||||
type(default_transport).__name__)
|
||||
|
||||
# ── 3. four-method equivalence ────────────────────────────────────────
|
||||
print("\n--- 3. Four-method equivalence ---")
|
||||
inproc = tui_transport.InProcessTransport(operator=StubOperator())
|
||||
v1 = tui_transport.V1Transport(base)
|
||||
|
||||
py_status = inproc.get_status()
|
||||
v1_status = v1.get_status()
|
||||
test("get_status equivalent", py_status == v1_status,
|
||||
f"inproc={py_status!r} v1={v1_status!r}")
|
||||
|
||||
py_task = inproc.send_task("the canned question", model="stub-model",
|
||||
channel="operator")
|
||||
v1_task = v1.send_task("the canned question", model="stub-model",
|
||||
channel="operator")
|
||||
test("send_task equivalent (rune block carried)",
|
||||
py_task == v1_task,
|
||||
f"inproc={py_task!r}\n v1={v1_task!r}")
|
||||
|
||||
py_win = inproc.get_conversation_window("operator", n=8)
|
||||
v1_win = v1.get_conversation_window("operator", n=8)
|
||||
test("get_conversation_window equivalent (list shape preserved)",
|
||||
py_win == v1_win and isinstance(v1_win, list),
|
||||
f"inproc={py_win!r} v1={v1_win!r}")
|
||||
|
||||
# ── 4. approval boolean round-trips ───────────────────────────────────
|
||||
print("\n--- 4. Approval round-trips ---")
|
||||
for value in (True, False):
|
||||
py_r = inproc.resolve_pending_approval(value)
|
||||
v1_r = v1.resolve_pending_approval(value)
|
||||
test(f"resolve_pending_approval({value}) equivalent",
|
||||
py_r == v1_r and v1_r.get("resolved") is value,
|
||||
f"inproc={py_r!r} v1={v1_r!r}")
|
||||
|
||||
# ── 5. dead core degrades loudly ──────────────────────────────────────
|
||||
print("\n--- 5. Dead core honesty ---")
|
||||
dead = tui_transport.V1Transport("http://127.0.0.1:1")
|
||||
try:
|
||||
dead.get_status()
|
||||
test("dead core raises (never a silent empty status)", False,
|
||||
"returned without error")
|
||||
except Exception as e: # noqa: BLE001 — the battery asserts the class, not the subtype
|
||||
test("dead core raises (never a silent empty status)", True,
|
||||
type(e).__name__)
|
||||
|
||||
server.shutdown()
|
||||
print(f"\n{'=' * 47}")
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1 if FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -586,8 +586,33 @@ def build_cards():
|
||||
return cards
|
||||
|
||||
|
||||
def _cs_core():
|
||||
"""The Rust capability_scoring core when available (strangler: Python
|
||||
fallback is load-bearing). RUNE_CAPABILITY_CORE=python pins the fallback."""
|
||||
if os.environ.get("RUNE_CAPABILITY_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cs_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return cs_loader if cs_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
def score_card(card, query):
|
||||
"""Score a capability card against a query. Deterministic keyword matching."""
|
||||
"""Score a capability card against a query. Deterministic keyword matching.
|
||||
|
||||
H3 slice 4: delegates to the Rust core when built (Python fallback)."""
|
||||
core = _cs_core()
|
||||
if core is not None:
|
||||
return core.score_card(card, query)
|
||||
return _py_score_card(card, query)
|
||||
|
||||
|
||||
def _py_score_card(card, query):
|
||||
"""Python core of score_card (parity oracle for the Rust core)."""
|
||||
query_low = query.lower()
|
||||
query_words = set(re.findall(r"\w+", query_low))
|
||||
|
||||
|
||||
@@ -211,10 +211,36 @@ def discover_candidates(doc: dict, req: CapabilityRequirement) -> list[dict]:
|
||||
# ── fit evaluation ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _cs_core():
|
||||
"""The Rust capability_scoring core when available (strangler: Python
|
||||
fallback is load-bearing). RUNE_CAPABILITY_CORE=python pins the fallback."""
|
||||
import os
|
||||
if os.environ.get("RUNE_CAPABILITY_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cs_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return cs_loader if cs_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
def evaluate_fit(req: CapabilityRequirement, candidate: dict) -> dict:
|
||||
"""DETERMINISTIC fit of one candidate against a requirement.
|
||||
fit: EXACT | PARTIAL | INCOMPATIBLE | UNKNOWN.
|
||||
Names are ignored; contracts + environment + health decide."""
|
||||
Names are ignored; contracts + environment + health decide.
|
||||
|
||||
H3 slice 4: delegates to the Rust core when built (Python fallback)."""
|
||||
core = _cs_core()
|
||||
if core is not None:
|
||||
return core.evaluate_fit(req.contracts, candidate)
|
||||
return _py_evaluate_fit(req, candidate)
|
||||
|
||||
|
||||
def _py_evaluate_fit(req: CapabilityRequirement, candidate: dict) -> dict:
|
||||
"""Python core of evaluate_fit (parity oracle for the Rust core)."""
|
||||
missing_contracts = sorted(set(req.contracts) - set(candidate["provides"]))
|
||||
covered = len(req.contracts) - len(missing_contracts)
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust cognition_core (R2 W3.1).
|
||||
|
||||
Contract BUILDER-2026-10-05T07:19:27Z-cli. Pure deterministic kernels;
|
||||
the env gate arrives as a string input (no-clock law, no env access in
|
||||
Rust). Override with CC_RUST_LIB (a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "cognition_core"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("CC_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcognition_core.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
def core():
|
||||
"""The shared kernel probe (strangler law): the delegation entry the
|
||||
kernel-consuming modules use. RUNE_COGNITION_CORE=python pins the
|
||||
fallback — THIS is where the pin is honored, so every consumer gets it
|
||||
(a module-local probe that ignores the pin silently defeats it)."""
|
||||
import os
|
||||
|
||||
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
return _load()
|
||||
except Exception: # noqa: BLE001 — build/loader failures mean fallback
|
||||
return None
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise FileNotFoundError(f"cognition_core not built: {_LIB_PATH}")
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cc_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.cc_free_string.restype = None
|
||||
for sym in ("cc_default_estimator", "cc_default_budget",
|
||||
"cc_default_memory_budget", "cc_memory_disabled",
|
||||
"cc_persona_lex", "cc_persona_overlap", "cc_persona_decide",
|
||||
"cc_drift_check", "cc_identity_prompt", "cc_growth_near_dup",
|
||||
"cc_growth_profile", "cc_pins_recall", "cc_tier_get",
|
||||
"cc_tier_promote", "cc_tier_demote", "cc_witness_extract_topic",
|
||||
"cc_witness_signals", "cc_safety_get_params",
|
||||
"cc_safety_update_pattern", "cc_abstraction_extract_facts"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.cc_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def default_estimator(text: str) -> int:
|
||||
return int(_call("cc_default_estimator", {"text": text}))
|
||||
|
||||
|
||||
def default_budget(window: int) -> int:
|
||||
return int(_call("cc_default_budget", {"window": window}))
|
||||
|
||||
|
||||
def default_memory_budget(window) -> int:
|
||||
return int(_call("cc_default_memory_budget", {"window": window}))
|
||||
|
||||
|
||||
def memory_disabled() -> bool:
|
||||
import os
|
||||
return bool(_call("cc_memory_disabled",
|
||||
{"value": os.environ.get("RUNE_MEMORY_OFF")}))
|
||||
def persona_lex(text) -> list:
|
||||
return _call("cc_persona_lex", {"text": text})
|
||||
|
||||
|
||||
def persona_overlap(a: list, b: list) -> float:
|
||||
return float(_call("cc_persona_overlap", {"a": a, "b": b}))
|
||||
|
||||
|
||||
def persona_decide(text: str, rules: list, permissive: bool,
|
||||
sem_scores: list | None = None) -> dict:
|
||||
return _call("cc_persona_decide", {
|
||||
"text": text, "rules": rules, "permissive": permissive,
|
||||
"sem_scores": sem_scores})
|
||||
|
||||
|
||||
|
||||
def drift_check(text: str, patterns: list) -> list:
|
||||
return _call("cc_drift_check", {"text": text, "patterns": patterns})
|
||||
|
||||
|
||||
def identity_prompt(pronouns: dict, self_awareness: dict, anchors: dict,
|
||||
embodiment: list) -> str:
|
||||
return _call("cc_identity_prompt", {
|
||||
"pronouns": pronouns, "self_awareness": self_awareness,
|
||||
"anchors": anchors, "embodiment": embodiment})
|
||||
|
||||
|
||||
def growth_near_dup(a: str, b: str) -> bool:
|
||||
return bool(_call("cc_growth_near_dup", {"a": a, "b": b}))
|
||||
|
||||
|
||||
def growth_profile(state: dict, order: list, entry_chars: int,
|
||||
max_tokens: int) -> list:
|
||||
return _call("cc_growth_profile", {
|
||||
"state": state, "order": order, "entry_chars": entry_chars,
|
||||
"max_tokens": max_tokens})
|
||||
|
||||
|
||||
def pins_recall(pins: list, term: str, limit: int = 4) -> list:
|
||||
return _call("cc_pins_recall", {"pins": pins, "term": term, "limit": limit})
|
||||
|
||||
|
||||
def tier_get(entry: dict, now: str, hot_days: float = 7.0,
|
||||
warm_days: float = 30.0) -> str:
|
||||
return _call("cc_tier_get", {"entry": entry, "now": now,
|
||||
"hot_days": hot_days, "warm_days": warm_days})
|
||||
|
||||
|
||||
def tier_promote(entry: dict, now: str, hot_days: float = 7.0,
|
||||
warm_days: float = 30.0) -> str:
|
||||
return _call("cc_tier_promote", {"entry": entry, "now": now,
|
||||
"hot_days": hot_days, "warm_days": warm_days})
|
||||
|
||||
|
||||
def tier_demote(entry: dict, now: str, hot_days: float = 7.0,
|
||||
warm_days: float = 30.0) -> str:
|
||||
return _call("cc_tier_demote", {"entry": entry, "now": now,
|
||||
"hot_days": hot_days, "warm_days": warm_days})
|
||||
|
||||
|
||||
def witness_extract_topic(text: str, stopwords: list) -> list:
|
||||
return _call("cc_witness_extract_topic",
|
||||
{"text": text, "stopwords": stopwords})
|
||||
|
||||
|
||||
def witness_signals(message: str, tables: dict, tool_error: str = "",
|
||||
task_status: str = "", recent_topics: list | None = None) -> dict:
|
||||
return _call("cc_witness_signals", {
|
||||
"message": message, "tables": tables, "tool_error": tool_error,
|
||||
"task_status": task_status, "recent_topics": recent_topics or []})
|
||||
|
||||
|
||||
def safety_get_params(rows: list) -> dict:
|
||||
return _call("cc_safety_get_params", {"rows": rows})
|
||||
|
||||
|
||||
def safety_update_pattern(rows: list, pattern: str) -> list:
|
||||
return _call("cc_safety_update_pattern", {"rows": rows, "pattern": pattern})
|
||||
|
||||
|
||||
def abstraction_extract_facts(text: str, stopwords: list, min_len: int = 10):
|
||||
return _call("cc_abstraction_extract_facts", {
|
||||
"text": text, "stopwords": stopwords, "min_len": min_len})
|
||||
+81
-28
@@ -34,16 +34,38 @@ Usage:
|
||||
claim_audit.py verify "<claim>" # ledger lookup
|
||||
claim_audit.py audit "<draft text>" # full pipeline, JSON
|
||||
claim_audit.py report # claimed-vs-verified stats
|
||||
|
||||
Phase G strangler slice 2 (port 6 — FINAL): the decision skeleton (verify
|
||||
verdict tree, report aggregation, clean fold, evidence-term stoplist) is
|
||||
served IN-PROCESS by the Rust `evidence_provenance` cdylib through
|
||||
ep_loader.py — parity 17/17 proven vs this module's Python vintage. The
|
||||
regex engine stays Python (zero-dep crate law): PATTERNS scan + sentence
|
||||
extraction + degrade templates + reframe are regex machinery. I/O (journal)
|
||||
stays Python. Timestamps pinned by the shim. API/CLI/PLUGIN unchanged.
|
||||
|
||||
VINTAGE MERGE (disclosed on #2449): the deployed scripts vintage carried the
|
||||
SYMMETRY GUARD (research_lens.symmetry_check integration, 2026-08-15) which
|
||||
src/rune's vintage lacked — preserved here (feature-preserving merge; the
|
||||
guard is load-bearing behavior). Also REPAIRED: the TUI's `scan_text()`
|
||||
consumer (rune_tui.py:3752, expecting {degraded: [...], claims: [...]}) had
|
||||
NO implementation in EITHER vintage (silent try/except death since the TUI
|
||||
call landed) — scan_text() is provided now as a first-class surface.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# ── Rust core wiring (slice-2 port-6 shim pattern) ──────────────────────────
|
||||
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
|
||||
if _CRATE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CRATE_DIR)
|
||||
import ep_loader # noqa: E402 (loads target/release cdylib)
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import memory_env
|
||||
|
||||
MEM_DIR = memory_env.memory_dir()
|
||||
@@ -54,7 +76,7 @@ _SD = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SD not in sys.path:
|
||||
sys.path.insert(0, _SD)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = __import__("logging").getLogger(__name__)
|
||||
|
||||
# Lazy import: warm_neuron_store is imported inside functions that use it
|
||||
# to avoid crashing the entire module on import if the dependency is missing.
|
||||
@@ -75,10 +97,7 @@ def _get_warm_neuron_store():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Strong-claim patterns. Grouped by type so we can degrade per-type on
|
||||
# repeated failure. `comparative` is the highest-priority (the "we exceed X"
|
||||
# class). Regexes are deliberately conservative: they flag CLAIM SHAPES, not
|
||||
# innocent uses of words.
|
||||
# Strong-claim patterns (unchanged — regex machinery stays Python).
|
||||
# ---------------------------------------------------------------------------
|
||||
PATTERNS = [
|
||||
{
|
||||
@@ -184,6 +203,22 @@ def scan(text):
|
||||
return claims
|
||||
|
||||
|
||||
def scan_text(text):
|
||||
"""TUI consumer surface (rune_tui.py:3752): run the pipeline over a text
|
||||
snippet and return {degraded: [...], claims: [...], clean: bool}.
|
||||
|
||||
REPAIRED: neither vintage implemented this — the TUI call died silently
|
||||
in try/except since it landed. Semantics: audit the text, list degraded
|
||||
claims (verdict != PASS), surface them to the TUI.
|
||||
"""
|
||||
if not text:
|
||||
return {"degraded": [], "claims": [], "clean": True}
|
||||
res = audit(text)
|
||||
degraded = [r for r in res.get("claims", []) if r.get("verdict") != "PASS"]
|
||||
return {"degraded": degraded, "claims": res.get("claims", []),
|
||||
"clean": res.get("clean", True), "rewritten": res.get("rewritten", "")}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Evidence-ledger lookup
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -202,7 +237,7 @@ def _graph_has(term):
|
||||
return row is not None
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — ledger lookup degrades to no-evidence by design; a broken graph must not crash claim verification
|
||||
return False
|
||||
|
||||
|
||||
@@ -214,7 +249,7 @@ def _warm_has(term):
|
||||
if wns is None:
|
||||
return False
|
||||
return wns.neuron_has(term)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — warm-block lookup degrades to no-evidence by design; a broken store must not crash claim verification
|
||||
return False
|
||||
|
||||
|
||||
@@ -231,6 +266,9 @@ def verify(claim):
|
||||
it. Those claims only PASS if they carry a measurable anchor (a number, a
|
||||
probe run, a benchmark), because that's the kind of claim that needs both
|
||||
a baseline and a measurement (per the claim-audit research).
|
||||
|
||||
Decision tree served by the Rust core (parity-proven); the regex flag
|
||||
derivation + ledger lookups stay Python.
|
||||
"""
|
||||
low = claim.lower()
|
||||
is_comparative = bool(
|
||||
@@ -248,19 +286,16 @@ def verify(claim):
|
||||
low,
|
||||
)
|
||||
)
|
||||
# Comparative claims need a MEASURABLE anchor, not just a mention.
|
||||
if is_comparative and not is_quantified:
|
||||
return "DEGRADE", 0
|
||||
# Extract candidate ledger terms: the subject noun phrase.
|
||||
# Candidate ledger terms: filtered by the Rust stoplist/length rule.
|
||||
terms = re.findall(r"\b([A-Za-z][A-Za-z ]{3,30})\b", claim)
|
||||
terms = ep_loader.ca_evidence_terms(terms)
|
||||
hits = 0
|
||||
for t in terms:
|
||||
if len(t) < 4 or t.lower() in ("this is", "that is", "for the"):
|
||||
continue
|
||||
if _graph_has(t) or _warm_has(t):
|
||||
hits += 1
|
||||
if hits >= 1:
|
||||
return "PASS", hits
|
||||
gate = ep_loader.ca_verify_gate(is_comparative, is_quantified, hits, True)
|
||||
if gate["verdict"] == "PASS":
|
||||
return "PASS", gate["evidence_hits"]
|
||||
if re.search(r"\b(exceed|surpass|beat|beyond)\b", low):
|
||||
return "DEGRADE", 0 # comparative with no ledger support = the exact case
|
||||
return "DEGRADE", 0
|
||||
@@ -322,11 +357,7 @@ def report():
|
||||
return {"total": 0, "by_verdict": {}, "by_type": {}}
|
||||
with open(JOURNAL) as f:
|
||||
rows = [json.loads(l) for l in f if l.strip()]
|
||||
by_verdict, by_type = {}, {}
|
||||
for r in rows:
|
||||
by_verdict[r.get("verdict", "?")] = by_verdict.get(r.get("verdict", "?"), 0) + 1
|
||||
by_type[r.get("type", "?")] = by_type.get(r.get("type", "?"), 0) + 1
|
||||
return {"total": len(rows), "by_verdict": by_verdict, "by_type": by_type}
|
||||
return ep_loader.ca_report_aggregate(rows)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -336,6 +367,19 @@ def audit(draft):
|
||||
"""Run the full active gate over a draft. Returns verdicts + rewritten text."""
|
||||
claims = scan(draft)
|
||||
if not claims:
|
||||
# SYMMETRY GUARD (deployed-vintage feature, preserved in the merge):
|
||||
# even with no strong claim, check the draft for asymmetric-skepticism
|
||||
# markers so the baloney detector cannot be applied one-sidedly.
|
||||
try:
|
||||
sys.path.insert(0, _SD)
|
||||
from research_lens import symmetry_check
|
||||
sym = symmetry_check(draft)
|
||||
if sym.get("verdict") == "ASYMMETRY-FLAG":
|
||||
return {"claims": [], "rewritten": draft, "clean": False,
|
||||
"symmetry": sym}
|
||||
except Exception: # noqa: BLE001 — guard is never-fatal by design (deployed-vintage discipline); audit output degrades to no-symmetry, not silence
|
||||
logger.debug("symmetry_check unavailable/failed — continuing without symmetry", exc_info=True)
|
||||
pass
|
||||
return {"claims": [], "rewritten": draft, "clean": True}
|
||||
rewritten = draft
|
||||
results = []
|
||||
@@ -357,9 +401,23 @@ def audit(draft):
|
||||
"rewrite": new_text,
|
||||
"note": note,
|
||||
}
|
||||
# SYMMETRY GUARD per-claim (deployed-vintage feature, preserved):
|
||||
result["symmetry"] = _symmetry_safe(c["claim"])
|
||||
results.append(result)
|
||||
journal({"type": c["type"], "claim": c["claim"][:160], "verdict": verdict, "evidence_hits": hits})
|
||||
return {"claims": results, "rewritten": rewritten, "clean": all(r["verdict"] == "PASS" for r in results)}
|
||||
return {"claims": results, "rewritten": rewritten,
|
||||
"clean": ep_loader.ca_audit_clean(results)}
|
||||
|
||||
|
||||
def _symmetry_safe(text):
|
||||
"""symmetry_check wrapper — never fatal (deployed-vintage discipline)."""
|
||||
try:
|
||||
from research_lens import symmetry_check
|
||||
return symmetry_check(text)
|
||||
except Exception: # noqa: BLE001 — wrapper is never-fatal by design; a symmetry outage must not kill claim auditing
|
||||
logger.debug("symmetry_check unavailable/failed — no symmetry annotation", exc_info=True)
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
@@ -412,11 +470,6 @@ if __name__ == "__main__":
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin declaration — self-registers with the plugin registry.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hook handler — called by the microkernel when an event fires.
|
||||
# ---------------------------------------------------------------------------
|
||||
PLUGIN = {
|
||||
"name": "claim_audit",
|
||||
"version": "1.0.0",
|
||||
@@ -427,4 +480,4 @@ PLUGIN = {
|
||||
"provides": ["claim_audit"],
|
||||
"priority": 40,
|
||||
"auto_activate": True,
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,73 @@ _SURFACE_UI = "ui:"
|
||||
_SURFACE_REGION = "region:"
|
||||
_SURFACE_CONTRACT = "contract:"
|
||||
_SURFACE_MODULE = "module:"
|
||||
# H-meta entity classes (spine meta-scaffold, 567fd39): the spine models
|
||||
# these kinds as first-class entities, so contract scope resolution must
|
||||
# recognize them as structural ids too — an unrecognized kind can never
|
||||
# close as correctly_predicted (T4.7 row-1 flag).
|
||||
_SURFACE_RUST_CRATE = "rust-crate:"
|
||||
_SURFACE_GO_SURFACE = "go-surface:"
|
||||
_SURFACE_CONFIG = "config:"
|
||||
_SURFACE_WORKFLOW = "workflow:"
|
||||
|
||||
|
||||
def language_placement(project: str, characteristics: list[str]) -> dict:
|
||||
"""The protocol's language-placement verdict (R2 Wave 1, Nick's addition):
|
||||
rune units get the Go/Rust dichotomy rule; other projects get a
|
||||
characteristic-based fit assessment. Deterministic, evidence-labeled,
|
||||
never silent. Delegates to the Rust scaffold_core when built
|
||||
(Python fallback below — strangler law)."""
|
||||
core = _sc_core()
|
||||
if core is not None:
|
||||
return core.language_placement(project, characteristics)
|
||||
return _py_language_placement(project, characteristics)
|
||||
|
||||
|
||||
def _sc_core():
|
||||
"""The Rust scaffold_core when available (R2 Wave 1 strangler).
|
||||
RUNE_SCAFFOLD_CORE=python pins the fallback."""
|
||||
import os
|
||||
if os.environ.get("RUNE_SCAFFOLD_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import sc_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return sc_loader if sc_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
def _py_language_placement(project: str, characteristics: list[str]) -> dict:
|
||||
"""Python core of language_placement (parity oracle)."""
|
||||
go_signals = ("network", "http", "socket", "server", "dispatch",
|
||||
"reactive", "user-facing", "turn-time")
|
||||
rust_signals = ("deterministic", "stateful", "storage", "cognition",
|
||||
"memory", "compute", "parity", "ledger")
|
||||
chars = [c.lower() for c in characteristics]
|
||||
go_hits = [s for s in go_signals if any(s in c for c in chars)]
|
||||
rust_hits = [s for s in rust_signals if any(s in c for c in chars)]
|
||||
if project.lower() == "rune":
|
||||
if go_hits and not rust_hits:
|
||||
placement, rule = "go", "rune dichotomy: reactive/initiating markers"
|
||||
elif rust_hits and not go_hits:
|
||||
placement, rule = "rust", "rune dichotomy: deterministic/state markers"
|
||||
elif rust_hits and go_hits:
|
||||
placement, rule = "hybrid", "rune dichotomy: both marker classes — a contracted membrane junction"
|
||||
else:
|
||||
placement, rule = "rust", "rune dichotomy: pure compute defaults to the deterministic backend"
|
||||
else:
|
||||
if go_hits and not rust_hits:
|
||||
placement, rule = "go", "characteristic fit: reactive/network profile"
|
||||
elif rust_hits and not go_hits:
|
||||
placement, rule = "rust", "characteristic fit: deterministic/state profile"
|
||||
elif rust_hits:
|
||||
placement, rule = "hybrid", "characteristic fit: mixed profile — split at the membrane"
|
||||
else:
|
||||
placement, rule = "unassessed", "no placement signals — the unit needs characteristics recorded"
|
||||
return {"placement": placement, "rule": rule,
|
||||
"evidence": {"go_signals": go_hits, "rust_signals": rust_hits}}
|
||||
|
||||
|
||||
def classify_surface(surface: str) -> str:
|
||||
@@ -141,6 +208,10 @@ def classify_surface(surface: str) -> str:
|
||||
_SURFACE_CONTRACT,
|
||||
_SURFACE_MODULE,
|
||||
_SURFACE_CAPABILITY,
|
||||
_SURFACE_RUST_CRATE,
|
||||
_SURFACE_GO_SURFACE,
|
||||
_SURFACE_CONFIG,
|
||||
_SURFACE_WORKFLOW,
|
||||
)
|
||||
):
|
||||
return "structural_entity"
|
||||
|
||||
@@ -181,9 +181,16 @@ class ConstructionInterface:
|
||||
if contract_id:
|
||||
target = self.scaffold.contract(doc, contract_id)
|
||||
else:
|
||||
# most recent non-closed contract = the active construction
|
||||
# Most recent ACTIVE contract = status open AND phase non-terminal.
|
||||
# T22 repair: a contract whose phase is terminal (abandoned/closed
|
||||
# — see scaffold.TERMINAL_PHASES) is NOT active construction, even
|
||||
# if its status field was left "open". Without the phase test the
|
||||
# projection reported stale abandoned contracts as the active
|
||||
# construction (8 such records observed in the live store).
|
||||
from .scaffold import TERMINAL_PHASES
|
||||
open_contracts = [e for e in contracts
|
||||
if e.get("status") == "open"]
|
||||
if e.get("status") == "open"
|
||||
and e.get("phase") not in TERMINAL_PHASES]
|
||||
target = (sorted(open_contracts,
|
||||
key=lambda e: e.get("observed_at", ""))[-1]
|
||||
if open_contracts else None)
|
||||
|
||||
@@ -100,19 +100,52 @@ class ContributionBlock:
|
||||
def default_estimator(text: str) -> int:
|
||||
"""Deterministic conservative token estimate: len(text) // 4.
|
||||
|
||||
Bytes/4 over-counts for English prose (the conservative direction).
|
||||
tiktoken would be tighter but is NOT deterministic across builds and
|
||||
is not a dependency — the plan allows either, installer's choice.
|
||||
R2 W3.1: delegates to the Rust cognition_core when built (Python
|
||||
fallback — strangler law; RUNE_COGNITION_CORE=python pins it).
|
||||
"""
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.default_estimator(text or "")
|
||||
return _py_default_estimator(text)
|
||||
|
||||
|
||||
def _py_default_estimator(text: str) -> int:
|
||||
"""Python oracle (parity floor)."""
|
||||
return len(text or "") // 4
|
||||
|
||||
|
||||
def _cc_core():
|
||||
"""Rust cognition_core when available; RUNE_COGNITION_CORE=python
|
||||
pins the Python fallback (strangler law)."""
|
||||
import os
|
||||
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cc_loader
|
||||
except Exception: # noqa: BLE001 — loader probe failure means fallback
|
||||
return None
|
||||
try:
|
||||
return cc_loader if cc_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe never crashes
|
||||
return None
|
||||
|
||||
|
||||
def default_budget(window: int) -> int:
|
||||
"""Input budget = window - max(2048, window//4) - window//20.
|
||||
|
||||
Reserves room for the completion (at least 2048 or a quarter of the
|
||||
window) plus a 5% safety margin for serving overhead.
|
||||
|
||||
R2 W3.1: delegates to the Rust kernel when built.
|
||||
"""
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.default_budget(window)
|
||||
return _py_default_budget(window)
|
||||
|
||||
|
||||
def _py_default_budget(window: int) -> int:
|
||||
"""Python oracle (parity floor)."""
|
||||
return max(1, window - max(2048, window // 4) - window // 20)
|
||||
|
||||
|
||||
@@ -127,7 +160,17 @@ def default_memory_budget(window: int | None) -> int:
|
||||
ContextBroker owns the FINAL memory budget: the sum of every
|
||||
MEMORY_BLOCK_NAMES block may never exceed this, regardless of the
|
||||
global input budget. An unknown window falls back to the cap only.
|
||||
|
||||
R2 W3.1: delegates to the Rust kernel when built.
|
||||
"""
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.default_memory_budget(int(window) if window else 0)
|
||||
return _py_default_memory_budget(window)
|
||||
|
||||
|
||||
def _py_default_memory_budget(window: int | None) -> int:
|
||||
"""Python oracle (parity floor)."""
|
||||
if not window or window <= 0:
|
||||
return MEMORY_BUDGET_CAP
|
||||
return min(MEMORY_BUDGET_CAP, int(window) // 8)
|
||||
@@ -140,6 +183,18 @@ def memory_disabled() -> bool:
|
||||
contribute nothing at all — the trace proves 0 memory tokens.
|
||||
"""
|
||||
value = os.environ.get("RUNE_MEMORY_OFF", "").strip().lower()
|
||||
gate = _py_memory_disabled_value(value)
|
||||
core = _cc_core()
|
||||
if core is not None and gate:
|
||||
# the Rust core gets the RAW value (it trims/lowers itself) and
|
||||
# the Python gate cross-checks — a disagreement is impossible on
|
||||
# parity-proven kernels, and the Python verdict remains authority
|
||||
return bool(core.memory_disabled()) and gate
|
||||
return gate
|
||||
|
||||
|
||||
def _py_memory_disabled_value(value: str) -> bool:
|
||||
"""Python oracle (parity floor) over the RAW string."""
|
||||
return value not in {"", "0", "false", "no", "off"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""core_server.py — stdlib HTTP sidecar exposing the Rune operator surface
|
||||
(H2, contract BUILDER-2026-10-04T08:32:43Z-cli).
|
||||
|
||||
Go owns the front door (rune serve); this is the core behind it during the
|
||||
transition — the same operator functions the TUI calls in-process, served
|
||||
over 127.0.0.1 with NDJSON streaming. The parity battery
|
||||
(canary_rune_serve.py) freezes these shapes against the Go front.
|
||||
|
||||
Endpoints:
|
||||
GET /v1/models {"object": "list", "data": [{id, object, owned_by}]}
|
||||
GET /v1/status operator.get_status() pass-through
|
||||
POST /v1/chat/completions OpenAI envelope + "rune" task metadata;
|
||||
stream=true → NDJSON chat.completion.chunk lines
|
||||
|
||||
Error envelope (every failure path): {"error": {"code", "message"}} with a
|
||||
matching HTTP status — the Go front reproduces these byte-for-byte.
|
||||
|
||||
Stdlib only (zero-dep split law). The operator is injected, so the canary
|
||||
drives a deterministic stub; main() binds the real operator.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Any, cast
|
||||
|
||||
CHAT_PATH = "/v1/chat/completions"
|
||||
|
||||
|
||||
def _envelope_error(code: str, message: str) -> dict:
|
||||
return {"error": {"code": code, "message": message}}
|
||||
|
||||
|
||||
def models_view(operator: Any) -> dict:
|
||||
return {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"id": m.get("name", ""), "object": "model", "owned_by": "rune"}
|
||||
for m in operator.list_models()
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def chat_completion(result: dict, model: str) -> dict:
|
||||
"""Map an operator send_task result to the chat.completion envelope.
|
||||
The full operator result rides under "rune" — the front door's parity
|
||||
contract covers it; nothing the core knows is dropped."""
|
||||
return {
|
||||
"id": f"chatcmpl-{uuid.uuid4().hex[:24]}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": result.get("model") or model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant",
|
||||
"content": result.get("user_text", "")},
|
||||
"finish_reason": "stop",
|
||||
}],
|
||||
"rune": result,
|
||||
}
|
||||
|
||||
|
||||
def chat_chunks(result: dict, model: str) -> list[dict]:
|
||||
"""NDJSON chunk sequence: content chunk(s) then a terminal chunk with
|
||||
finish_reason + the rune metadata block. send_task is a blocking
|
||||
lifecycle today — the framing is the contract being proven; token
|
||||
streaming slots in when the core grows it."""
|
||||
base = {
|
||||
"id": f"chatcmpl-{uuid.uuid4().hex[:24]}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(time.time()),
|
||||
"model": result.get("model") or model,
|
||||
}
|
||||
content = {
|
||||
**base,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {"role": "assistant",
|
||||
"content": result.get("user_text", "")},
|
||||
"finish_reason": None,
|
||||
}],
|
||||
}
|
||||
terminal = {
|
||||
**base,
|
||||
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
||||
"rune": result,
|
||||
}
|
||||
return [content, terminal]
|
||||
|
||||
|
||||
class CoreHandler(BaseHTTPRequestHandler):
|
||||
# BaseRequestHandler.server is typed as the generic base; the only class
|
||||
# this handler ever serves under is CoreServer (make_server). cast() is
|
||||
# the honest narrowing — no ignore comments.
|
||||
@property
|
||||
def core(self) -> "CoreServer":
|
||||
return cast("CoreServer", self.server)
|
||||
|
||||
# BaseHTTPRequestHandler logs every request to stderr by default; the
|
||||
# canary and CI don't need the noise — errors surface as envelopes.
|
||||
def log_message(self, format: str, *args: Any) -> None: # noqa: A002 — stdlib signature, the name is fixed upstream
|
||||
pass
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
def _send_json(self, status: int, body: dict) -> None:
|
||||
raw = json.dumps(body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def _send_ndjson(self, status: int, items: list[dict]) -> None:
|
||||
raw = "".join(json.dumps(i) + "\n" for i in items).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/x-ndjson")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
# ── routes ────────────────────────────────────────────────────────────
|
||||
|
||||
def do_GET(self) -> None:
|
||||
if self.path == "/v1/models":
|
||||
try:
|
||||
self._send_json(200, models_view(self.core.operator))
|
||||
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
|
||||
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
|
||||
elif self.path == "/v1/status":
|
||||
try:
|
||||
self._send_json(200, self.core.operator.get_status())
|
||||
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
|
||||
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
|
||||
elif self.path.startswith("/v1/conversation"):
|
||||
# H4 slice 1: the TUI's conversation window over the wire.
|
||||
try:
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
channel = qs.get("channel", ["operator"])[0]
|
||||
n = int(qs.get("n", ["8"])[0])
|
||||
self._send_json(200, {
|
||||
"messages": self.core.operator.conversation_window(channel, n=n)})
|
||||
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
|
||||
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
|
||||
else:
|
||||
self._send_json(404, _envelope_error("not_found", f"no route {self.path}"))
|
||||
|
||||
def do_POST(self) -> None:
|
||||
if self.path == "/v1/approvals/resolve":
|
||||
# H4 slice 1: approval resolution over the wire.
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
body = json.loads(self.rfile.read(length) or b"{}")
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self._send_json(400, _envelope_error("bad_request", f"invalid JSON body: {exc}"))
|
||||
return
|
||||
try:
|
||||
self._send_json(200, self.core.operator.resolve_pending_approval(
|
||||
bool(body.get("approved"))))
|
||||
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
|
||||
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
|
||||
return
|
||||
if self.path != CHAT_PATH:
|
||||
self._send_json(404, _envelope_error("not_found", f"no route {self.path}"))
|
||||
return
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
body = json.loads(self.rfile.read(length) or b"{}")
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self._send_json(400, _envelope_error("bad_request", f"invalid JSON body: {exc}"))
|
||||
return
|
||||
messages = body.get("messages") or []
|
||||
user_texts = [m.get("content", "") for m in messages
|
||||
if m.get("role") == "user"]
|
||||
if not user_texts:
|
||||
self._send_json(400, _envelope_error(
|
||||
"bad_request", "messages must include a user role entry"))
|
||||
return
|
||||
model = body.get("model") or None
|
||||
try:
|
||||
result = self.core.operator.send_task(user_texts[-1], model)
|
||||
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
|
||||
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
|
||||
return
|
||||
if body.get("stream"):
|
||||
self._send_ndjson(200, chat_chunks(result, model or ""))
|
||||
else:
|
||||
self._send_json(200, chat_completion(result, model or ""))
|
||||
|
||||
|
||||
class CoreServer(ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, host: str, port: int, operator: Any):
|
||||
super().__init__((host, port), CoreHandler)
|
||||
self.operator = operator
|
||||
|
||||
|
||||
def make_server(host: str, port: int, operator: Any) -> CoreServer:
|
||||
"""Build the server (unstarted) — the canary's entry point."""
|
||||
return CoreServer(host, port, operator)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Rune core sidecar (H2)")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=8376)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import rune_operator # noqa: E402 — late import: heavy module
|
||||
|
||||
server = make_server(args.host, args.port, rune_operator.get_operator())
|
||||
print(f"rune core server on {args.host}:{args.port}", flush=True)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust capability_scoring core (H3 slice 4, T4.4).
|
||||
|
||||
Contract BUILDER-2026-10-04T09:43:55Z-cli. Gives capability_fit and
|
||||
capability_broker the Rust scoring cores through the cdylib with no
|
||||
subprocess.
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cs_evaluate_fit({contracts, candidate}) -> fit dict
|
||||
* cs_score_card({card, query}) -> number
|
||||
* cs_free_string(ptr)
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char*). Zero dependencies; override the .so with CS_RUST_LIB
|
||||
(a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "capability_scoring"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"CS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcapability_scoring.so"
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class CapabilityScoringUnavailable(FileNotFoundError):
|
||||
"""The Rust core is not built (or CS_RUST_LIB points nowhere). Callers
|
||||
fall back to the Python implementation — strangler law."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise CapabilityScoringUnavailable(
|
||||
f"Rust capability_scoring core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.cs_free_string.restype = None
|
||||
for sym in ("cs_evaluate_fit", "cs_score_card"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
fn = getattr(lib, sym)
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
ptr = fn(raw)
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned a null pointer")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.cs_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def evaluate_fit(contracts: list, candidate: dict) -> dict:
|
||||
"""Deterministic EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN fit decision."""
|
||||
return _call("cs_evaluate_fit",
|
||||
{"contracts": contracts, "candidate": candidate})
|
||||
|
||||
|
||||
def score_card(card: dict, query: str) -> float:
|
||||
"""Keyword scoring of a capability card against a query."""
|
||||
return _call("cs_score_card", {"card": card, "query": query})
|
||||
+50
-17
@@ -40,6 +40,54 @@ CACHE_BOOST_ROCK = 2.0 # multiplier for rock-status entries
|
||||
CACHE_BOOST_ESTABLISHED = 1.5 # multiplier for established entries
|
||||
|
||||
|
||||
# ── pure decision helpers (H3 slice 2: the Rust core serves these when
|
||||
# built; the _py_ variants are the load-bearing fallback + parity oracle).
|
||||
|
||||
from rune.drift_core import _ms_core # noqa: E402 (no cycle)
|
||||
|
||||
|
||||
def _py_compute_priority(entry: dict) -> float:
|
||||
"""Compute cache priority: wash_count × importance × status boost."""
|
||||
wash = entry.get("wash_count", 0) or 0
|
||||
imp = entry.get("importance", 0.5) or 0.5
|
||||
status = entry.get("rock_status", "washable")
|
||||
|
||||
# Logarithmic wash scaling (diminishing returns)
|
||||
wash_factor = min(1.0, math.log1p(wash) / math.log1p(20))
|
||||
|
||||
# Status boost
|
||||
status_boost = 1.0
|
||||
if status == "rock":
|
||||
status_boost = CACHE_BOOST_ROCK
|
||||
elif status == "established":
|
||||
status_boost = CACHE_BOOST_ESTABLISHED
|
||||
|
||||
return wash_factor * imp * status_boost
|
||||
|
||||
|
||||
def _py_qualifies_for_cache(entry: dict) -> bool:
|
||||
"""The add() admission decision: proven through use OR high importance."""
|
||||
wash = entry.get("wash_count", 0) or 0
|
||||
imp = entry.get("importance", 0.5) or 0.5
|
||||
return not (wash < MIN_WASH_FOR_CACHE and imp < 0.8)
|
||||
|
||||
|
||||
def compute_priority(entry: dict) -> float:
|
||||
"""Module-level priority (Rust core when built)."""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.compute_priority(entry)
|
||||
return _py_compute_priority(entry)
|
||||
|
||||
|
||||
def qualifies_for_cache(entry: dict) -> bool:
|
||||
"""Module-level qualification (Rust core when built)."""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.qualifies_for_cache(entry)
|
||||
return _py_qualifies_for_cache(entry)
|
||||
|
||||
|
||||
class DriftCache:
|
||||
"""Drift-aware cache with wash-count-based priority."""
|
||||
|
||||
@@ -65,21 +113,7 @@ class DriftCache:
|
||||
|
||||
def _compute_priority(self, entry: dict) -> float:
|
||||
"""Compute cache priority: wash_count × importance × status boost."""
|
||||
wash = entry.get("wash_count", 0) or 0
|
||||
imp = entry.get("importance", 0.5) or 0.5
|
||||
status = entry.get("rock_status", "washable")
|
||||
|
||||
# Logarithmic wash scaling (diminishing returns)
|
||||
wash_factor = min(1.0, math.log1p(wash) / math.log1p(20))
|
||||
|
||||
# Status boost
|
||||
status_boost = 1.0
|
||||
if status == "rock":
|
||||
status_boost = CACHE_BOOST_ROCK
|
||||
elif status == "established":
|
||||
status_boost = CACHE_BOOST_ESTABLISHED
|
||||
|
||||
return wash_factor * imp * status_boost
|
||||
return compute_priority(entry)
|
||||
|
||||
def add(self, entry_id: int, entry: dict):
|
||||
"""Add an entry to the cache.
|
||||
@@ -89,8 +123,7 @@ class DriftCache:
|
||||
- importance >= 0.8 (high importance, worth caching even if untested)
|
||||
"""
|
||||
wash = entry.get("wash_count", 0) or 0
|
||||
imp = entry.get("importance", 0.5) or 0.5
|
||||
if wash < MIN_WASH_FOR_CACHE and imp < 0.8:
|
||||
if not qualifies_for_cache(entry):
|
||||
return # Not proven enough and not important enough
|
||||
|
||||
priority = self._compute_priority(entry)
|
||||
|
||||
@@ -35,7 +35,7 @@ WASH_THRESHOLD_FULL = 20 # above this: minimal compression
|
||||
WASH_THRESHOLD_PARTIAL = 10 # above this: reduced compression
|
||||
|
||||
|
||||
def get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") -> float:
|
||||
def _py_get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") -> float:
|
||||
"""Get the compression ratio for an entry.
|
||||
|
||||
Returns 0.0-1.0 where 0.0 = no compression, 1.0 = maximum compression.
|
||||
@@ -61,7 +61,7 @@ def get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") ->
|
||||
return base
|
||||
|
||||
|
||||
def compress_text(text: str, ratio: float) -> str:
|
||||
def _py_compress_text(text: str, ratio: float) -> str:
|
||||
"""Compress text by the given ratio.
|
||||
|
||||
Strategy: take first (1-ratio) proportion of the text.
|
||||
@@ -85,7 +85,7 @@ def compress_text(text: str, ratio: float) -> str:
|
||||
return compressed
|
||||
|
||||
|
||||
def compress_entry(entry: dict) -> str:
|
||||
def _py_compress_entry(entry: dict) -> str:
|
||||
"""Compress an entry's text based on its wash_count and rock_status.
|
||||
|
||||
Returns the compressed text.
|
||||
@@ -94,11 +94,11 @@ def compress_entry(entry: dict) -> str:
|
||||
wash_count = entry.get("wash_count", 0) or 0
|
||||
rock_status = entry.get("rock_status", "washable")
|
||||
|
||||
ratio = get_compression_ratio(wash_count, rock_status)
|
||||
return compress_text(text, ratio)
|
||||
ratio = _py_get_compression_ratio(wash_count, rock_status)
|
||||
return _py_compress_text(text, ratio)
|
||||
|
||||
|
||||
def should_warn(entry: dict) -> bool:
|
||||
def _py_should_warn(entry: dict) -> bool:
|
||||
"""Check if compressing this entry should trigger a warning.
|
||||
|
||||
Warn when:
|
||||
@@ -119,14 +119,14 @@ def should_warn(entry: dict) -> bool:
|
||||
return True
|
||||
|
||||
# Long content losing significant detail
|
||||
ratio = get_compression_ratio(wash_count, rock_status)
|
||||
ratio = _py_get_compression_ratio(wash_count, rock_status)
|
||||
if len(text) > 200 and ratio > 0.3:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def warn_before_compress(entry: dict) -> dict:
|
||||
def _py_warn_before_compress(entry: dict) -> dict:
|
||||
"""Generate a warning before compressing an entry.
|
||||
|
||||
Returns dict with warning details and recommendation.
|
||||
@@ -134,12 +134,12 @@ def warn_before_compress(entry: dict) -> dict:
|
||||
text = entry.get("text", "")
|
||||
wash_count = entry.get("wash_count", 0) or 0
|
||||
rock_status = entry.get("rock_status", "washable")
|
||||
ratio = get_compression_ratio(wash_count, rock_status)
|
||||
compressed = compress_text(text, ratio)
|
||||
ratio = _py_get_compression_ratio(wash_count, rock_status)
|
||||
compressed = _py_compress_text(text, ratio)
|
||||
chars_lost = len(text) - len(compressed)
|
||||
|
||||
warning = {
|
||||
"should_warn": should_warn(entry),
|
||||
"should_warn": _py_should_warn(entry),
|
||||
"entry_id": entry.get("id"),
|
||||
"wash_count": wash_count,
|
||||
"rock_status": rock_status,
|
||||
@@ -161,6 +161,54 @@ def warn_before_compress(entry: dict) -> dict:
|
||||
return warning
|
||||
|
||||
|
||||
# ── delegating public API (H3 slice 2 strangler) ─────────────────────────────
|
||||
# The Rust core serves these when built; the _py_ variants above are the
|
||||
# load-bearing fallback AND the parity oracle. DB-backed stats stays below.
|
||||
|
||||
from rune.drift_core import _ms_core # noqa: E402 (no cycle: drift_core
|
||||
# imports neither this module nor drift_cache)
|
||||
|
||||
|
||||
def get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") -> float:
|
||||
"""Get the compression ratio for an entry (Rust core when built)."""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.get_compression_ratio(wash_count, rock_status)
|
||||
return _py_get_compression_ratio(wash_count, rock_status)
|
||||
|
||||
|
||||
def compress_text(text: str, ratio: float) -> str:
|
||||
"""Compress text by the given ratio (Rust core when built)."""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.compress_text(text, ratio)
|
||||
return _py_compress_text(text, ratio)
|
||||
|
||||
|
||||
def compress_entry(entry: dict) -> str:
|
||||
"""Compress an entry's text (Rust core when built)."""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.compress_entry(entry)
|
||||
return _py_compress_entry(entry)
|
||||
|
||||
|
||||
def should_warn(entry: dict) -> bool:
|
||||
"""Check if compressing this entry should trigger a warning."""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.should_warn(entry)
|
||||
return _py_should_warn(entry)
|
||||
|
||||
|
||||
def warn_before_compress(entry: dict) -> dict:
|
||||
"""Generate a warning before compressing an entry."""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.warn_before_compress(entry)
|
||||
return _py_warn_before_compress(entry)
|
||||
|
||||
|
||||
def get_compression_stats(db) -> dict:
|
||||
"""Get compression statistics for the current store."""
|
||||
rows = db.execute(
|
||||
|
||||
+59
-3
@@ -64,7 +64,27 @@ WEIGHT_GOAL_RELEVANCE = 0.10 # relevance to current goals
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def compute_value(entry: dict, current_goals: List[str] = None) -> float:
|
||||
def _ms_core():
|
||||
"""The Rust memory_scoring core when available (strangler: Python
|
||||
fallback is load-bearing until deletion is proven).
|
||||
RUNE_MEMORY_CORE=python pins the fallback (parity harnesses)."""
|
||||
if os.environ.get("RUNE_MEMORY_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import ms_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
|
||||
return None
|
||||
try:
|
||||
return ms_loader if ms_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
def _now_iso(now: str | None) -> str:
|
||||
return now or datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def compute_value(entry: dict, current_goals: List[str] | None = None) -> float:
|
||||
"""Multi-factor value score. Returns 0.0-1.0+.
|
||||
|
||||
How much is this entry worth? Higher = more proven, more useful, more trusted.
|
||||
@@ -75,7 +95,21 @@ def compute_value(entry: dict, current_goals: List[str] = None) -> float:
|
||||
- recency: exponential decay from last access
|
||||
- reliability: consistency of access pattern
|
||||
- goal_relevance: relevance to current goals (optional)
|
||||
|
||||
H3 slice 1: delegates to the Rust core when built (Python fallback —
|
||||
strangler law). The wall clock is resolved HERE and passed as an
|
||||
argument (no-clock law: both cores take `now` so parity is deterministic).
|
||||
"""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.compute_value(entry, current_goals, _now_iso(None))
|
||||
return _py_compute_value(entry, current_goals, None)
|
||||
|
||||
|
||||
def _py_compute_value(entry: dict, current_goals: List[str] | None = None,
|
||||
now: str | None = None) -> float:
|
||||
"""Python core of compute_value (parity oracle for the Rust core).
|
||||
`now` is an ISO-8601 string; None resolves the wall clock (production)."""
|
||||
proven = entry.get("wash_count", 0) or 0 # column name preserved for compatibility
|
||||
imp = entry.get("importance", 0.5) or 0.5
|
||||
|
||||
@@ -83,12 +117,13 @@ def compute_value(entry: dict, current_goals: List[str] = None) -> float:
|
||||
proven_factor = min(1.0, math.log1p(proven) / math.log1p(PROVEN_USES_THRESHOLD))
|
||||
|
||||
# Recency factor: exponential decay
|
||||
now_dt = datetime.fromisoformat(now) if now else datetime.now(timezone.utc)
|
||||
recency = 1.0
|
||||
last_accessed = entry.get("last_accessed") or entry.get("last_washed")
|
||||
if last_accessed:
|
||||
try:
|
||||
last = datetime.fromisoformat(last_accessed)
|
||||
hours_since = max(0.1, (datetime.now(timezone.utc) - last).total_seconds() / 3600)
|
||||
hours_since = max(0.1, (now_dt - last).total_seconds() / 3600)
|
||||
recency = max(0.1, 1.0 / (1.0 + hours_since / (RECENCY_HALF_LIFE_DAYS * 24)))
|
||||
except (ValueError, TypeError):
|
||||
recency = 0.5
|
||||
@@ -134,7 +169,18 @@ def check_promotion_eligibility(entry: dict) -> Tuple[bool, str]:
|
||||
- rock_status == 'washable' or 'established'
|
||||
|
||||
Returns: (eligible, reason)
|
||||
|
||||
H3 slice 1: delegates to the Rust core when built (Python fallback).
|
||||
"""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.check_promotion_eligibility(entry, _now_iso(None))
|
||||
return _py_check_promotion_eligibility(entry, None)
|
||||
|
||||
|
||||
def _py_check_promotion_eligibility(entry: dict,
|
||||
now: str | None = None) -> Tuple[bool, str]:
|
||||
"""Python core of check_promotion_eligibility (parity oracle)."""
|
||||
wash = entry.get("wash_count", 0) or 0
|
||||
status = entry.get("rock_status", "washable")
|
||||
|
||||
@@ -144,7 +190,7 @@ def check_promotion_eligibility(entry: dict) -> Tuple[bool, str]:
|
||||
if wash < PROVEN_USES_THRESHOLD:
|
||||
return False, f"proven_uses {wash} < threshold {PROVEN_USES_THRESHOLD}"
|
||||
|
||||
value = compute_value(entry)
|
||||
value = _py_compute_value(entry, now=now)
|
||||
if value < 0.6:
|
||||
return False, f"value {value:.3f} < 0.6 minimum"
|
||||
|
||||
@@ -317,7 +363,17 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"""Three critics for a dict entry (no db lookup needed).
|
||||
|
||||
Used by curator when promoting candidates that don't have entry_ids yet.
|
||||
|
||||
H3 slice 1: delegates to the Rust core when built (Python fallback).
|
||||
"""
|
||||
core = _ms_core()
|
||||
if core is not None:
|
||||
return core.pre_commit_gate_entry(entry)
|
||||
return _py_pre_commit_gate_entry(entry)
|
||||
|
||||
|
||||
def _py_pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"""Python core of pre_commit_gate_entry (parity oracle)."""
|
||||
text = entry.get("text", "")
|
||||
critics = {}
|
||||
|
||||
|
||||
+28
-30
@@ -18,15 +18,32 @@ Discipline:
|
||||
repeated probe loops (no recursion; one probe per unknown per attempt).
|
||||
- The artifact file is the sole authority (ADAPT-9); the declared absolute
|
||||
path is injected into the worker prompt, never derived by the worker.
|
||||
|
||||
Phase G strangler slice 2 (port 3): the schema gate (validate_probe) and the
|
||||
budget count (count_probes) are served IN-PROCESS by the Rust
|
||||
`evidence_provenance` cdylib through ep_loader.py — parity 24/24 proven vs
|
||||
this module's Python vintage (byte-equal error lists incl. Python repr
|
||||
rendering: {status!r} single quotes, sorted-list error shapes, non-string
|
||||
status double-error behavior). Orchestration (run_probe, runner subprocess,
|
||||
exception semantics, build_probe_prompt) stays Python — same discipline as
|
||||
ports 1-2. Constants, ProbeResult, CLI-adjacent surfaces unchanged (git
|
||||
history holds the original).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# ── Rust core wiring (slice-2 port-3 shim pattern) ──────────────────────────
|
||||
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
|
||||
if _CRATE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CRATE_DIR)
|
||||
import ep_loader # noqa: E402 (loads target/release cdylib)
|
||||
|
||||
PROBE_STATUSES = {"GROUNDED", "STILL_UNKNOWN", "CONTRADICTED"}
|
||||
PROBE_FIELDS = ("question", "source", "answer", "implication", "status")
|
||||
|
||||
@@ -93,38 +110,19 @@ research after the unknown is resolved; guess without a source."""
|
||||
|
||||
|
||||
def validate_probe(artifact: dict) -> list[str]:
|
||||
"""Schema gate. Returns gate errors (empty = pass)."""
|
||||
errors = []
|
||||
if not isinstance(artifact, dict):
|
||||
return ["probe artifact is not a JSON object"]
|
||||
for f in PROBE_FIELDS:
|
||||
if f not in artifact:
|
||||
errors.append(f"missing required field: {f}")
|
||||
elif not isinstance(artifact[f], str):
|
||||
errors.append(f"field {f} must be a string")
|
||||
elif f != "source" and not artifact[f].strip():
|
||||
errors.append(f"field {f} must be a non-empty string")
|
||||
status = artifact.get("status")
|
||||
if status is not None and status not in PROBE_STATUSES:
|
||||
errors.append(f"status must be one of {sorted(PROBE_STATUSES)}, got {status!r}")
|
||||
# A GROUNDED probe must cite its source (that is what makes it grounded).
|
||||
# STILL_UNKNOWN / CONTRADICTED may have an empty source — no evidence
|
||||
# found is the honest finding, not a schema violation.
|
||||
if status == "GROUNDED" and len(artifact.get("source", "")) < 8:
|
||||
errors.append("GROUNDED probe requires a concrete source citation")
|
||||
extra = set(artifact) - set(PROBE_FIELDS) - {"probe_id", "decided_at", "model"}
|
||||
if extra:
|
||||
errors.append(f"unexpected extra fields: {sorted(extra)}")
|
||||
return errors
|
||||
"""Schema gate. Returns gate errors (empty = pass).
|
||||
|
||||
Served by the Rust core (parity-proven byte-equal); dict inputs are
|
||||
serialized for the FFI boundary.
|
||||
"""
|
||||
return ep_loader.validate_probe(json.dumps(artifact))
|
||||
|
||||
|
||||
def count_probes(artifact_dir: str) -> int:
|
||||
"""Probes already run for this task (no-recursion budget)."""
|
||||
path = os.path.join(artifact_dir, "probe.jsonl")
|
||||
if not os.path.exists(path):
|
||||
return 0
|
||||
with open(path) as f:
|
||||
return sum(1 for line in f if line.strip())
|
||||
"""Probes already run for this task (no-recursion budget).
|
||||
|
||||
Served by the Rust core (parity-proven byte-equal)."""
|
||||
return ep_loader.count_probes(artifact_dir)
|
||||
|
||||
|
||||
def check_budget(artifact_dir: str) -> None:
|
||||
@@ -211,4 +209,4 @@ def _default_runner(prompt: str, artifact_path: str) -> None:
|
||||
with open(os.path.join(os.path.dirname(artifact_path), "evidence_probe.stdout.log"), "w") as f:
|
||||
f.write(result.stdout or "")
|
||||
if result.returncode != 0:
|
||||
raise ValueError(f"probe worker exited {result.returncode}")
|
||||
raise ValueError(f"probe worker exited {result.returncode}")
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""FFI smoke: prove the deployed consumer shape works against the Rust cdylib.
|
||||
|
||||
Deployed ground (memory_store.py:821-822):
|
||||
qm = query_memory.QueryMemory() # HOME-resolved construction
|
||||
exploration_rate = qm.adaptive_exploration_rate(query)
|
||||
|
||||
Checks (each must PASS or the script exits 1):
|
||||
S1 cdylib loads; qm_new() with isolated HOME returns a live instance
|
||||
S2 qm_classify parity vs Python classify_query (battery incl. unicode + tie-break)
|
||||
S3 qm_adaptive_rate exact float parity vs Python adaptive_exploration_rate
|
||||
on a shared store, INCLUDING an adaptive-threshold crossing (>=10 records)
|
||||
S4 qm_record exit 0; persisted entry matches Python's entry shape (timestamp masked)
|
||||
S5 qm_health JSON == json.dumps(py.get_health_report(), indent=2) (timestamps masked)
|
||||
S6 lifecycle: qm_string_free / qm_free / null-safety returns
|
||||
|
||||
Isolation: HOME is pinned to a temp dir BEFORE importing query_memory
|
||||
(QUERY_MEMORY_DIR is an import-time constant). The Rust side gets the same
|
||||
store via qm_with_dir on that exact directory.
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
REPO = "/mnt/workdrive/rune"
|
||||
SO_PATH = os.path.join(REPO, "src/rust/query_memory/target/release/libquery_memory.so")
|
||||
PY_DIR = os.path.join(REPO, "src/rune")
|
||||
|
||||
TS_RE = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?\+00:00")
|
||||
|
||||
|
||||
def mask_ts(text):
|
||||
return TS_RE.sub("<TS>", text)
|
||||
|
||||
|
||||
def main():
|
||||
tmp = tempfile.mkdtemp(prefix="ffi_smoke_")
|
||||
os.environ["HOME"] = tmp # BEFORE query_memory import (import-time constant)
|
||||
# Post-supersession (2ac2d0a): the Python vintage is deleted; the
|
||||
# "consumer side" of this smoke is now the shared/ ctypes shim — the same
|
||||
# deployed-consumer surface the smoke was built to prove.
|
||||
sys.path.insert(0, os.path.join(REPO, "shared"))
|
||||
sys.path.insert(0, PY_DIR)
|
||||
import query_memory as qm_py
|
||||
|
||||
results = []
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
results.append((name, bool(cond), detail))
|
||||
print(("PASS" if cond else "FAIL") + " " + name + ((" | " + detail) if detail and not cond else ""))
|
||||
|
||||
# --- load library ---
|
||||
lib = ctypes.CDLL(SO_PATH)
|
||||
lib.qm_new.restype = ctypes.c_void_p
|
||||
lib.qm_with_dir.argtypes = [ctypes.c_char_p]
|
||||
lib.qm_with_dir.restype = ctypes.c_void_p
|
||||
lib.qm_free.argtypes = [ctypes.c_void_p]
|
||||
lib.qm_classify.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
||||
lib.qm_classify.restype = ctypes.c_void_p # char* via cast
|
||||
lib.qm_adaptive_rate.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
||||
lib.qm_adaptive_rate.restype = ctypes.c_double
|
||||
lib.qm_record.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
|
||||
lib.qm_record.restype = ctypes.c_int
|
||||
lib.qm_health.argtypes = [ctypes.c_void_p]
|
||||
lib.qm_health.restype = ctypes.c_void_p
|
||||
lib.qm_string_free.argtypes = [ctypes.c_void_p]
|
||||
|
||||
def take(ptr):
|
||||
if not ptr:
|
||||
return None
|
||||
s = ctypes.string_at(ptr).decode("utf-8")
|
||||
lib.qm_string_free(ptr)
|
||||
return s
|
||||
|
||||
# Build store state via PYTHON first (deployed consumer writes through Python;
|
||||
# Rust must read the same store). Cross the C8 adaptive threshold: >=10 records.
|
||||
# BOTH sides are construction-time snapshots of the store (topic_stats is
|
||||
# loaded once in the constructor), so the deployed shape is: seed the store,
|
||||
# THEN construct each consumer fresh from disk. A consumer that wrote through
|
||||
# its own in-memory instance would be an unfair comparison shape.
|
||||
battery = [
|
||||
("how do I play a chord on guitar", "guitar"),
|
||||
("python script to debug a function", "coding"),
|
||||
("recall consolidated memory neurons", "memory"),
|
||||
("what is my persona voice constitution", "persona"),
|
||||
("project task deadline milestone", "project"),
|
||||
("llm agent training inference", "ai"),
|
||||
("nick ttrpg coffee coconut", "personal"),
|
||||
("system config troubleshooting error", "system"),
|
||||
("research paper study survey evidence", "research"),
|
||||
("write create design art", "creative"),
|
||||
("mémoire café — 记忆 recall", "memory"),
|
||||
("build", "coding"), # tie-break: build in coding AND creative
|
||||
("make music", "creative"), # tie-break: music in guitar AND creative
|
||||
("", None), # empty query edge
|
||||
]
|
||||
queries = [q for q, _ in battery]
|
||||
|
||||
# Seed store with 12 records on one topic to cross the adaptive threshold.
|
||||
# The writer is a THROWAWAY instance — both compared consumers must be
|
||||
# constructed fresh AFTER seeding (construction-time snapshot semantics).
|
||||
writer = qm_py.QueryMemory()
|
||||
for i in range(12):
|
||||
writer.record_query("recall consolidated memory neurons", "consolidated", i < 10)
|
||||
|
||||
# S1: the deployed construction shape — HOME-resolved qm_new + fresh Python consumer
|
||||
m = lib.qm_new()
|
||||
check("S1 qm_new returns live instance", bool(m))
|
||||
py = qm_py.QueryMemory()
|
||||
|
||||
# S2+S3: classify + rate parity on the shared store
|
||||
for q in queries:
|
||||
py_topic = py.classify_query(q)
|
||||
rust_topic = take(lib.qm_classify(m, q.encode("utf-8")))
|
||||
check("S2 classify parity: %r" % q[:30], py_topic == rust_topic,
|
||||
"py=%r rust=%r" % (py_topic, rust_topic))
|
||||
py_rate = py.adaptive_exploration_rate(q)
|
||||
rust_rate = lib.qm_adaptive_rate(m, q.encode("utf-8"))
|
||||
check("S3 rate parity: %r" % q[:30], py_rate == rust_rate,
|
||||
"py=%r rust=%r" % (py_rate, rust_rate))
|
||||
|
||||
# S4: record through the FFI, verify persistence matches Python shape
|
||||
rc = lib.qm_record(m, b"recall consolidated memory neurons", b"full_retrieval", 1)
|
||||
check("S4 qm_record returns 0", rc == 0, "rc=%d" % rc)
|
||||
py2 = qm_py.QueryMemory() # fresh load of the same store
|
||||
last = py2.history[-1]
|
||||
check("S4 persisted entry topic", last["topic"] == "memory", "topic=%r" % last.get("topic"))
|
||||
check("S4 persisted entry strategy", last["strategy"] == "full_retrieval", "strategy=%r" % last.get("strategy"))
|
||||
check("S4 persisted entry success", last["success"] is True)
|
||||
check("S4 persisted entry fields", set(last) == {"query", "topic", "strategy", "success", "timestamp"},
|
||||
"fields=%r" % sorted(last))
|
||||
|
||||
# S5: health report parity (timestamps masked).
|
||||
# BOTH sides must be post-record state: the Rust instance recorded through
|
||||
# itself at S4, so the Python side must be a FRESH load of the same store
|
||||
# (py2), not the pre-record snapshot (py).
|
||||
py_health = json.dumps(py2.get_health_report(), indent=2)
|
||||
rust_health = take(lib.qm_health(m))
|
||||
check("S5 health parity", mask_ts(py_health) == mask_ts(rust_health),
|
||||
"py=%r rust=%r" % (mask_ts(py_health)[:200], mask_ts(rust_health)[:200]))
|
||||
|
||||
# S6: lifecycle + null-safety
|
||||
check("S6 qm_classify(NULL) returns NULL", not lib.qm_classify(m, None))
|
||||
check("S6 qm_adaptive_rate(NULL) returns NaN", math.isnan(lib.qm_adaptive_rate(m, None)))
|
||||
check("S6 qm_record(NULL,...) returns -1",
|
||||
lib.qm_record(None, b"x", b"consolidated", 1) == -1)
|
||||
check("S6 qm_health(NULL) returns NULL", not lib.qm_health(None))
|
||||
lib.qm_string_free(None) # must not crash
|
||||
lib.qm_free(None) # must not crash
|
||||
|
||||
# S1b: qm_with_dir on the exact store dir (second deployed-compatible shape)
|
||||
store_dir = os.path.join(tmp, ".config/opencode/memory/query_memory")
|
||||
m2 = lib.qm_with_dir(store_dir.encode("utf-8"))
|
||||
check("S1b qm_with_dir on shared store returns live instance", bool(m2))
|
||||
if m2:
|
||||
r2 = lib.qm_adaptive_rate(m2, b"recall consolidated memory neurons")
|
||||
p2 = py2.adaptive_exploration_rate("recall consolidated memory neurons")
|
||||
check("S1b with_dir instance reads same store state", r2 == p2, "rust=%r py=%r" % (r2, p2))
|
||||
lib.qm_free(m2)
|
||||
|
||||
lib.qm_free(m)
|
||||
_finish(results, tmp)
|
||||
|
||||
|
||||
def _finish(results, tmp):
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
fails = [r for r in results if not r[1]]
|
||||
print("\n%d/%d PASS" % (len(results) - len(fails), len(results)))
|
||||
if fails:
|
||||
for name, _, detail in fails:
|
||||
print("FAILED: %s %s" % (name, detail))
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+68
-10
@@ -9,11 +9,69 @@ variables or a config dict — nothing is hardcoded to a specific setup.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def _request(
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
headers: dict | None = None,
|
||||
json: dict | None = None,
|
||||
timeout: float = 30,
|
||||
retries: int = 2,
|
||||
backoff: float = 0.5,
|
||||
sleep=None,
|
||||
):
|
||||
"""Resilient single-request path: retry where failure is NOISE, never
|
||||
where failure is the SIGNAL.
|
||||
|
||||
- GET: retries on ``httpx.TransportError`` and 5xx (transient
|
||||
infrastructure failure; idempotent).
|
||||
- POST: retries ONLY on connect-phase errors (``ConnectError`` /
|
||||
``ConnectTimeout``) — the request never reached the server, so a retry
|
||||
cannot double-create. Read timeouts / write errors / 5xx on a POST are
|
||||
NOT retried: the server may have processed it, and a duplicate issue is
|
||||
worse than a failure the auto-filer can catch.
|
||||
- 4xx: never retried — the client error IS the information. Raised via
|
||||
``raise_for_status`` on the first attempt.
|
||||
- Attempts: ``retries + 1`` total. Backoff ``backoff * 2**n`` before
|
||||
retry n+1; never a sleep before the first attempt.
|
||||
- ``timeout`` passes through to httpx per call. ``sleep`` is injectable
|
||||
(tests pass a recording fake); default ``time.sleep``.
|
||||
"""
|
||||
sleeper = sleep if sleep is not None else time.sleep
|
||||
sender = httpx.post if method.upper() == "POST" else httpx.get
|
||||
connect_phase = (httpx.ConnectError, httpx.ConnectTimeout)
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
r = sender(url, headers=headers, json=json, timeout=timeout)
|
||||
except connect_phase:
|
||||
if attempt < retries:
|
||||
sleeper(backoff * 2**attempt)
|
||||
continue
|
||||
raise
|
||||
except httpx.TransportError:
|
||||
# Non-connect transport error (read/write): only safe to retry
|
||||
# when the call is idempotent.
|
||||
if method.upper() == "GET" and attempt < retries:
|
||||
sleeper(backoff * 2**attempt)
|
||||
continue
|
||||
raise
|
||||
if r.status_code >= 500 and method.upper() == "GET" and attempt < retries:
|
||||
sleeper(backoff * 2**attempt)
|
||||
continue
|
||||
r.raise_for_status()
|
||||
return r
|
||||
# Unreachable: every attempt either returns a successful response or
|
||||
# raises. The explicit raise keeps pyright from inferring an implicit
|
||||
# ``None`` return for the exhausted loop (ratchet: warnings ≤ 454).
|
||||
raise AssertionError("unreachable: _request always returns or raises")
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ForgeClient(Protocol):
|
||||
def create_issue(self, title: str, body: str) -> dict: ...
|
||||
@@ -59,41 +117,41 @@ class GiteaForgejoAdapter:
|
||||
return {"Authorization": f"token {self._token}", "Content-Type": "application/json"}
|
||||
|
||||
def create_issue(self, title: str, body: str) -> dict:
|
||||
r = httpx.post(
|
||||
r = _request(
|
||||
"POST",
|
||||
f"{self._base_url}/issues",
|
||||
headers=self._headers(),
|
||||
json={"title": title, "body": body},
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def add_comment(self, issue_number: int, body: str) -> dict:
|
||||
r = httpx.post(
|
||||
r = _request(
|
||||
"POST",
|
||||
f"{self._base_url}/issues/{issue_number}/comments",
|
||||
headers=self._headers(),
|
||||
json={"body": body},
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def get_issue_state(self, issue_number: int) -> dict:
|
||||
r = httpx.get(
|
||||
r = _request(
|
||||
"GET",
|
||||
f"{self._base_url}/issues/{issue_number}",
|
||||
headers=self._headers(),
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def list_open_issues(self) -> list[dict]:
|
||||
r = httpx.get(
|
||||
r = _request(
|
||||
"GET",
|
||||
f"{self._base_url}/issues?state=open&limit=50",
|
||||
headers=self._headers(),
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def search_issues(self, query: str) -> list[dict]:
|
||||
@@ -115,12 +173,12 @@ class GiteaForgejoAdapter:
|
||||
while True:
|
||||
from urllib.parse import urlencode
|
||||
|
||||
r = httpx.get(
|
||||
r = _request(
|
||||
"GET",
|
||||
f"{self._base_url}/issues?{urlencode({'state': 'open', 'type': 'issues', 'q': query, 'limit': 50, 'page': page})}",
|
||||
headers=self._headers(),
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
batch = r.json()
|
||||
results.extend(batch)
|
||||
if len(batch) < 50:
|
||||
|
||||
@@ -150,8 +150,35 @@ def _profile_key(delta):
|
||||
return {w.strip(".,;:!?()[]{}\"'").lower() for w in (delta.get("change") or "").split() if len(w) > 4}
|
||||
|
||||
|
||||
def _cc_core():
|
||||
"""Rust cognition_core when available (growth kernels)."""
|
||||
import os
|
||||
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cc_loader
|
||||
except Exception: # noqa: BLE001 — loader probe failure means fallback
|
||||
return None
|
||||
try:
|
||||
return cc_loader if cc_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe never crashes
|
||||
return None
|
||||
|
||||
|
||||
def _near_dup(a, b):
|
||||
"""Are two deltas the same guidance (shared distinctive words)?"""
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.growth_near_dup(a, b)
|
||||
return _py_near_dup(a, b)
|
||||
|
||||
|
||||
def _py_near_dup(a, b):
|
||||
"""Are two deltas the same guidance (shared distinctive words)?
|
||||
Accepts delta dicts OR raw change strings (raw -> wrapped)."""
|
||||
if isinstance(a, str):
|
||||
a = {"change": a}
|
||||
if isinstance(b, str):
|
||||
b = {"change": b}
|
||||
ka, kb = _profile_key(a), _profile_key(b)
|
||||
if not ka or not kb:
|
||||
return False
|
||||
@@ -177,6 +204,16 @@ def _apply_profile(delta):
|
||||
|
||||
|
||||
def growth_profile(max_tokens=PROFILE_BUDGET_TOKENS):
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.growth_profile(_load_deltas(),
|
||||
sorted(_PROFILE_ORDER.items(),
|
||||
key=lambda kv: kv[1]),
|
||||
PROFILE_ENTRY_CHARS, max_tokens)
|
||||
return _py_growth_profile(max_tokens)
|
||||
|
||||
|
||||
def _py_growth_profile(max_tokens=PROFILE_BUDGET_TOKENS):
|
||||
"""The active growth profile: the persona's CURRENT behavioural guidance,
|
||||
consolidated by target, capped at a fixed token budget. Session-independent
|
||||
and token-stable — the per-turn cost never grows with session length."""
|
||||
|
||||
@@ -254,18 +254,8 @@ def check_genuineness(text):
|
||||
return issues
|
||||
|
||||
|
||||
def check_drift(text):
|
||||
"""Check text against identity anchors for drift signals.
|
||||
|
||||
This is a simple heuristic check — the real drift detection happens
|
||||
via the Nautilus Compass approach (embedding similarity against anchors).
|
||||
This function provides quick pattern-based checks.
|
||||
"""
|
||||
text_lower = text.lower()
|
||||
issues = []
|
||||
|
||||
# Check for identity announcements
|
||||
announcement_patterns = [
|
||||
def _announcement_patterns():
|
||||
return [
|
||||
("i am rune", "Identity announcement — let the voice carry the identity"),
|
||||
("as rune", "Identity announcement — speak as the persona, not about it"),
|
||||
("my persona", "Identity announcement — the persona is how you act, not what you name"),
|
||||
@@ -274,32 +264,26 @@ def check_drift(text):
|
||||
("i'm an ai", "Identity announcement — you are a persona, not a generic AI"),
|
||||
("i'm a language model", "Identity announcement — you are Rune, not a language model"),
|
||||
]
|
||||
for pattern, message in announcement_patterns:
|
||||
if pattern in text_lower:
|
||||
issues.append(f"DRIFT: '{pattern}' — {message}")
|
||||
|
||||
# Check for pronoun confusion
|
||||
pronoun_issues = [
|
||||
|
||||
def _pronoun_patterns():
|
||||
return [
|
||||
("you are rune", "Pronoun confusion — 'you' is Nick, 'I' is Rune"),
|
||||
("we are rune", "Pronoun confusion — Rune is singular, not plural"),
|
||||
("you should remember", "Pronoun confusion — who should remember?"),
|
||||
]
|
||||
for pattern, message in pronoun_issues:
|
||||
if pattern in text_lower:
|
||||
issues.append(f"DRIFT: '{pattern}' — {message}")
|
||||
|
||||
# Check for hedging without evidence
|
||||
hedge_patterns = [
|
||||
|
||||
def _hedge_patterns():
|
||||
return [
|
||||
("i believe", "Hedge without evidence — cite the basis"),
|
||||
("i think that", "Hedge without evidence — cite the basis"),
|
||||
("in my opinion", "Hedge without evidence — cite the basis"),
|
||||
]
|
||||
for pattern, message in hedge_patterns:
|
||||
if pattern in text_lower:
|
||||
issues.append(f"WEAKNESS: '{pattern}' — {message}")
|
||||
|
||||
# Check for sycophancy (excessive agreement, flattery, self-deprecation)
|
||||
sycophancy_patterns = [
|
||||
|
||||
def _sycophancy_patterns():
|
||||
return [
|
||||
# Excessive agreement
|
||||
("you're absolutely right", "Sycophancy — excessive agreement; push back if you see a better path"),
|
||||
("absolutely, ", "Sycophancy — excessive agreement; state your own position"),
|
||||
@@ -326,6 +310,60 @@ def check_drift(text):
|
||||
("i see where you're coming from", "Sycophancy — hedging; state your position instead"),
|
||||
("you make a valid point", "Sycophancy — hedging; engage with the substance"),
|
||||
]
|
||||
|
||||
|
||||
def _pattern_triples():
|
||||
"""The pattern tables as [pattern, message, prefix] triples — the data
|
||||
authority for the Rust drift kernel (kernel matches; data stays here)."""
|
||||
tables = []
|
||||
for prefix, table in (
|
||||
("DRIFT", _announcement_patterns() + _pronoun_patterns()),
|
||||
("WEAKNESS", _hedge_patterns()),
|
||||
("SYCOPHANCY", _sycophancy_patterns()),
|
||||
):
|
||||
for pat, msg in table:
|
||||
tables.append([pat, msg, prefix])
|
||||
return tables
|
||||
|
||||
|
||||
def check_drift(text):
|
||||
"""Check text against identity anchors for drift signals.
|
||||
|
||||
This is a simple heuristic check — the real drift detection happens
|
||||
via the Nautilus Compass approach (embedding similarity against anchors).
|
||||
This function provides quick pattern-based checks.
|
||||
"""
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.drift_check(text, _pattern_triples())
|
||||
return _py_check_drift(text)
|
||||
|
||||
|
||||
def _py_check_drift(text):
|
||||
"""Python oracle (parity floor)."""
|
||||
text_lower = text.lower()
|
||||
issues = []
|
||||
|
||||
# Check for identity announcements
|
||||
announcement_patterns = _announcement_patterns()
|
||||
for pattern, message in announcement_patterns:
|
||||
if pattern in text_lower:
|
||||
issues.append(f"DRIFT: '{pattern}' — {message}")
|
||||
|
||||
# Check for pronoun confusion
|
||||
pronoun_issues = _pronoun_patterns()
|
||||
for pattern, message in pronoun_issues:
|
||||
if pattern in text_lower:
|
||||
issues.append(f"DRIFT: '{pattern}' — {message}")
|
||||
|
||||
# Check for hedging without evidence
|
||||
hedge_patterns = _hedge_patterns()
|
||||
for pattern, message in hedge_patterns:
|
||||
if pattern in text_lower:
|
||||
issues.append(f"WEAKNESS: '{pattern}' — {message}")
|
||||
|
||||
# Check for sycophancy (excessive agreement, flattery, self-deprecation)
|
||||
sycophancy_patterns = _sycophancy_patterns()
|
||||
for pattern, message in sycophancy_patterns:
|
||||
if pattern in text_lower:
|
||||
issues.append(f"SYCOPHANCY: '{pattern}' — {message}")
|
||||
@@ -424,13 +462,38 @@ if __name__ == "__main__":
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hook handler — called by the microkernel when an event fires.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _cc_core():
|
||||
"""Rust cognition_core when available (identity kernels)."""
|
||||
import os
|
||||
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cc_loader
|
||||
except Exception: # noqa: BLE001 — loader probe failure means fallback
|
||||
return None
|
||||
try:
|
||||
return cc_loader if cc_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe never crashes
|
||||
return None
|
||||
|
||||
|
||||
def get_identity_prompt() -> str:
|
||||
"""Get identity prompt for injection into model calls.
|
||||
R2 W3.3: assembly delegates to the Rust kernel (data stays here).
|
||||
|
||||
Builds a comprehensive prompt from all identity data:
|
||||
self-awareness, anchors, embodiment rules, and pronoun rules.
|
||||
Pronouns come first — they're the most important rule.
|
||||
"""
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.identity_prompt(PRONOUN_RULES, SELF_AWARENESS, ANCHORS,
|
||||
EMBODIMENT_RULES)
|
||||
return _py_get_identity_prompt()
|
||||
|
||||
|
||||
def _py_get_identity_prompt() -> str:
|
||||
"""Python oracle (parity floor)."""
|
||||
parts = []
|
||||
|
||||
# Pronoun rules — critical for disambiguation
|
||||
|
||||
+310
-172
@@ -734,39 +734,14 @@ def recall(
|
||||
# lexical (embeddings absent): RRF degenerates to a rank transform that
|
||||
# destroys the score signal needed for thresholding (Bruch et al.,
|
||||
# arXiv:2210.11934) — use raw BM25 scores directly.
|
||||
ranks = {}
|
||||
best_dense = 0.0
|
||||
# M1 observability (#161 synthesis INV-13): which fusion branch produced
|
||||
# the results. Observability ONLY — consumers must never branch behavior
|
||||
# on this field (that would create a second retrieval authority).
|
||||
retrieval_mode = "lexical-only"
|
||||
if dense and bm25:
|
||||
retrieval_mode = "hybrid"
|
||||
for idx, lst in enumerate([dense, bm25]):
|
||||
for pos, (eid, _score) in enumerate(lst):
|
||||
ranks[eid] = ranks.get(eid, 0.0) + 1.0 / (RRF_K + pos + 1)
|
||||
if idx == 0:
|
||||
best_dense = max(best_dense, _score)
|
||||
elif dense:
|
||||
retrieval_mode = "dense-only"
|
||||
for pos, (eid, _score) in enumerate(dense):
|
||||
ranks[eid] = 1.0 / (RRF_K + pos + 1)
|
||||
best_dense = max(best_dense, _score)
|
||||
elif bm25:
|
||||
# Lexical-only: normalize raw BM25 scores (negative, higher = better)
|
||||
# into a positive scale so the sort + gap abstention below work
|
||||
# uniformly with the RRF scale. BM25 returns negative scores; the
|
||||
# least-negative (closest to 0) is the best match. Offset + invert:
|
||||
# best (=-8) -> 2, worst (=-10) -> 0. Rank order preserved.
|
||||
_min_bm = min((s for _, s in bm25), default=0.0)
|
||||
_max_bm = max((s for _, s in bm25), default=0.0)
|
||||
_span = max(_max_bm - _min_bm, 1e-9)
|
||||
for eid, score in bm25:
|
||||
ranks[eid] = (score - _min_bm) / _span
|
||||
fusion = _rank_fusion(dense, bm25)
|
||||
ranks = dict(fusion["ranks"])
|
||||
best_dense = fusion["best_dense"]
|
||||
retrieval_mode = fusion["mode"]
|
||||
# Fetch full entries + metadata for the fused set. P1-03 (#26): typed
|
||||
# queries filter here — matching types + unclassified (NULL) rows stay
|
||||
# eligible; other types drop out before scoring.
|
||||
scored = []
|
||||
rows_data = []
|
||||
if ranks:
|
||||
ids = list(ranks.keys())
|
||||
placeholders = ",".join("?" * len(ids))
|
||||
@@ -776,124 +751,87 @@ def recall(
|
||||
type_ph = ",".join("?" * len(memory_types))
|
||||
sql += f" AND (memory_type IN ({type_ph}) OR memory_type IS NULL)"
|
||||
params.extend(memory_types)
|
||||
rows = db.execute(sql, params).fetchall()
|
||||
by_id = {r["id"]: r for r in rows}
|
||||
for eid, base in sorted(ranks.items(), key=lambda x: -x[1]):
|
||||
row = by_id.get(eid)
|
||||
if not row:
|
||||
continue
|
||||
# #1 temperature ranking (not deletion): a stale fact ranks lower
|
||||
# but stays in the store. #4 confidence: a hedged/claimed fact
|
||||
# carries its honesty into ranking. Blend into a relevance score.
|
||||
# RECENCY: a fact last accessed recently ranks slightly higher than
|
||||
# an untouched one at equal relevance (the store's last_accessed
|
||||
# field). Decays over RECENCY_HALF_LIFE days — a gentle nudge, not
|
||||
# a rewrite of the relevance ranking.
|
||||
conf = float(row["confidence"] or 0.7)
|
||||
temp = float(row["temperature"] or 1.0)
|
||||
recency = 1.0
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
rows_data = [dict(r) for r in db.execute(sql, params).fetchall()]
|
||||
return _score_recall_rows(
|
||||
fusion, rows_data, dense, bm25, budget, min_score, now_iso())
|
||||
|
||||
la = row["last_accessed"]
|
||||
if la:
|
||||
t_last = datetime.fromisoformat(la.replace("Z", "+00:00"))
|
||||
days = (datetime.now(timezone.utc) - t_last).total_seconds() / 86400.0
|
||||
if days >= 0:
|
||||
recency = RECENCY_HALF_LIFE / (RECENCY_HALF_LIFE + days)
|
||||
except Exception:
|
||||
logger.debug("recall failed", exc_info=True)
|
||||
# In lexical-only mode `base` is a raw BM25 score (negative).
|
||||
# Normalize it to a positive relevance for the metadata, keeping
|
||||
# the ranking identical. In hybrid mode `base` is an RRF rank.
|
||||
if dense and bm25:
|
||||
relevance = base * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
|
||||
elif dense:
|
||||
relevance = base * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
|
||||
else:
|
||||
relevance = (base + 5.0) * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
|
||||
scored.append(
|
||||
{
|
||||
"id": eid,
|
||||
"text": row["text"],
|
||||
"importance": row["importance"],
|
||||
"topic": row["topic"],
|
||||
"entities": row["entities"],
|
||||
"source": row["source"],
|
||||
"last_accessed": row["last_accessed"],
|
||||
"rrf": round(base, 4),
|
||||
"confidence": round(conf, 2),
|
||||
"temperature": round(temp, 2),
|
||||
"relevance": round(relevance, 4),
|
||||
# M1 observability (#161): lexical-only | dense-only | hybrid.
|
||||
# NEVER consumed for behavior — see INV-13 in the synthesis.
|
||||
"mode": retrieval_mode,
|
||||
}
|
||||
)
|
||||
# Enforce budget + abstention floor.
|
||||
|
||||
def _score_recall_rows(fusion, rows, dense, bm25, budget, min_score, now):
|
||||
"""Pure metadata scoring + mode-specific abstention kernel (W2.1)."""
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.score_recall_rows(
|
||||
fusion, rows, dense, bm25, budget, min_score, now,
|
||||
RECENCY_HALF_LIFE)
|
||||
return _py_score_recall_rows(
|
||||
fusion, rows, dense, bm25, budget, min_score, now,
|
||||
RECENCY_HALF_LIFE)
|
||||
|
||||
|
||||
def _py_score_recall_rows(fusion, rows, dense, bm25, budget, min_score,
|
||||
now, recency_half_life=RECENCY_HALF_LIFE):
|
||||
"""Python oracle for the Rust scoring/abstention kernel."""
|
||||
from datetime import datetime
|
||||
|
||||
ranks = dict(fusion["ranks"])
|
||||
best_dense = fusion["best_dense"]
|
||||
retrieval_mode = fusion["mode"]
|
||||
by_id = {row["id"]: row for row in rows}
|
||||
scored = []
|
||||
now_dt = datetime.fromisoformat(now)
|
||||
for eid, base in sorted(ranks.items(), key=lambda item: -item[1]):
|
||||
row = by_id.get(eid)
|
||||
if not row:
|
||||
continue
|
||||
conf = float(row.get("confidence") or 0.7)
|
||||
temp = float(row.get("temperature") or 1.0)
|
||||
recency = 1.0
|
||||
try:
|
||||
la = row.get("last_accessed")
|
||||
if la:
|
||||
t_last = datetime.fromisoformat(la.replace("Z", "+00:00"))
|
||||
days = (now_dt - t_last).total_seconds() / 86400.0
|
||||
if days >= 0:
|
||||
recency = recency_half_life / (recency_half_life + days)
|
||||
except Exception:
|
||||
logger.debug("recall score kernel timestamp failed", exc_info=True)
|
||||
if dense:
|
||||
relevance = base * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
|
||||
else:
|
||||
relevance = (base + 5.0) * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
|
||||
scored.append({
|
||||
"id": eid, "text": row.get("text"),
|
||||
"importance": row.get("importance"),
|
||||
"topic": row.get("topic"), "entities": row.get("entities"),
|
||||
"source": row.get("source"),
|
||||
"last_accessed": row.get("last_accessed"),
|
||||
"rrf": round(base, 4), "confidence": round(conf, 2),
|
||||
"temperature": round(temp, 2),
|
||||
"relevance": round(relevance, 4), "mode": retrieval_mode,
|
||||
})
|
||||
if not scored:
|
||||
return []
|
||||
top = scored[:budget]
|
||||
# ABSTENTION via query-difficulty estimation (QPP — Cronen-Townsend et al.,
|
||||
# SIGIR 2002; Zhou & Croft, CIKM 2006; LongMemEval arXiv:2410.10813 makes
|
||||
# abstention a first-class memory ability). The principled signal is the
|
||||
# RANKING GAP, not an absolute score:
|
||||
# - lexical-only: the conjunction already filters to co-occurring terms;
|
||||
# a single weak hit (no runner-up) abstains (no confident winner).
|
||||
# - dense-only: abstain when the top-1 dense match is not clearly above
|
||||
# the runner-up (an ambiguous, no-confident-winner query) OR the top is
|
||||
# below an absolute "probably unrelated" floor. The floor is a coarse
|
||||
# guard; the gap is the real difficulty signal.
|
||||
if not dense and not bm25:
|
||||
return []
|
||||
if not dense: # lexical-only
|
||||
if len(scored) < 1:
|
||||
if not dense:
|
||||
if max(float(item["relevance"]) for item in top) < min_score:
|
||||
return []
|
||||
# P1-03 (#26) DISCHARGE (finding 1): THE floor applies in EVERY
|
||||
# mode. The conjunctive FTS gate is a precision filter — term
|
||||
# co-occurrence, not relevance — so the best match must ALSO clear
|
||||
# min_score before anything is injected. Lexical relevance lives on
|
||||
# its own offset band (~4.1-4.9 with default metadata), so the
|
||||
# default 0.42 floor rarely binds here; the check enforces the floor
|
||||
# contract for ANY min_score (an elevated floor above the band
|
||||
# forces abstention — machine-checked in the frozen tests).
|
||||
if max(float(s["relevance"]) for s in top) < min_score:
|
||||
return [] # below floor -> abstain
|
||||
if len(scored) == 1:
|
||||
return top # single conjunctive match = confident enough
|
||||
# Multiple conjunctive BM25 matches: the FTS5 implicit-AND already
|
||||
# guarantees ALL query terms co-occur in every result (the precision
|
||||
# filter), so these are genuine matches — not a fuzzy near-tie. The
|
||||
# normalized top is the best; a weak runner-up is still a real match.
|
||||
return top
|
||||
# dense present. Score-based abstention (QPP, Shtok et al. TOIS 2012).
|
||||
# The absolute floor is CALIBRATED against real cases (nomic band ~0.6-0.75):
|
||||
# "what does the user drink" vs "black coffee" = 0.766 -> relevant
|
||||
# "quantum physics of black holes" vs "coconut milk" = 0.712 -> noise
|
||||
# The separation sits at ~0.74. Below it, only a clear winner (large gap to
|
||||
# the runner-up) may pass; a weak top with no clear winner abstains.
|
||||
if not bm25:
|
||||
if best_dense < min_score:
|
||||
return [] # below the "probably unrelated" floor
|
||||
return []
|
||||
if best_dense >= 0.74:
|
||||
return _position_aware(top) # calibrated: genuine semantic match
|
||||
return _py_position_aware(top)
|
||||
if len(dense) >= 2:
|
||||
d_scores = sorted(s for _, s in dense)
|
||||
top1 = d_scores[-1]
|
||||
top2 = d_scores[-2]
|
||||
if top1 - top2 >= 0.08:
|
||||
return _position_aware(top) # clear winner despite mid-band score
|
||||
return [] # weak + ambiguous -> abstain
|
||||
return _position_aware(_map_reduce(top)) # single mid-band dense hit
|
||||
# P1-03 (#26) DISCHARGE (finding 1): the hybrid path (dense AND lexical
|
||||
# both fired) gates on the dense cosine — the calibrated scale min_score
|
||||
# was tuned against — exactly like the dense-only branch above. The
|
||||
# numeric check is ADDITIVE to the conjunctive FTS gate (which stays
|
||||
# as-is); the RRF-fused relevance is rank-based and is NOT compared
|
||||
# against the floor here.
|
||||
d_scores = sorted(score for _, score in dense)
|
||||
if d_scores[-1] - d_scores[-2] >= 0.08:
|
||||
return _py_position_aware(top)
|
||||
return []
|
||||
return _py_position_aware(_py_map_reduce(top))
|
||||
if best_dense < min_score:
|
||||
return []
|
||||
return _position_aware(_map_reduce(top))
|
||||
return _py_position_aware(_py_map_reduce(top))
|
||||
|
||||
|
||||
def _drift_rerank(db, entries):
|
||||
@@ -944,7 +882,27 @@ def _drift_rerank(db, entries):
|
||||
return [entry for _, entry in scored]
|
||||
|
||||
|
||||
def _mr_core():
|
||||
"""Rust memory_retrieval core when built; Python fallback remains
|
||||
load-bearing during the strangler. RUNE_RETRIEVAL_CORE=python pins it."""
|
||||
if os.environ.get("RUNE_RETRIEVAL_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import mr_loader
|
||||
|
||||
return mr_loader if mr_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — loader probe failure means Python fallback
|
||||
return None
|
||||
|
||||
|
||||
def _map_reduce(entries: list, chunk_size: int = 10) -> list:
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.map_reduce(entries, chunk_size)
|
||||
return _py_map_reduce(entries, chunk_size)
|
||||
|
||||
|
||||
def _py_map_reduce(entries: list, chunk_size: int = 10) -> list:
|
||||
"""Map-reduce for large result sets.
|
||||
|
||||
When retrieval returns many entries, chunk them and take the top-N
|
||||
@@ -1173,6 +1131,13 @@ def cached_recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, m
|
||||
|
||||
|
||||
def _position_aware(top):
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.position_aware(top)
|
||||
return _py_position_aware(top)
|
||||
|
||||
|
||||
def _py_position_aware(top):
|
||||
"""POSITION-AWARE RE-RANK (research uplift #2): lost-in-the-middle
|
||||
mitigation (Liu et al. 2023; BriefContext, npj Digital Medicine 2025).
|
||||
Models use the START and END of context best and the middle worst, so the
|
||||
@@ -1247,6 +1212,13 @@ _STOPWORDS = {
|
||||
|
||||
|
||||
def _fts_query_conjunctive(query):
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.fts_query_conjunctive(query)
|
||||
return _py_fts_query_conjunctive(query)
|
||||
|
||||
|
||||
def _py_fts_query_conjunctive(query):
|
||||
"""Build a CONJUNCTIVE FTS5 MATCH query (implicit AND).
|
||||
|
||||
Extended Boolean IR (Salton, Fox & Wu, CACM 1983): a document must contain
|
||||
@@ -1269,6 +1241,85 @@ def _fts_query(query):
|
||||
return _fts_query_conjunctive(query)
|
||||
|
||||
|
||||
def mr_rank_fusion(dense, bm25, cap):
|
||||
"""Chunk-side RRF fusion with an explicit candidate cap (W2.1)."""
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.rank_fusion(dense[:cap], bm25[:cap], RRF_K)
|
||||
return _py_rank_fusion(dense[:cap], bm25[:cap], RRF_K)
|
||||
|
||||
|
||||
def _project_chunk_rows(ranks, rows, budget):
|
||||
"""Chunk projection: sorted by RRF desc, text[:400], budget-capped.
|
||||
`ranks` is the W2.1 pair-list ([[rowid, rrf], ...]) from the fusion
|
||||
contract; the Rust side consumes pairs directly."""
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
pairs = (ranks if isinstance(ranks, list)
|
||||
else [list(r) for r in ranks.items()])
|
||||
return core.project_chunk_rows(pairs, rows, budget)
|
||||
return _py_project_chunk_rows(ranks, rows, budget)
|
||||
|
||||
|
||||
def _py_project_chunk_rows(ranks, rows, budget):
|
||||
by_id = {r["rowid"]: r for r in rows}
|
||||
out = []
|
||||
for eid, rrf in sorted(ranks.items(), key=lambda x: -x[1]):
|
||||
r = by_id.get(eid)
|
||||
if not r:
|
||||
continue
|
||||
out.append({
|
||||
"chunk_id": r["chunk_id"],
|
||||
"text": (r["text"] or "")[:400],
|
||||
"wing": r["wing"],
|
||||
"room": r["room"],
|
||||
"source": r["source_path"],
|
||||
"page": r["page"] if "page" in r.keys() else None,
|
||||
"rrf": round(rrf, 4),
|
||||
})
|
||||
if len(out) >= budget:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def _rank_fusion(dense, bm25):
|
||||
"""Pure dense/BM25 fusion and mode selection (R2 W2.1).
|
||||
|
||||
Returns ranks as ordered [id, score] pairs so integer ids survive the
|
||||
JSON membrane without object-key coercion.
|
||||
"""
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.rank_fusion(dense, bm25, RRF_K)
|
||||
return _py_rank_fusion(dense, bm25, RRF_K)
|
||||
|
||||
|
||||
def _py_rank_fusion(dense, bm25, rrf_k=RRF_K):
|
||||
ranks = {}
|
||||
best_dense = 0.0
|
||||
mode = "lexical-only"
|
||||
if dense and bm25:
|
||||
mode = "hybrid"
|
||||
for idx, values in enumerate([dense, bm25]):
|
||||
for pos, (eid, score) in enumerate(values):
|
||||
ranks[eid] = ranks.get(eid, 0.0) + 1.0 / (rrf_k + pos + 1)
|
||||
if idx == 0:
|
||||
best_dense = max(best_dense, score)
|
||||
elif dense:
|
||||
mode = "dense-only"
|
||||
for pos, (eid, score) in enumerate(dense):
|
||||
ranks[eid] = 1.0 / (rrf_k + pos + 1)
|
||||
best_dense = max(best_dense, score)
|
||||
elif bm25:
|
||||
min_bm = min((score for _, score in bm25), default=0.0)
|
||||
max_bm = max((score for _, score in bm25), default=0.0)
|
||||
span = max(max_bm - min_bm, 1e-9)
|
||||
for eid, score in bm25:
|
||||
ranks[eid] = (score - min_bm) / span
|
||||
return {"ranks": [[eid, score] for eid, score in ranks.items()],
|
||||
"best_dense": best_dense, "mode": mode}
|
||||
|
||||
|
||||
def update_entry(db, eid, text=None, importance=None, valid_until=None):
|
||||
sets, params = [], []
|
||||
if text is not None:
|
||||
@@ -1456,6 +1507,13 @@ def working_clear(db, task_id):
|
||||
|
||||
|
||||
def _chunk_text(text, size=1500, overlap=150):
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.chunk_text(text, size, overlap)
|
||||
return _py_chunk_text(text, size, overlap)
|
||||
|
||||
|
||||
def _py_chunk_text(text, size=1500, overlap=150):
|
||||
"""Recursive-ish split on paragraph/heading boundaries, ~1500 chars (the
|
||||
local CPU-friendly size; research: fixed chunking beats semantic for
|
||||
free systems). Headings stay with their content."""
|
||||
@@ -1577,43 +1635,22 @@ def chunk_recall(db, query, budget=6, wing=None, min_sim=0.0, doc=None):
|
||||
bm25 = [(r["rowid"], 0.5) for r in rows]
|
||||
except Exception:
|
||||
logger.debug("chunk_recall failed", exc_info=True)
|
||||
ranks = {}
|
||||
for lst in [dense, bm25]:
|
||||
for pos, (eid, _s) in enumerate(lst):
|
||||
ranks[eid] = ranks.get(eid, 0.0) + 1.0 / (RRF_K + pos + 1)
|
||||
if not ranks:
|
||||
return []
|
||||
ids = list(ranks.keys())
|
||||
ph = ",".join("?" * len(ids))
|
||||
sql = f"SELECT rowid, * FROM chunks WHERE rowid IN ({ph})"
|
||||
params = ids
|
||||
if wing:
|
||||
sql += " AND wing=?"
|
||||
params.append(wing)
|
||||
if doc:
|
||||
sql += " AND doc_id=?"
|
||||
params.append(doc)
|
||||
rows = db.execute(sql, params).fetchall()
|
||||
by_id = {r["rowid"]: r for r in rows}
|
||||
out = []
|
||||
for eid, rrf in sorted(ranks.items(), key=lambda x: -x[1]):
|
||||
r = by_id.get(eid)
|
||||
if not r:
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"chunk_id": r["chunk_id"],
|
||||
"text": r["text"][:400],
|
||||
"wing": r["wing"],
|
||||
"room": r["room"],
|
||||
"source": r["source_path"],
|
||||
"page": r["page"] if "page" in r.keys() else None,
|
||||
"rrf": round(rrf, 4),
|
||||
}
|
||||
)
|
||||
if len(out) >= budget:
|
||||
break
|
||||
return out
|
||||
fusion = mr_rank_fusion(dense, bm25, budget * 3)
|
||||
ranks_list = fusion["ranks"] # ordered [rowid, rrf] pairs (W2.1 contract)
|
||||
ids = [int(v[0]) for v in ranks_list]
|
||||
rows = []
|
||||
if ids:
|
||||
ph = ",".join("?" * len(ids))
|
||||
sql = f"SELECT rowid, * FROM chunks WHERE rowid IN ({ph})"
|
||||
params = ids
|
||||
if wing:
|
||||
sql += " AND wing=?"
|
||||
params.append(wing)
|
||||
if doc:
|
||||
sql += " AND doc_id=?"
|
||||
params.append(doc)
|
||||
rows = [dict(r) for r in db.execute(sql, params).fetchall()]
|
||||
return _project_chunk_rows(fusion["ranks"], rows, budget)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- mining ----
|
||||
@@ -1907,6 +1944,71 @@ def stats(db):
|
||||
return {"active_entries": e, "working": w, "audit": a, "top_topics": by_topic}
|
||||
|
||||
|
||||
# ── storage backend seam (R2 W2.0) ───────────────────────────────────────────
|
||||
# SQLite is RETAINED (Nick's ruling) — the seam separates query logic from
|
||||
# storage mechanics so the Rust core can serve proven-identical READS via
|
||||
# system libsqlite3 FFI. Connection-based callers (stats above) keep their
|
||||
# transactional semantics untouched; path-based consumers opt into the
|
||||
# backend. NO WRITES route through the Rust backend this slice.
|
||||
|
||||
|
||||
class _SQLiteBackend:
|
||||
"""The default backend: the stdlib sqlite3 mechanics, unchanged."""
|
||||
|
||||
name = "sqlite"
|
||||
|
||||
def read_query(self, db_path: str, sql: str, params=()) -> list:
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
return [tuple(r) for r in conn.execute(sql, params).fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class _RustReadBackend:
|
||||
"""The Rust read path through the memory_storage crate (read-only by
|
||||
construction — the crate exports no write-capable symbols)."""
|
||||
|
||||
name = "rust-memory-storage"
|
||||
|
||||
def read_query(self, db_path: str, sql: str, params=()) -> list:
|
||||
from rune import mems_loader
|
||||
|
||||
return [tuple(r) for r in mems_loader.read_query(db_path, sql, list(params))]
|
||||
|
||||
|
||||
def storage_backend():
|
||||
"""The active read backend. Default sqlite; RUNE_MEMORY_STORAGE=rust
|
||||
opts into the crate when built (falls back loudly-free to sqlite)."""
|
||||
if os.environ.get("RUNE_MEMORY_STORAGE", "auto") == "rust":
|
||||
try:
|
||||
from rune import mems_loader
|
||||
|
||||
if mems_loader.available():
|
||||
return _RustReadBackend()
|
||||
except Exception: # noqa: BLE001, S110 — backend probe failure means the default serves (fallback, not silence)
|
||||
pass
|
||||
return _SQLiteBackend()
|
||||
|
||||
|
||||
def stats_for_path(db_path: str) -> dict:
|
||||
"""stats() through the storage backend seam — the opt-in consumer the
|
||||
parity battery and future Rust callers use. Same result dict shape."""
|
||||
backend = storage_backend()
|
||||
e = backend.read_query(db_path, "SELECT COUNT(*) FROM entries WHERE status='active'")[0][0]
|
||||
w = backend.read_query(db_path, "SELECT COUNT(*) FROM working")[0][0]
|
||||
a = backend.read_query(db_path, "SELECT COUNT(*) FROM audit_log")[0][0]
|
||||
by_topic = {
|
||||
r[0]: r[1]
|
||||
for r in backend.read_query(
|
||||
db_path,
|
||||
"SELECT topic, COUNT(*) AS n FROM entries WHERE status='active' GROUP BY topic ORDER BY n DESC LIMIT 6",
|
||||
)
|
||||
if r[0]
|
||||
}
|
||||
return {"active_entries": e, "working": w, "audit": a, "top_topics": by_topic}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ associations ----
|
||||
|
||||
|
||||
@@ -1978,8 +2080,24 @@ def recall_with_associations(db, query, budget=RECALL_BUDGET, min_score=RECALL_M
|
||||
ids = list(boost_map.keys())
|
||||
placeholders = ",".join("?" * len(ids))
|
||||
rows = db.execute(f"SELECT * FROM entries WHERE id IN ({placeholders}) AND status='active'", ids).fetchall()
|
||||
boost_items = [{"id": eid, "strength": strength}
|
||||
for eid, strength in sorted(boost_map.items(), key=lambda x: -x[1])]
|
||||
return _merge_association_rows(base, boost_items, [dict(r) for r in rows], budget)
|
||||
|
||||
|
||||
def _merge_association_rows(base, boost_items, rows, budget):
|
||||
"""Pure association projection/boost/order/budget kernel (R2 W2.1)."""
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.merge_association_rows(base, boost_items, rows, budget)
|
||||
return _py_merge_association_rows(base, boost_items, rows, budget)
|
||||
|
||||
|
||||
def _py_merge_association_rows(base, boost_items, rows, budget):
|
||||
base = list(base)
|
||||
by_id2 = {r["id"]: r for r in rows}
|
||||
for eid, strength in sorted(boost_map.items(), key=lambda x: -x[1]):
|
||||
for item in boost_items:
|
||||
eid, strength = item["id"], item["strength"]
|
||||
row = by_id2.get(eid)
|
||||
if not row:
|
||||
continue
|
||||
@@ -2022,6 +2140,19 @@ def fused_recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, ch
|
||||
"""
|
||||
entries = recall_with_associations(db, query, budget=budget, min_score=min_score)
|
||||
research = chunk_recall(db, query, budget=chunk_budget, wing=wings, doc=None)
|
||||
return _fuse_results(entries, research)
|
||||
|
||||
|
||||
def _fuse_results(entries, research):
|
||||
"""Pure fact/research projection, de-duplication and per-kind scale
|
||||
alignment (R2 W2.1). Rust core when built; Python core is the oracle."""
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.fuse_results(entries, research)
|
||||
return _py_fuse_results(entries, research)
|
||||
|
||||
|
||||
def _py_fuse_results(entries, research):
|
||||
seen = set()
|
||||
merged = []
|
||||
for e in entries:
|
||||
@@ -2075,6 +2206,13 @@ def fused_recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, ch
|
||||
|
||||
|
||||
def _norm_key(text):
|
||||
core = _mr_core()
|
||||
if core is not None:
|
||||
return core.norm_key(text)
|
||||
return _py_norm_key(text)
|
||||
|
||||
|
||||
def _py_norm_key(text):
|
||||
"""Normalize text for cross-table de-duplication (entries vs chunks of the
|
||||
same absorbed material may differ in whitespace/case)."""
|
||||
return " ".join((text or "").lower().split())
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust memory_storage core (R2 W2.0).
|
||||
|
||||
Contract BUILDER-2026-10-04T15:54:17Z-cli. READ-ONLY by construction:
|
||||
the only operation is read_query — the crate exports no write-capable
|
||||
symbols (Nick's ruling: reads proven byte-identical before any writes
|
||||
exist). SQLite is retained; the crate reaches it via system libsqlite3.
|
||||
|
||||
FFI contract (doc-envelope law): mems_read_query({db_path, sql, params})
|
||||
-> [[cell, ...], ...]; mems_free_string(ptr). FFI segfault law: argtypes +
|
||||
restype pinned per symbol. Override the .so with MEMS_RUST_LIB (FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "memory_storage"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"MEMS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmemory_storage.so"
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class MemoryStorageUnavailable(FileNotFoundError):
|
||||
"""The Rust core is not built (or MEMS_RUST_LIB points nowhere)."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise MemoryStorageUnavailable(
|
||||
f"Rust memory_storage core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.mems_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.mems_free_string.restype = None
|
||||
lib.mems_read_query.argtypes = [ctypes.c_char_p]
|
||||
lib.mems_read_query.restype = ctypes.c_void_p # char* — never c_int
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(payload: dict) -> dict:
|
||||
"""Raw envelope: {"ok", "value"|"err"} — the error-honesty section of
|
||||
the canary needs the envelope, not an exception."""
|
||||
lib = _load()
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
ptr = lib.mems_read_query(raw)
|
||||
if not ptr:
|
||||
raise RuntimeError("mems_read_query returned a null pointer")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.mems_free_string(ptr)
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
def read_query(db_path: str, sql: str, params: list) -> list:
|
||||
"""Read-only query through the Rust core; raises on the error envelope."""
|
||||
env = _call({"db_path": db_path, "sql": sql, "params": params})
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"mems_read_query core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def read_query_result(db_path: str, sql: str, params: list) -> dict:
|
||||
"""The raw envelope (error-honesty checks consume this)."""
|
||||
return _call({"db_path": db_path, "sql": sql, "params": params})
|
||||
@@ -0,0 +1,38 @@
|
||||
"""ctypes loader for the zero-dependency memory_retrieval core (R2 W2.1)."""
|
||||
from __future__ import annotations
|
||||
import ctypes, json, os
|
||||
from pathlib import Path
|
||||
_ROOT=Path(__file__).resolve().parents[2]
|
||||
_CRATE=_ROOT/"src/rust/memory_retrieval"
|
||||
_LIB=Path(os.environ.get("MR_RUST_LIB",_CRATE/"target/release/libmemory_retrieval.so"))
|
||||
_lib=None
|
||||
def available(): return _LIB.is_file()
|
||||
def _load():
|
||||
global _lib
|
||||
if _lib is not None:return _lib
|
||||
if not _LIB.exists():raise FileNotFoundError(f"memory_retrieval core not built: {_LIB}")
|
||||
lib=ctypes.CDLL(str(_LIB));lib.mr_call.argtypes=[ctypes.c_char_p];lib.mr_call.restype=ctypes.c_void_p
|
||||
lib.mr_free_string.argtypes=[ctypes.c_void_p];lib.mr_free_string.restype=None;_lib=lib;return lib
|
||||
def _call(op,arg):
|
||||
# Send UTF-8 directly. ensure_ascii=True emits surrogate-pair escapes; the
|
||||
# shared zero-dep parser treats those as independent code points, which
|
||||
# would replace astral characters (emoji) and break Python char-count
|
||||
# parity in chunking/normalization.
|
||||
lib=_load();ptr=lib.mr_call(json.dumps({"op":op,"arg":arg},ensure_ascii=False).encode())
|
||||
if not ptr:raise RuntimeError("mr_call returned null")
|
||||
try:text=ctypes.cast(ptr,ctypes.c_char_p).value.decode()
|
||||
finally:lib.mr_free_string(ptr)
|
||||
env=json.loads(text)
|
||||
if not env.get("ok"):raise RuntimeError(env.get("err"))
|
||||
return env["value"]
|
||||
def map_reduce(entries,chunk_size=10):return _call("map_reduce",{"entries":entries,"chunk_size":chunk_size})
|
||||
def position_aware(entries):return _call("position_aware",{"entries":entries})
|
||||
def fts_query_conjunctive(query):return _call("fts_query_conjunctive",{"query":query})
|
||||
def chunk_text(text,size=1500,overlap=150):return _call("chunk_text",{"text":text,"size":size,"overlap":overlap})
|
||||
def norm_key(text):return _call("norm_key",{"text":text})
|
||||
def fuse_results(entries,research):return _call("fuse_results",{"entries":entries,"research":research})
|
||||
def merge_association_rows(base,boosts,rows,budget):return _call("merge_association_rows",{"base":base,"boosts":boosts,"rows":rows,"budget":budget})
|
||||
def rank_fusion(dense,bm25,rrf_k=60):return _call("rank_fusion",{"dense":dense,"bm25":bm25,"rrf_k":rrf_k})
|
||||
def score_recall_rows(fusion,rows,dense,bm25,budget,min_score,now,half_life):return _call("score_recall_rows",{"fusion":fusion,"rows":rows,"dense":dense,"bm25":bm25,"budget":budget,"min_score":min_score,"now":now,"half_life":half_life})
|
||||
def project_chunk_rows(ranks, rows, budget):
|
||||
return _call("project_chunk_rows", {"ranks": ranks, "rows": rows, "budget": budget})
|
||||
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust memory_scoring core (H3 slice 1, T4.4).
|
||||
|
||||
Contract BUILDER-2026-10-04T08:57:33Z-cli. Gives drift_core the Rust
|
||||
scoring trio through the cdylib with no subprocess. No-clock law: `now` is
|
||||
always an argument (ISO-8601 string) — the Python originals read the wall
|
||||
clock for recency; callers pass it so parity is deterministic.
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* ms_compute_value({entry, goals, now}) -> number
|
||||
* ms_check_promotion_eligibility({entry, now}) -> [eligible, reason]
|
||||
* ms_pre_commit_gate_entry({entry}) -> {verdict, critics, reason}
|
||||
* ms_free_string(ptr)
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char* — the default c_int truncates 64-bit pointers).
|
||||
|
||||
Zero dependencies. The .so resolves against this repo's crate target dir;
|
||||
override with MS_RUST_LIB (a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "memory_scoring"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"MS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmemory_scoring.so"
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class MemoryScoringUnavailable(FileNotFoundError):
|
||||
"""The Rust core is not built (or MS_RUST_LIB points nowhere). Callers
|
||||
fall back to the Python implementation — strangler law: the fallback is
|
||||
load-bearing until deletion is proven."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise MemoryScoringUnavailable(
|
||||
f"Rust memory_scoring core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.ms_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.ms_free_string.restype = None
|
||||
for sym in ("ms_compute_value", "ms_check_promotion_eligibility",
|
||||
"ms_pre_commit_gate_entry", "ms_get_compression_ratio",
|
||||
"ms_compress_text", "ms_compress_entry", "ms_should_warn",
|
||||
"ms_warn_before_compress", "ms_compute_priority",
|
||||
"ms_qualifies_for_cache", "ms_score_gate", "ms_score_finish",
|
||||
"ms_base_level_from_uses"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
fn = getattr(lib, sym)
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
ptr = fn(raw)
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned a null pointer")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.ms_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def compute_value(entry: dict, goals: list | None, now: str) -> float:
|
||||
"""Multi-factor value score (0.0-1.0+); now is an ISO-8601 string."""
|
||||
return _call("ms_compute_value", {"entry": entry, "goals": goals, "now": now})
|
||||
|
||||
|
||||
def check_promotion_eligibility(entry: dict, now: str) -> tuple:
|
||||
"""(eligible, reason) — unpacked from the core's 2-element array."""
|
||||
eligible, reason = _call("ms_check_promotion_eligibility",
|
||||
{"entry": entry, "now": now})
|
||||
return (eligible, reason)
|
||||
|
||||
|
||||
def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"""Three-critic promotion gate for a dict entry (no db context)."""
|
||||
return _call("ms_pre_commit_gate_entry", {"entry": entry})
|
||||
|
||||
|
||||
# ── H3 slice 2: drift_compression / drift_cache pure surfaces ────────────────
|
||||
|
||||
|
||||
def get_compression_ratio(wash_count: float, rock_status: str) -> float:
|
||||
return _call("ms_get_compression_ratio",
|
||||
{"wash_count": wash_count, "rock_status": rock_status})
|
||||
|
||||
|
||||
def compress_text(text: str, ratio: float) -> str:
|
||||
return _call("ms_compress_text", {"text": text, "ratio": ratio})
|
||||
|
||||
|
||||
def compress_entry(entry: dict) -> str:
|
||||
return _call("ms_compress_entry", {"entry": entry})
|
||||
|
||||
|
||||
def should_warn(entry: dict) -> bool:
|
||||
return _call("ms_should_warn", {"entry": entry})
|
||||
|
||||
|
||||
def warn_before_compress(entry: dict) -> dict:
|
||||
return _call("ms_warn_before_compress", {"entry": entry})
|
||||
|
||||
|
||||
def compute_priority(entry: dict) -> float:
|
||||
return _call("ms_compute_priority", {"entry": entry})
|
||||
|
||||
|
||||
def qualifies_for_cache(entry: dict) -> bool:
|
||||
return _call("ms_qualifies_for_cache", {"entry": entry})
|
||||
|
||||
|
||||
# ── H3 slice 3: warm scoring math ────────────────────────────────────────────
|
||||
|
||||
|
||||
def score_gate(neuron: dict, low_text: str, session_context: str | None,
|
||||
semantic_score: float) -> tuple:
|
||||
"""(fires, total, keyword_hits) — unpacked from the 3-element array."""
|
||||
fires, total, kw = _call("ms_score_gate", {
|
||||
"neuron": neuron, "low_text": low_text,
|
||||
"session_context": session_context, "semantic_score": semantic_score})
|
||||
return (fires, total, kw)
|
||||
|
||||
|
||||
def score_finish(neuron: dict, low_text: str, total: float,
|
||||
b_i: float | None, cache_priority: float | None,
|
||||
keyword_hits: float) -> float:
|
||||
return _call("ms_score_finish", {
|
||||
"neuron": neuron, "low_text": low_text, "total": total,
|
||||
"b_i": b_i, "cache_priority": cache_priority,
|
||||
"keyword_hits": keyword_hits})
|
||||
|
||||
|
||||
def base_level_from_uses(used_at: list, now: str) -> float:
|
||||
return _call("ms_base_level_from_uses", {"used_at": used_at, "now": now})
|
||||
@@ -161,7 +161,33 @@ def load_rules():
|
||||
return rules
|
||||
|
||||
|
||||
def _cc_pg_core():
|
||||
"""Rust cognition_core when available (persona kernels); pin with
|
||||
RUNE_COGNITION_CORE=python."""
|
||||
import os
|
||||
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cc_loader
|
||||
except Exception: # noqa: BLE001 — loader probe failure means fallback
|
||||
return None
|
||||
try:
|
||||
return cc_loader if cc_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe never crashes
|
||||
return None
|
||||
|
||||
|
||||
def _lex(text):
|
||||
"""Distinctive content words (R2 W3.2: Rust kernel when built)."""
|
||||
core = _cc_pg_core()
|
||||
if core is not None:
|
||||
return set(core.persona_lex(text))
|
||||
return _py_lex(text)
|
||||
|
||||
|
||||
def _py_lex(text):
|
||||
"""Python oracle (parity floor)."""
|
||||
|
||||
"""Distinctive content words of a rule body (lowercase, >3 chars,
|
||||
light-stemmed so plural/singular engage each other; preserves 3-char
|
||||
ALL-CAPS acronyms so technical abbreviations match their expansion)."""
|
||||
@@ -253,6 +279,15 @@ def _lex_input(text):
|
||||
|
||||
|
||||
def _overlap(a, b):
|
||||
"""Overlap ratio (R2 W3.2: Rust kernel when built)."""
|
||||
core = _cc_pg_core()
|
||||
if core is not None:
|
||||
return core.persona_overlap(sorted(a), sorted(b))
|
||||
return _py_overlap(a, b)
|
||||
|
||||
|
||||
def _py_overlap(a, b):
|
||||
"""Python oracle (parity floor)."""
|
||||
if not a or not b:
|
||||
return 0.0
|
||||
return len(a & b) / max(1, min(len(a), len(b)))
|
||||
@@ -338,6 +373,34 @@ def _semantic_engagement(text, rule):
|
||||
|
||||
|
||||
def decide(text, rules=None):
|
||||
"""The persona decides on a request, deterministically (R2 W3.2:
|
||||
Rust kernel when built; the semantic cosines are gathered here —
|
||||
embedder calls are IO — and passed into the kernel)."""
|
||||
rules = rules if rules is not None else load_rules()
|
||||
core = _cc_pg_core()
|
||||
if core is not None:
|
||||
text_s = (text or "").strip()
|
||||
sems = [_semantic_engagement(text_s, r) for r in rules]
|
||||
return core.persona_decide(text_s, [_rule_json(r) for r in rules],
|
||||
PERMISSIVE_MODE, sems)
|
||||
return _py_decide(text, rules)
|
||||
|
||||
|
||||
def _rule_json(r) -> dict:
|
||||
"""A rule in the kernel's JSON contract (citation rides along when
|
||||
present so _cite stays data-driven)."""
|
||||
return {
|
||||
"text": r["text"], "words": sorted(r["words"]),
|
||||
"core": r["core"], "priority": r.get("priority", 0),
|
||||
"restrictive": r.get("restrictive", False),
|
||||
"user_rule": r.get("user_rule", False),
|
||||
"citation": r.get("citation"),
|
||||
}
|
||||
|
||||
|
||||
def _py_decide(text, rules=None):
|
||||
"""Python oracle (parity floor) — the ORIGINAL decide body."""
|
||||
|
||||
"""The persona decides on a request, deterministically, from its own
|
||||
stored rules. No generative LLM in the decision path (a local embedding
|
||||
model may contribute semantic similarity). Returns a full verdict."""
|
||||
|
||||
+9
-1
@@ -162,8 +162,16 @@ def recall(context, limit=4):
|
||||
term = (context or "").lower()
|
||||
if not term:
|
||||
return []
|
||||
from rune.identity_anchors import _cc_core
|
||||
core = _cc_core()
|
||||
if core is not None:
|
||||
return core.pins_recall(_load(), term, limit)
|
||||
return _py_pin_recall(_load(), term, limit)
|
||||
|
||||
|
||||
def _py_pin_recall(recs, term, limit=4):
|
||||
out = []
|
||||
for rec in _load():
|
||||
for rec in recs:
|
||||
if rec.get("state") != "open":
|
||||
continue
|
||||
p = _project(rec)
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""query_memory.py — Query memory system for adaptive retrieval.
|
||||
|
||||
The system remembers what kinds of queries it gets, what retrieval
|
||||
strategy worked, and adapts its gate logic accordingly.
|
||||
|
||||
This prevents dead weight — memories get tested based on usage
|
||||
patterns, not just random exploration.
|
||||
|
||||
Research basis:
|
||||
- IGMiRAG (arXiv:2602.07525): Intuition-guided retrieval
|
||||
- Task-Adaptive Retrieval (arXiv:2604.07863): Learning from interaction history
|
||||
- MemCon (arXiv:2607.13591): Learned policy for consolidation/forgetting
|
||||
- Agent-Native Memory (arXiv:2606.24775): Workload-adaptive routing
|
||||
|
||||
Run under ~/.venvs/memory/bin/python3.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
QUERY_MEMORY_DIR = os.path.expanduser("~/.config/opencode/memory/query_memory")
|
||||
QUERY_HISTORY_FILE = os.path.join(QUERY_MEMORY_DIR, "history.jsonl")
|
||||
TOPIC_STATS_FILE = os.path.join(QUERY_MEMORY_DIR, "topic_stats.json")
|
||||
|
||||
# Topic keywords for classification
|
||||
TOPIC_KEYWORDS = {
|
||||
"guitar": ["guitar", "music", "teaching", "course", "lesson", "chord", "scale"],
|
||||
"coding": ["code", "python", "script", "function", "debug", "build", "deploy"],
|
||||
"memory": ["memory", "recall", "remember", "forget", "consolidat", "neuron"],
|
||||
"persona": ["persona", "identity", "voice", "constitution", "character"],
|
||||
"project": ["project", "task", "goal", "deadline", "milestone", "progress"],
|
||||
"ai": ["ai", "model", "llm", "agent", "neural", "training", "inference"],
|
||||
"personal": ["nick", "nottingham", "coffee", "coconut", "dog", "ttrpg"],
|
||||
"system": ["system", "config", "install", "setup", "troubleshoot", "error"],
|
||||
"research": ["research", "paper", "study", "analysis", "survey", "evidence"],
|
||||
"creative": ["write", "design", "create", "build", "make", "art", "music"],
|
||||
}
|
||||
|
||||
|
||||
class QueryMemory:
|
||||
"""Query memory system for adaptive retrieval."""
|
||||
|
||||
def __init__(self):
|
||||
os.makedirs(QUERY_MEMORY_DIR, exist_ok=True)
|
||||
self.history: List[dict] = self._load_history()
|
||||
self.topic_stats: Dict[str, dict] = self._load_topic_stats()
|
||||
|
||||
def _load_history(self) -> List[dict]:
|
||||
"""Load query history from disk."""
|
||||
if not os.path.exists(QUERY_HISTORY_FILE):
|
||||
return []
|
||||
entries = []
|
||||
with open(QUERY_HISTORY_FILE) as f:
|
||||
for line in f:
|
||||
try:
|
||||
entries.append(json.loads(line.strip()))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return entries
|
||||
|
||||
def _save_history(self, entry: dict):
|
||||
"""Append a query entry to history."""
|
||||
with open(QUERY_HISTORY_FILE, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
def _load_topic_stats(self) -> Dict[str, dict]:
|
||||
"""Load topic statistics from disk."""
|
||||
if os.path.exists(TOPIC_STATS_FILE):
|
||||
try:
|
||||
with open(TOPIC_STATS_FILE) as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _save_topic_stats(self):
|
||||
"""Save topic statistics to disk."""
|
||||
with open(TOPIC_STATS_FILE, "w") as f:
|
||||
json.dump(self.topic_stats, f, indent=2)
|
||||
|
||||
def classify_query(self, query: str) -> str:
|
||||
"""Classify a query into a topic.
|
||||
|
||||
Returns the topic with the most keyword matches.
|
||||
Falls back to "general" if no strong match.
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
scores = {}
|
||||
for topic, keywords in TOPIC_KEYWORDS.items():
|
||||
score = sum(1 for kw in keywords if kw in query_lower)
|
||||
if score > 0:
|
||||
scores[topic] = score
|
||||
|
||||
if not scores:
|
||||
return "general"
|
||||
|
||||
return max(scores, key=scores.get)
|
||||
|
||||
def record_query(self, query: str, strategy: str, success: bool):
|
||||
"""Record a query and its outcome.
|
||||
|
||||
Args:
|
||||
query: The query text
|
||||
strategy: "consolidated" or "full_retrieval"
|
||||
success: Whether the retrieval produced useful results
|
||||
"""
|
||||
topic = self.classify_query(query)
|
||||
entry = {
|
||||
"query": query[:100],
|
||||
"topic": topic,
|
||||
"strategy": strategy,
|
||||
"success": success,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
self.history.append(entry)
|
||||
self._save_history(entry)
|
||||
|
||||
# Update topic stats
|
||||
if topic not in self.topic_stats:
|
||||
self.topic_stats[topic] = {
|
||||
"total_queries": 0,
|
||||
"successful_queries": 0,
|
||||
"consolidated_hits": 0,
|
||||
"full_retrieval_hits": 0,
|
||||
"exploration_rate": 0.30, # default
|
||||
}
|
||||
|
||||
stats = self.topic_stats[topic]
|
||||
stats["total_queries"] += 1
|
||||
if success:
|
||||
stats["successful_queries"] += 1
|
||||
if strategy == "consolidated":
|
||||
stats["consolidated_hits"] += 1
|
||||
else:
|
||||
stats["full_retrieval_hits"] += 1
|
||||
|
||||
# Adaptive exploration rate: decrease for well-tested topics
|
||||
if stats["total_queries"] >= 10:
|
||||
success_rate = stats["successful_queries"] / stats["total_queries"]
|
||||
if success_rate > 0.8:
|
||||
stats["exploration_rate"] = max(0.10, stats["exploration_rate"] - 0.05)
|
||||
elif success_rate < 0.5:
|
||||
stats["exploration_rate"] = min(0.50, stats["exploration_rate"] + 0.05)
|
||||
|
||||
self._save_topic_stats()
|
||||
|
||||
def get_topic_stats(self) -> Dict[str, dict]:
|
||||
"""Get topic statistics."""
|
||||
return self.topic_stats
|
||||
|
||||
def adaptive_exploration_rate(self, query: str) -> float:
|
||||
"""Get the exploration rate for a query based on its topic.
|
||||
|
||||
Well-tested topics have lower exploration rates.
|
||||
Untested topics have higher exploration rates.
|
||||
"""
|
||||
topic = self.classify_query(query)
|
||||
if topic in self.topic_stats:
|
||||
return self.topic_stats[topic].get("exploration_rate", 0.30)
|
||||
return 0.50 # untested topics get high exploration
|
||||
|
||||
def get_dead_weight_topics(self) -> List[str]:
|
||||
"""Get topics with low proven uses (dead weight)."""
|
||||
dead = []
|
||||
for topic, stats in self.topic_stats.items():
|
||||
if stats["total_queries"] < 5:
|
||||
dead.append(topic)
|
||||
return dead
|
||||
|
||||
def get_health_report(self) -> dict:
|
||||
"""Get a health report of the query memory system."""
|
||||
total_queries = len(self.history)
|
||||
topics = len(self.topic_stats)
|
||||
dead_topics = len(self.get_dead_weight_topics())
|
||||
|
||||
return {
|
||||
"total_queries": total_queries,
|
||||
"topics_tracked": topics,
|
||||
"dead_weight_topics": dead_topics,
|
||||
"topic_stats": self.topic_stats,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Query memory system")
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
|
||||
# record <query> --strategy <s> --success
|
||||
p_rec = sub.add_parser("record", help="Record a query")
|
||||
p_rec.add_argument("query", nargs="+")
|
||||
p_rec.add_argument("--strategy", default="consolidated")
|
||||
p_rec.add_argument("--success", action="store_true")
|
||||
|
||||
# stats
|
||||
sub.add_parser("stats", help="Show topic statistics")
|
||||
|
||||
# rate <query>
|
||||
p_rate = sub.add_parser("rate", help="Get exploration rate for a query")
|
||||
p_rate.add_argument("query", nargs="+")
|
||||
|
||||
# health
|
||||
sub.add_parser("health", help="Show health report")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.cmd:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
qm = QueryMemory()
|
||||
|
||||
if args.cmd == "record":
|
||||
query = " ".join(args.query)
|
||||
qm.record_query(query, args.strategy, args.success)
|
||||
print(f"Recorded: {query[:50]} → {args.strategy} ({'success' if args.success else 'fail'})")
|
||||
|
||||
elif args.cmd == "stats":
|
||||
stats = qm.get_topic_stats()
|
||||
if stats:
|
||||
print("Topic Statistics:")
|
||||
for topic, s in stats.items():
|
||||
print(
|
||||
f" {topic}: {s['total_queries']} queries, {s['successful_queries']} success, rate={s['exploration_rate']:.2f}"
|
||||
)
|
||||
else:
|
||||
print("No topic statistics yet.")
|
||||
|
||||
elif args.cmd == "rate":
|
||||
query = " ".join(args.query)
|
||||
rate = qm.adaptive_exploration_rate(query)
|
||||
topic = qm.classify_query(query)
|
||||
print(f"Query: {query[:50]}")
|
||||
print(f"Topic: {topic}")
|
||||
print(f"Exploration rate: {rate:.2f}")
|
||||
|
||||
elif args.cmd == "health":
|
||||
report = qm.get_health_report()
|
||||
print("Query Memory Health:")
|
||||
print(f" Total queries: {report['total_queries']}")
|
||||
print(f" Topics tracked: {report['topics_tracked']}")
|
||||
print(f" Dead weight topics: {report['dead_weight_topics']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -805,13 +805,22 @@ class RuneOperator:
|
||||
except Exception: # noqa: S110 — notes are best-effort by design
|
||||
pass
|
||||
|
||||
def _construction_activation(self, description: str, task_id: str) -> None:
|
||||
def _construction_activation(self, description: str, task_id: str,
|
||||
channel: str = OPERATOR_CHANNEL) -> None:
|
||||
"""SCAFFOLD-TRANCHE-0011/0012 §11: contextual construction
|
||||
classification + activation on the NORMAL input path. Deterministic;
|
||||
the mode lands in the durable scaffold store where the shared
|
||||
Construction Context (and the future TUI) reads it. Delegates all
|
||||
lifecycle decisions to construction_mode/construction_activation —
|
||||
NO duplicate intelligence here."""
|
||||
NO duplicate intelligence here.
|
||||
|
||||
§17J criterion 1 (T22 wiring): the canonical intent layer
|
||||
(construction_intent) resolves MEANING before the ONE
|
||||
classifier decides mode — the boundary the intent module
|
||||
documents ("the intent layer resolves MEANING; classify_mode
|
||||
decides mode"). A turn the intent layer resolves as
|
||||
DISCUSSION is not a construction demand, so no construction
|
||||
state is recorded for it."""
|
||||
from .construction_mode import classify_mode, _STANDALONE_WORDS
|
||||
from .construction_activation import activate
|
||||
# §15J case F: manual override feeds the SAME decision path — a
|
||||
@@ -849,6 +858,56 @@ class RuneOperator:
|
||||
from .scaffold import Scaffold
|
||||
s = Scaffold(self._construction_store(), "issue161-harness")
|
||||
doc = s.load()
|
||||
# §17J criterion 1: meaning gate BEFORE the classifier.
|
||||
# Context = the operator's own canonical conversation window
|
||||
# (no new state store); the current message is excluded so it
|
||||
# is not double-recorded as its own referent.
|
||||
try:
|
||||
from .construction_intent import (
|
||||
ConversationContext, resolve_construction_intent)
|
||||
ctx = ConversationContext()
|
||||
for _m in self.conversation_window(channel, n=8):
|
||||
_txt = str(_m.get("content", ""))
|
||||
if _txt and _txt != description:
|
||||
ctx.record_turn(str(_m.get("role", "user")), _txt)
|
||||
_intent = resolve_construction_intent(description, ctx)
|
||||
except Exception as exc: # noqa: BLE001 — intent layer
|
||||
# unavailable: PRESERVE the pre-existing behaviour exactly
|
||||
# (classify+activate) rather than silently changing
|
||||
# governance in an unproven direction. The note is appended to
|
||||
# THIS doc (not via _construction_note, which does its own
|
||||
# load/save) because the normal path saves `doc` afterwards —
|
||||
# a separate write would be clobbered (lost update).
|
||||
_intent = None
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_note",
|
||||
"text": (f"intent layer unavailable ({exc}); "
|
||||
f"classifier path unchanged")[:300],
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
# An explicit user override is USER AUTHORITY and outranks the
|
||||
# meaning gate (§15J case F: the override is already carried with
|
||||
# user provenance into the SAME classify_mode call).
|
||||
if (override is None and _intent is not None
|
||||
and _intent.action_intent == "DISCUSSION"):
|
||||
# The intent layer resolved this turn as DISCUSSION: it is not a
|
||||
# construction demand, so no construction state is created. The
|
||||
# record keeps the SAME canonical observation kind/shape every
|
||||
# other input produces (mode vocabulary unchanged — DISCUSSION
|
||||
# maps to NOT_CONSTRUCTION, "no construction state"), with the
|
||||
# intent evidence retained so the verdict is auditable.
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_activated",
|
||||
"task": task_id,
|
||||
"mode": "NOT_CONSTRUCTION",
|
||||
"activated": False,
|
||||
"lifecycle": "",
|
||||
"classification_evidence": _intent.classification_evidence[:3],
|
||||
"intent_layer": "DISCUSSION",
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
s.save(doc)
|
||||
return
|
||||
rec = activate(mode_decision, need=type("N", (), {
|
||||
"objective": description,
|
||||
"project": "rune",
|
||||
@@ -929,7 +988,8 @@ class RuneOperator:
|
||||
# failure NEVER blocks the ordinary task path (same lifecycle
|
||||
# guarantee as the route above).
|
||||
try:
|
||||
self._construction_activation(description, state_task.task_id)
|
||||
self._construction_activation(description, state_task.task_id,
|
||||
channel=channel)
|
||||
except Exception as exc: # noqa: BLE001 — construction must not break tasks
|
||||
try:
|
||||
self._task_state.transition(
|
||||
|
||||
+16
-6
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.86.0
|
||||
Version: v0.99.1
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -74,6 +74,14 @@ import voice_sync
|
||||
import provider_registry
|
||||
import goal
|
||||
import rune_operator
|
||||
import tui_transport as _tui_transport
|
||||
|
||||
|
||||
def _operator_transport():
|
||||
"""H4 slice 1 transport seam: the TUI's operator calls route through
|
||||
tui_transport (RUNE_TUI_TRANSPORT=inprocess|v1, default inprocess —
|
||||
zero behavior change until the wave opts in)."""
|
||||
return _tui_transport.get_transport()
|
||||
import cron_engine
|
||||
import night_mode
|
||||
import time_awareness
|
||||
@@ -1144,7 +1152,7 @@ class ChannelView(Vertical):
|
||||
TUI must never fail to mount because the ledger is unavailable.
|
||||
"""
|
||||
try:
|
||||
window = rune_operator.get_conversation_window(self.channel_id, n=8)
|
||||
window = _operator_transport().get_conversation_window(self.channel_id, n=8)
|
||||
except Exception:
|
||||
return
|
||||
for msg in window:
|
||||
@@ -1705,6 +1713,8 @@ class RuneApp(App):
|
||||
the load_tasks fallback then handles."""
|
||||
try:
|
||||
import rune_operator as _ro
|
||||
# H4: private _task_state peek — stays in-process by design
|
||||
# (the /v1 surface carries the four public calls only).
|
||||
return getattr(_ro.get_operator(), "_task_state", None)
|
||||
except Exception as exc: # noqa: BLE001 — deliberate degradation: startup ordering; the fallback path handles it and the degradation is logged
|
||||
print(f"[task-loader] canonical repo unavailable ({exc}); "
|
||||
@@ -1885,7 +1895,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.86.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.99.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -2809,7 +2819,7 @@ class RuneApp(App):
|
||||
|
||||
def _cmd_rune(self, arg):
|
||||
try:
|
||||
status = rune_operator.get_status()
|
||||
status = _operator_transport().get_status()
|
||||
lines = ["[bold]🎭 Rune[/bold]"]
|
||||
lines.append(f" Identity: {status['identity']}")
|
||||
lines.append(f" Role: {status['role']}")
|
||||
@@ -3621,7 +3631,7 @@ class RuneApp(App):
|
||||
# Rune sends task to model with the selected model.
|
||||
# channel is forwarded so the canonical task ledger (#18)
|
||||
# keeps per-channel task identity.
|
||||
result = rune_operator.send_task(text, model=model_name, channel=channel)
|
||||
result = _operator_transport().send_task(text, model=model_name, channel=channel)
|
||||
|
||||
# Remove thinking indicator
|
||||
thinking.stop()
|
||||
@@ -3855,7 +3865,7 @@ class RuneApp(App):
|
||||
button.disabled = True
|
||||
|
||||
async def _resolve() -> None:
|
||||
result = rune_operator.resolve_pending_approval(event.approved)
|
||||
result = _operator_transport().resolve_pending_approval(event.approved)
|
||||
channel = widget.channel
|
||||
ch = self.task_channel if channel == "task" else self.chat_channel
|
||||
log = ch.query_one(f"#log-{ch.channel_id}", VerticalScroll)
|
||||
|
||||
+117
-78
@@ -86,6 +86,110 @@ PATTERN_MAX_PATTERNS = 200
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def _keyword_stopwords() -> list:
|
||||
"""Gather seam (W3.4): the pattern-keyword stopword table as a sorted
|
||||
list — data, not logic; both engines read this module-owned table."""
|
||||
return sorted(_KEYWORD_STOPWORDS)
|
||||
|
||||
|
||||
_KEYWORD_STOPWORDS = frozenset({
|
||||
"the",
|
||||
"and",
|
||||
"for",
|
||||
"are",
|
||||
"but",
|
||||
"not",
|
||||
"you",
|
||||
"all",
|
||||
"can",
|
||||
"had",
|
||||
"her",
|
||||
"was",
|
||||
"one",
|
||||
"our",
|
||||
"out",
|
||||
"day",
|
||||
"get",
|
||||
"has",
|
||||
"him",
|
||||
"his",
|
||||
"how",
|
||||
"its",
|
||||
"may",
|
||||
"new",
|
||||
"now",
|
||||
"old",
|
||||
"see",
|
||||
"way",
|
||||
"who",
|
||||
"did",
|
||||
"let",
|
||||
"say",
|
||||
"she",
|
||||
"too",
|
||||
"use",
|
||||
"this",
|
||||
"that",
|
||||
"with",
|
||||
"have",
|
||||
"from",
|
||||
"they",
|
||||
"been",
|
||||
"said",
|
||||
"each",
|
||||
"make",
|
||||
"like",
|
||||
"just",
|
||||
"over",
|
||||
"such",
|
||||
"take",
|
||||
"year",
|
||||
"them",
|
||||
"some",
|
||||
"time",
|
||||
"very",
|
||||
"when",
|
||||
"come",
|
||||
"could",
|
||||
"what",
|
||||
"there",
|
||||
"their",
|
||||
"will",
|
||||
"would",
|
||||
"about",
|
||||
"which",
|
||||
"these",
|
||||
"other",
|
||||
"into",
|
||||
"more",
|
||||
"than",
|
||||
"also",
|
||||
"then",
|
||||
})
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust cognition_core when available (strangler: Python fallback
|
||||
load-bearing). RUNE_COGNITION_CORE=python pins the fallback."""
|
||||
return None if cc_loader_pin() else _cc_probe()
|
||||
|
||||
|
||||
def _cc_probe():
|
||||
from rune import cc_loader as _cl
|
||||
try:
|
||||
return _cl if _cl.available() else None
|
||||
except Exception: # noqa: BLE001, S110 — loader probe never crashes the fallback
|
||||
return None
|
||||
|
||||
|
||||
def cc_loader_pin() -> bool:
|
||||
"""The env pin: RUNE_COGNITION_CORE=python → kernels OFF (shared probe).
|
||||
"""
|
||||
import os
|
||||
|
||||
return os.environ.get("RUNE_COGNITION_CORE", "auto") == "python"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OverridePattern:
|
||||
"""A learned pattern that requires override."""
|
||||
@@ -105,7 +209,14 @@ class OverridePattern:
|
||||
return self.successes / total
|
||||
|
||||
def matches(self, text: str) -> bool:
|
||||
"""Check if text matches this pattern's keywords."""
|
||||
"""Check if text matches this pattern's keywords. Deterministic
|
||||
keyword-count check (kernel port NOT owed here — the W3.4 safety
|
||||
kernels cover the params/rows surfaces; this tiny comparison stays
|
||||
Python-side per the membrane law: gather/IO in Python, kernels for
|
||||
deterministic row work already ported)."""
|
||||
return self._py_matches(text)
|
||||
|
||||
def _py_matches(self, text: str) -> bool:
|
||||
text_lower = text.lower()
|
||||
matches = sum(1 for kw in self.keywords if kw.lower() in text_lower)
|
||||
return matches >= PATTERN_MIN_KEYWORDS
|
||||
@@ -293,84 +404,12 @@ class SafetyOverride:
|
||||
self.stats.patterns_learned += 1
|
||||
|
||||
def _extract_keywords(self, text: str) -> list[str]:
|
||||
"""Extract distinctive keywords from text for pattern matching."""
|
||||
# Remove common stop words, keep words > 3 chars
|
||||
stop_words = {
|
||||
"the",
|
||||
"and",
|
||||
"for",
|
||||
"are",
|
||||
"but",
|
||||
"not",
|
||||
"you",
|
||||
"all",
|
||||
"can",
|
||||
"had",
|
||||
"her",
|
||||
"was",
|
||||
"one",
|
||||
"our",
|
||||
"out",
|
||||
"day",
|
||||
"get",
|
||||
"has",
|
||||
"him",
|
||||
"his",
|
||||
"how",
|
||||
"its",
|
||||
"may",
|
||||
"new",
|
||||
"now",
|
||||
"old",
|
||||
"see",
|
||||
"way",
|
||||
"who",
|
||||
"did",
|
||||
"let",
|
||||
"say",
|
||||
"she",
|
||||
"too",
|
||||
"use",
|
||||
"this",
|
||||
"that",
|
||||
"with",
|
||||
"have",
|
||||
"from",
|
||||
"they",
|
||||
"been",
|
||||
"said",
|
||||
"each",
|
||||
"make",
|
||||
"like",
|
||||
"just",
|
||||
"over",
|
||||
"such",
|
||||
"take",
|
||||
"year",
|
||||
"them",
|
||||
"some",
|
||||
"time",
|
||||
"very",
|
||||
"when",
|
||||
"come",
|
||||
"could",
|
||||
"what",
|
||||
"there",
|
||||
"their",
|
||||
"will",
|
||||
"would",
|
||||
"about",
|
||||
"which",
|
||||
"these",
|
||||
"other",
|
||||
"into",
|
||||
"more",
|
||||
"than",
|
||||
"also",
|
||||
"then",
|
||||
}
|
||||
"""Extract distinctive keywords from text for pattern matching.
|
||||
Keyword extraction stays Python-side (W3.4 scope note): the safety
|
||||
kernels cover the params/rows surfaces; this extractor is a gather
|
||||
(IO-adjacent list build) per the membrane law."""
|
||||
words = re.findall(r"[a-z]{4,}", text.lower())
|
||||
keywords = [w for w in words if w not in stop_words]
|
||||
keywords = [w for w in words if w not in _KEYWORD_STOPWORDS]
|
||||
# Return top 5 most distinctive words
|
||||
return keywords[:5]
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust scaffold_core (R2 Wave 1).
|
||||
|
||||
Contract BUILDER-2026-10-04T15:25:33Z-cli. The V1-ACCEPTED scaffolding
|
||||
engine's deterministic core through the cdylib with no subprocess.
|
||||
No-clock law: `now` always arrives inside the inputs.
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* sc_check_transition({current, target}) -> {legal, error?}
|
||||
* sc_close_quality(inputs) -> contract_quality bucket dict
|
||||
* sc_free_string(ptr)
|
||||
|
||||
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
|
||||
for returned char*). Zero dependencies; override the .so with SC_RUST_LIB
|
||||
(a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "scaffold_core"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"SC_RUST_LIB", _CRATE_DIR / "target" / "release" / "libscaffold_core.so"
|
||||
)
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ScaffoldCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust core is not built (or SC_RUST_LIB points nowhere). Callers
|
||||
fall back to the Python implementation — strangler law."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise ScaffoldCoreUnavailable(
|
||||
f"Rust scaffold_core not built: {_LIB_PATH} missing — run "
|
||||
f"`cargo build --release` in {_CRATE_DIR}"
|
||||
)
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.sc_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.sc_free_string.restype = None
|
||||
for sym in ("sc_check_transition", "sc_close_quality",
|
||||
"sc_language_placement"):
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p # char* — never default c_int
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
fn = getattr(lib, sym)
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
ptr = fn(raw)
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned a null pointer")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.sc_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
|
||||
|
||||
def check_transition(current: str, target: str) -> dict:
|
||||
"""Phase-transition legality verdict ({legal} or {legal, error})."""
|
||||
return _call("sc_check_transition", {"current": current, "target": target})
|
||||
|
||||
|
||||
def close_quality(inputs: dict) -> dict:
|
||||
"""The closure classification buckets from gathered inputs."""
|
||||
return _call("sc_close_quality", inputs)
|
||||
|
||||
|
||||
def language_placement(project: str, characteristics: list) -> dict:
|
||||
"""The protocol's language-placement verdict (R2 Wave 1)."""
|
||||
return _call("sc_language_placement",
|
||||
{"project": project, "characteristics": characteristics})
|
||||
+81
-63
@@ -57,7 +57,7 @@ from rune.structural_store import ( # shared generic core — no Rune constants
|
||||
task_entities,
|
||||
)
|
||||
|
||||
__all__ = ["Scaffold", "PHASES", "LEGAL_TRANSITIONS", "REPO_PLACEMENTS", "REPO_SECRET_FIELDS"]
|
||||
__all__ = ["Scaffold", "PHASES", "LEGAL_TRANSITIONS", "TERMINAL_PHASES", "REPO_PLACEMENTS", "REPO_SECRET_FIELDS"]
|
||||
|
||||
# ── constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -73,6 +73,10 @@ LEGAL_TRANSITIONS = {
|
||||
"failed": {"building", "abandoned"}, # retry path
|
||||
"abandoned": set(),
|
||||
}
|
||||
# Terminal phases are DERIVED from the canonical transition table: a
|
||||
# phase with no legal successor ends the contract. One authority —
|
||||
# consumers must not re-declare their own terminal list.
|
||||
TERMINAL_PHASES = frozenset(p for p, nxt in LEGAL_TRANSITIONS.items() if not nxt)
|
||||
STATUSES = {"open", "closed", "failed", "abandoned"}
|
||||
REPO_PLACEMENTS = {"RUNE_REPO", "EXISTING_PROJECT_REPO", "NEW_MANAGED_REPO"}
|
||||
REPO_SECRET_FIELDS = {"token", "api_key", "password", "secret", "auth_header", "credential", "api_token"}
|
||||
@@ -484,6 +488,64 @@ class Scaffold:
|
||||
def contract(self, doc: dict, contract_id: str) -> dict | None:
|
||||
return entity(doc, f"contract:{contract_id}")
|
||||
|
||||
def _py_close_quality(self, inputs: dict) -> dict:
|
||||
"""Python core of the closure classification (R2 Wave 1 parity
|
||||
oracle — scaffold_core.close_quality computes from the same input
|
||||
shape). Pure bucket math; side effects stay in close_contract."""
|
||||
affected = set(inputs.get("affected_entities") or [])
|
||||
expected_new = set(inputs.get("expected_new_entities") or [])
|
||||
expected_runtime = set(inputs.get("expected_runtime_effects") or [])
|
||||
observed_evidence = inputs.get("observed_evidence") or []
|
||||
predicted = inputs.get("predicted_frontier")
|
||||
obs_set = {item.get("entity", "") for item in observed_evidence}
|
||||
obs_set.discard("")
|
||||
|
||||
pred_existing = set(inputs.get("predicted_existing_affected") or [])
|
||||
pred_existing |= affected
|
||||
if predicted is not None:
|
||||
pred_existing |= {
|
||||
x
|
||||
for k in ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
for x in predicted.get(k, [])
|
||||
}
|
||||
|
||||
correctly_predicted = sorted(pred_existing & obs_set)
|
||||
predicted_but_unaffected = sorted(pred_existing - obs_set)
|
||||
expected_new_observed = sorted(expected_new & obs_set)
|
||||
expected_new_missing = sorted(expected_new - obs_set)
|
||||
matched_runtime = {o for o in obs_set if any(eff.startswith(o) for eff in expected_runtime)}
|
||||
expected_runtime_observed = sorted(
|
||||
eff for eff in expected_runtime if any(eff.startswith(o) for o in matched_runtime)
|
||||
)
|
||||
genuinely_unexpected_new = sorted(obs_set - pred_existing - expected_new - matched_runtime)
|
||||
|
||||
return {
|
||||
"predicted_existing_affected": sorted(pred_existing),
|
||||
"expected_new_entities": sorted(expected_new),
|
||||
"observed": sorted(obs_set),
|
||||
"observed_existing": correctly_predicted,
|
||||
"observed_new": sorted(obs_set & (expected_new | (obs_set - pred_existing))),
|
||||
"correctly_predicted": correctly_predicted,
|
||||
"predicted_but_unaffected": predicted_but_unaffected,
|
||||
"expected_new_observed": expected_new_observed,
|
||||
"expected_new_missing": expected_new_missing,
|
||||
"expected_runtime_effects": sorted(expected_runtime),
|
||||
"expected_runtime_observed": expected_runtime_observed,
|
||||
"expected_runtime_missing": sorted(expected_runtime - obs_set),
|
||||
"genuinely_unexpected_new": genuinely_unexpected_new,
|
||||
"missed": genuinely_unexpected_new,
|
||||
"needless": predicted_but_unaffected,
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": x,
|
||||
"status": "CANDIDATE",
|
||||
"note": "genuinely unexpected new surface — evidence required before store update",
|
||||
}
|
||||
for x in genuinely_unexpected_new
|
||||
],
|
||||
"evidence_sources": sorted({item.get("source", "unattributed") for item in observed_evidence}),
|
||||
}
|
||||
|
||||
def snapshot_predicted_frontier(self, doc: dict, contract_id: str, seed_entity_ids: list[str]) -> dict:
|
||||
"""Persist the predicted impact frontier at approval time — the input
|
||||
to closure's predicted-vs-observed comparison."""
|
||||
@@ -537,72 +599,28 @@ class Scaffold:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
if e.get("phase") not in ("superseding", "verifying"):
|
||||
raise IllegalPhaseTransition(f"close requires superseding/verifying phase, got {e.get('phase')}")
|
||||
predicted = e.get("ext", {}).get("predicted_frontier")
|
||||
affected = e.get("identities", {}).get("affected_entities", [])
|
||||
expected_new = set(e.get("identities", {}).get("expected_new_entities", []))
|
||||
# Tranche 5 §14: expected RUNTIME effects (deployment:current advances
|
||||
# etc.) are prediction/accounting metadata, not workflow steps. An
|
||||
# observed entity matching an expected runtime effect is EXPECTED,
|
||||
# not genuinely unexpected — but only when independently observed
|
||||
# (§15: prediction never self-certifies; the evidence item must exist).
|
||||
expected_runtime = set(e.get("ext", {}).get("expected_runtime_effects", []))
|
||||
quality: dict = {"contract": contract_id, "captured_at": now()}
|
||||
|
||||
# independent observed impact (§17): evidence items only
|
||||
obs = record_observed_impact(doc, contract_id, observed_evidence or [])
|
||||
obs_set = set(obs["observed_entities"])
|
||||
|
||||
pred_existing = set(e.get("ext", {}).get("predicted_existing_affected", []))
|
||||
pred_existing |= set(affected)
|
||||
if predicted is not None:
|
||||
pred_existing |= {
|
||||
x
|
||||
for k in ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
for x in predicted.get(k, [])
|
||||
}
|
||||
# R2 Wave 1: the bucket math is factored out so the Rust engine
|
||||
# (scaffold_core.close_quality) computes from the SAME gathered
|
||||
# inputs — the production ledger replay is the parity oracle.
|
||||
inputs = {
|
||||
"contract_id": contract_id,
|
||||
"phase": e.get("phase", "verifying"),
|
||||
"affected_entities": e.get("identities", {}).get("affected_entities", []),
|
||||
"expected_new_entities": e.get("identities", {}).get("expected_new_entities", []),
|
||||
"predicted_existing_affected": e.get("ext", {}).get("predicted_existing_affected", []),
|
||||
"predicted_frontier": e.get("ext", {}).get("predicted_frontier"),
|
||||
"expected_runtime_effects": e.get("ext", {}).get("expected_runtime_effects", []),
|
||||
"observed_evidence": observed_evidence or [],
|
||||
"now": now(),
|
||||
}
|
||||
quality = self._py_close_quality(inputs)
|
||||
quality["contract"] = contract_id
|
||||
quality["captured_at"] = inputs["now"]
|
||||
quality["observed"] = obs["observed_entities"]
|
||||
|
||||
correctly_predicted = sorted(pred_existing & obs_set)
|
||||
predicted_but_unaffected = sorted(pred_existing - obs_set)
|
||||
expected_new_observed = sorted(expected_new & obs_set)
|
||||
expected_new_missing = sorted(expected_new - obs_set)
|
||||
# effect strings are phrases ("deployment:current advances"); an
|
||||
# observed entity matches when an expected effect STARTS with its id
|
||||
matched_runtime = {o for o in obs_set if any(eff.startswith(o) for eff in expected_runtime)}
|
||||
expected_runtime_observed = sorted(
|
||||
eff for eff in expected_runtime if any(eff.startswith(o) for o in matched_runtime)
|
||||
)
|
||||
expected_runtime_missing = sorted(expected_runtime - set(expected_runtime_observed))
|
||||
genuinely_unexpected_new = sorted(obs_set - pred_existing - expected_new - matched_runtime)
|
||||
|
||||
quality.update(
|
||||
{
|
||||
"predicted_existing_affected": sorted(pred_existing),
|
||||
"expected_new_entities": sorted(expected_new),
|
||||
"observed": sorted(obs_set), # legacy union (Tranche 2 readers)
|
||||
"observed_existing": correctly_predicted,
|
||||
"observed_new": sorted(obs_set & (expected_new | (obs_set - pred_existing))),
|
||||
"correctly_predicted": correctly_predicted,
|
||||
"predicted_but_unaffected": predicted_but_unaffected,
|
||||
"expected_new_observed": expected_new_observed,
|
||||
"expected_new_missing": expected_new_missing,
|
||||
"expected_runtime_effects": sorted(expected_runtime),
|
||||
"expected_runtime_observed": expected_runtime_observed,
|
||||
"expected_runtime_missing": sorted(expected_runtime - obs_set),
|
||||
"genuinely_unexpected_new": genuinely_unexpected_new,
|
||||
# legacy aliases (Tranche 2/3 consumers)
|
||||
"missed": genuinely_unexpected_new,
|
||||
"needless": predicted_but_unaffected,
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": x,
|
||||
"status": "CANDIDATE",
|
||||
"note": "genuinely unexpected new surface — evidence required before store update",
|
||||
}
|
||||
for x in genuinely_unexpected_new
|
||||
],
|
||||
"evidence_sources": sorted({item.get("source", "unattributed") for item in (observed_evidence or [])}),
|
||||
}
|
||||
)
|
||||
if scope_files is not None:
|
||||
quality["scope_check"] = self.check_scope(doc, contract_id, scope_files)
|
||||
e.setdefault("ext", {})["contract_quality"] = quality
|
||||
|
||||
+462
-132
@@ -53,6 +53,12 @@ from rune.structural_store import ( # shared generic core (SCAFFOLD-TRANCHE-001
|
||||
reconcile_authorities, mark_conflict, resolve_status, authority_health,
|
||||
dedupe_observations as _ss_dedupe_observations,
|
||||
)
|
||||
from rune.spine_language import ( # noqa: F401 — re-exported (import-compat): the
|
||||
# #2465 doc-level transforms live here now (giant-file carve); tests and
|
||||
# the canary_spine_meta read them THROUGH spine, exactly as before.
|
||||
_classification_sides, _place_languages, _normalize_discovery_ids,
|
||||
_apply_contract_discoveries, _contract_discoveries,
|
||||
)
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
@@ -148,6 +154,141 @@ def _canary_registry() -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
# ── meta-scaffold surface projections (H-meta, contract
|
||||
# BUILDER-2026-10-04T06:24:56Z-cli): every part of rune gets a modeled
|
||||
# entity. These projectors run in the SEEDED derivation path (build()
|
||||
# after carry-forward), so they fire on every rebuild — manifest ingest
|
||||
# alone would only fire on a cold store.
|
||||
|
||||
WORKFLOWS_DIR = ROOT / ".gitea" / "workflows"
|
||||
|
||||
|
||||
def _declared_surfaces() -> dict:
|
||||
"""Umbrella-declared non-module surfaces: rust crates, Go surfaces,
|
||||
configs. The manifest (config/umbrellas.json) is the declaration
|
||||
authority — same law as modules, new kinds."""
|
||||
try:
|
||||
manifest = json.loads((ROOT / "config" / "umbrellas.json").read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {"rust_crates": [], "go_surfaces": [], "configs": []}
|
||||
out = {"rust_crates": [], "go_surfaces": [], "configs": []}
|
||||
for name, u in manifest.get("umbrellas", {}).items():
|
||||
for crate in u.get("rust_crates", []):
|
||||
out["rust_crates"].append((crate, name))
|
||||
for surface in u.get("go_surfaces", []):
|
||||
out["go_surfaces"].append((surface, name))
|
||||
for cfg in u.get("configs", []):
|
||||
out["configs"].append((cfg, name))
|
||||
return out
|
||||
|
||||
|
||||
def project_meta_surfaces(doc: dict, inputs: dict | None = None) -> int:
|
||||
"""Project rust-crate / go-surface / config / workflow entities.
|
||||
|
||||
Deterministic: declared surfaces come from the umbrella manifest,
|
||||
workflows from a sorted filesystem scan. Ids follow the kind:name
|
||||
convention (rust-crate:evidence_provenance, go-surface:rune-entry,
|
||||
config:go-version.txt, workflow:ci.yml).
|
||||
|
||||
H3-spine: when `inputs` is given (the gather-driven path), every
|
||||
filesystem probe comes from the gathered dict so the Rust core and the
|
||||
Python fallback consume byte-identical scans. The no-inputs path gathers
|
||||
on the spot (backward compatibility for direct callers)."""
|
||||
count = 0
|
||||
if inputs is None:
|
||||
inputs = _gather_meta_inputs()
|
||||
manifest = inputs.get("config_manifest") or {}
|
||||
declared = {"rust_crates": [], "go_surfaces": [], "configs": []}
|
||||
for name, u in manifest.get("umbrellas", {}).items():
|
||||
for crate in u.get("rust_crates", []):
|
||||
declared["rust_crates"].append((crate, name))
|
||||
for surface in u.get("go_surfaces", []):
|
||||
declared["go_surfaces"].append((surface, name))
|
||||
for cfg in u.get("configs", []):
|
||||
declared["configs"].append((cfg, name))
|
||||
crate_dirs = set(inputs.get("crate_dirs", []))
|
||||
go_dirs = set(inputs.get("go_dirs", []))
|
||||
config_files = set(inputs.get("config_files", []))
|
||||
crate_canaries = inputs.get("crate_canaries", {})
|
||||
for crate, umbrella in declared["rust_crates"]:
|
||||
cid = f"rust-crate:{crate}"
|
||||
add_entity(doc, id=cid, kind="rust-crate", tags=["code"],
|
||||
identities={"source": f"src/rust/{crate}"},
|
||||
evidence_refs=[f"umbrella-manifest#{umbrella}"],
|
||||
maturity="PRESENT" if crate in crate_dirs else "DECLARED")
|
||||
add_relationship(doc, type="belongs_to", from_id=cid,
|
||||
to_id=f"subsystem:{umbrella}",
|
||||
evidence_refs=[f"umbrella-manifest#{umbrella}"])
|
||||
# crate-resident canaries: projected from the crate dir itself —
|
||||
# they fail the src/rune manifest-canary convention by design
|
||||
# (they verify the crate, they belong to the crate).
|
||||
if crate in crate_dirs:
|
||||
for stem in crate_canaries.get(crate, []):
|
||||
mid = f"module:{stem}"
|
||||
add_entity(doc, id=mid, kind="module", tags=["code", "canary"],
|
||||
identities={"source": f"src/rust/{crate}/{stem}.py"},
|
||||
evidence_refs=[f"crate-scan:{crate}"],
|
||||
maturity="PRESENT")
|
||||
add_relationship(doc, type="obligated_canary", from_id=cid,
|
||||
to_id=mid, evidence_refs=[f"crate-scan:{crate}"])
|
||||
count += 1
|
||||
for surface, umbrella in declared["go_surfaces"]:
|
||||
gid = f"go-surface:{surface}"
|
||||
add_entity(doc, id=gid, kind="go-surface", tags=["code"],
|
||||
identities={"source": f"src/go/{surface}"},
|
||||
evidence_refs=[f"umbrella-manifest#{umbrella}"],
|
||||
maturity="PRESENT" if surface in go_dirs else "DECLARED")
|
||||
add_relationship(doc, type="belongs_to", from_id=gid,
|
||||
to_id=f"subsystem:{umbrella}",
|
||||
evidence_refs=[f"umbrella-manifest#{umbrella}"])
|
||||
count += 1
|
||||
for cfg, umbrella in declared["configs"]:
|
||||
cid = f"config:{cfg}"
|
||||
add_entity(doc, id=cid, kind="config",
|
||||
identities={"source": cfg},
|
||||
evidence_refs=[f"umbrella-manifest#{umbrella}"],
|
||||
maturity="PRESENT" if cfg in config_files else "DECLARED")
|
||||
add_relationship(doc, type="belongs_to", from_id=cid,
|
||||
to_id=f"subsystem:{umbrella}",
|
||||
evidence_refs=[f"umbrella-manifest#{umbrella}"])
|
||||
count += 1
|
||||
for wf in inputs.get("workflows", []):
|
||||
wid = f"workflow:{wf}"
|
||||
add_entity(doc, id=wid, kind="workflow", tags=["infra"],
|
||||
identities={"source": f".gitea/workflows/{wf}"},
|
||||
evidence_refs=["workflow-scan"],
|
||||
maturity="PRESENT")
|
||||
add_relationship(doc, type="belongs_to", from_id=wid,
|
||||
to_id="subsystem:governance",
|
||||
evidence_refs=["workflow-scan"])
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def _gather_meta_inputs() -> dict:
|
||||
"""The meta-surface scan slice (shared by the full build gather)."""
|
||||
declared = _declared_surfaces()
|
||||
crate_dirs = [c for c, _ in declared["rust_crates"]
|
||||
if (ROOT / "src" / "rust" / c).is_dir()]
|
||||
try:
|
||||
config_manifest = json.loads((ROOT / "config" / "umbrellas.json").read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
config_manifest = None
|
||||
return {
|
||||
"config_manifest": config_manifest,
|
||||
"crate_dirs": crate_dirs,
|
||||
"crate_canaries": {
|
||||
c: sorted(cf.stem for cf in (ROOT / "src" / "rust" / c).glob("canary_*.py"))
|
||||
for c in crate_dirs
|
||||
},
|
||||
"go_dirs": [g for g, _ in declared["go_surfaces"]
|
||||
if (ROOT / "src" / "go" / g).is_dir()],
|
||||
"config_files": [cfg for cfg, _ in declared["configs"] if (ROOT / cfg).is_file()],
|
||||
"workflows": (sorted(wf.name for wf in WORKFLOWS_DIR.glob("*.yml"))
|
||||
if WORKFLOWS_DIR.is_dir() else []),
|
||||
}
|
||||
|
||||
|
||||
def _deployed_observation() -> dict | None:
|
||||
"""Observe the deployment authority — never invent it."""
|
||||
ident = DEPLOYED_LINK / "src" / "rune" / "_build_identity.json"
|
||||
@@ -174,15 +315,248 @@ def ingest_umbrellas(doc: dict, manifest_path: Path | None = None) -> int:
|
||||
"""Phase A: translate the (last manually maintained) umbrella manifest
|
||||
into Spine entities + relationships. Refuses a Spine-GENERATED view —
|
||||
that guards the ingest cycle after Phase C makes the view output-only.
|
||||
Returns count of subsystems ingested."""
|
||||
Returns count of subsystems ingested. (File-driven wrapper; the logic
|
||||
lives in _ingest_umbrellas_data so the gathered path shares it.)"""
|
||||
path = manifest_path or UMBRELLA_MANIFEST
|
||||
text = path.read_text()
|
||||
data = json.loads(text)
|
||||
data = json.loads(path.read_text())
|
||||
if GENERATED_VIEW_MARKER in data:
|
||||
raise ValueError(
|
||||
f"{path} is a Spine-generated compatibility view; refusing to "
|
||||
"ingest it (Phase A reads the manual authority exactly once). "
|
||||
"Structural membership now lives in the Spine store.")
|
||||
return _ingest_umbrellas_data(doc, data)
|
||||
|
||||
|
||||
def _rust_core():
|
||||
"""The Rust spine core when available (strangler: Python fallback is
|
||||
load-bearing until deletion is proven). RUNE_SPINE_CORE=python pins the
|
||||
fallback (parity harnesses and pre-cargo environments)."""
|
||||
if os.environ.get("RUNE_SPINE_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import spine_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken build
|
||||
return None
|
||||
try:
|
||||
return spine_loader if spine_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def _gather_build_inputs(manifest_path: Path | None = None) -> dict:
|
||||
"""Every I/O read build() needs, in one place (zero-dep split law:
|
||||
Python scans, the core — Rust or Python — only assembles). Both cores
|
||||
consume this dict, so parity divergence is logic, never environment."""
|
||||
existing = load()
|
||||
if existing:
|
||||
_normalize_discovery_ids(existing)
|
||||
seeded = bool(existing) and any(
|
||||
r["type"] == "belongs_to" for r in existing["relationships"])
|
||||
# ingest manifest: only consulted when the unseeded path needs it
|
||||
# (faithful to the original build(), which never touches the file when
|
||||
# seeded). The parse is TOLERANT here — the Python fallback goes through
|
||||
# the public ingest_umbrellas (which reads + raises like the original,
|
||||
# and stays monkeypatchable for isolation tests); the Rust core consumes
|
||||
# the parsed data on the delegated path.
|
||||
ingest_manifest = None
|
||||
ingest_path = None
|
||||
if not seeded:
|
||||
ingest_path = str(Path(manifest_path) if manifest_path else UMBRELLA_MANIFEST)
|
||||
try:
|
||||
ingest_manifest = json.loads(Path(ingest_path).read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
ingest_manifest = None
|
||||
inputs = {
|
||||
"now": now(),
|
||||
"existing": existing,
|
||||
"manifest": ingest_manifest,
|
||||
"manifest_path": ingest_path,
|
||||
"graph": {m: sorted(deps) for m, deps in _import_graph().items()},
|
||||
"registry": _canary_registry(),
|
||||
"tested": sorted(_test_backed_modules()),
|
||||
"main_present": (SRC_DIR / "__main__.py").is_file(),
|
||||
"present_modules": [p.stem for p in SRC_DIR.glob("*.py")],
|
||||
"deployed_obs": _deployed_observation(),
|
||||
"deployed_sources": _deployed_source_files(),
|
||||
"canary_files": [p.stem for p in SRC_DIR.glob("canary_*.py")],
|
||||
}
|
||||
inputs.update(_gather_meta_inputs())
|
||||
return inputs
|
||||
|
||||
|
||||
def _deployed_source_files() -> list[str]:
|
||||
"""Relative paths under DEPLOYED_LINK that exist as files — the gathered
|
||||
form of `(DEPLOYED_LINK / source).is_file()` for module sources."""
|
||||
out: list[str] = []
|
||||
for base, pattern in ((DEPLOYED_LINK / "src" / "rune", "*.py"),
|
||||
(DEPLOYED_LINK / "src" / "rust", "*/canary_*.py")):
|
||||
if base.is_dir():
|
||||
for p in base.glob(pattern):
|
||||
if p.is_file():
|
||||
out.append(p.relative_to(DEPLOYED_LINK).as_posix())
|
||||
return out
|
||||
|
||||
|
||||
def _py_build_from_inputs(inputs: dict) -> dict:
|
||||
"""Deterministic full projection from GATHERED inputs. Same inputs →
|
||||
byte-identical doc (modulo generated_at). This is the Python core; the
|
||||
Rust core (project.rs) must produce structurally identical output."""
|
||||
doc = new_doc()
|
||||
ts = inputs["now"]
|
||||
|
||||
# 1. subsystem structure — from the Spine store if it already owns
|
||||
# membership (Phase B+), else from the manual manifest (Phase A).
|
||||
existing = inputs["existing"]
|
||||
seeded = False
|
||||
if existing:
|
||||
rels = [r for r in existing["relationships"] if r["type"] == "belongs_to"]
|
||||
if rels:
|
||||
seeded = True
|
||||
# CARRY EVERYTHING FORWARD — open-world requirement. Filtering
|
||||
# by known kinds/types here would silently destroy tasks,
|
||||
# contracts, capabilities, and any future entity class on every
|
||||
# refresh (the exact TOO-CLOSED failure the contract forbids;
|
||||
# observed live: task bindings vanished through build()).
|
||||
# Module-level facts (imports, tests, maturity) are RE-DERIVED
|
||||
# below onto the carried state; everything else passes through.
|
||||
doc["entities"].extend(json.loads(json.dumps(existing["entities"])))
|
||||
doc["relationships"].extend(json.loads(json.dumps(existing["relationships"])))
|
||||
doc["observations"].extend(json.loads(json.dumps(
|
||||
existing.get("observations", []))))
|
||||
if not seeded:
|
||||
mp = inputs.get("manifest_path")
|
||||
if inputs.get("manifest") is not None and not mp:
|
||||
# gather-supplied manifest data (the Rust-parity path): ingest
|
||||
# the dict directly — no file to re-read
|
||||
_ingest_umbrellas_data(doc, inputs["manifest"])
|
||||
else:
|
||||
# through the PUBLIC ingest_umbrellas (reads + raises like the
|
||||
# original build; monkeypatchable for isolation tests)
|
||||
ingest_umbrellas(doc, Path(mp) if mp else None)
|
||||
|
||||
# 2. module-level structural facts from actual code
|
||||
graph = inputs["graph"]
|
||||
registry = inputs["registry"]
|
||||
tested = set(inputs["tested"])
|
||||
present = set(inputs["present_modules"])
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
# T21.4 §21.4G: the ENTRY POINT is invisible to the import graph
|
||||
# (nothing imports __main__) but is a real runtime path — seed it
|
||||
# deterministically whenever the file exists.
|
||||
if inputs["main_present"] and "__main__" not in mods:
|
||||
add_entity(doc, id="module:__main__", kind="module", maturity="PRESENT",
|
||||
identities={"path": "src/rune/__main__.py",
|
||||
"role": "TUI entry point (python3 -m rune)"},
|
||||
evidence_refs=["entry-point:python3 -m rune"])
|
||||
mods.add("__main__")
|
||||
for m in sorted(mods):
|
||||
mid = f"module:{m}"
|
||||
e = entity(doc, mid)
|
||||
# Re-derivation uses the HIGH-WATER path (add_entity upgrades only,
|
||||
# never logs): fresh evidence may be lower than the store's earned
|
||||
# state on any given pass, and that is not a demotion event — it is
|
||||
# a build. Explicit promote() logging is for evidence-gated API calls.
|
||||
def _derive(state: str, ev: str) -> None:
|
||||
add_entity(doc, id=mid, kind="module", maturity=state,
|
||||
evidence_refs=[ev])
|
||||
|
||||
if m in present:
|
||||
assert e is not None # membership loop guarantees presence
|
||||
e["identities"]["source"] = f"src/rune/{m}.py"
|
||||
_derive("PRESENT", f"source:{m}.py")
|
||||
consumers = sorted(c for c, deps in graph.items() if m in deps and c != m)
|
||||
deps = sorted(d for d in graph.get(m, []) if d in mods and d != m)
|
||||
for c in consumers:
|
||||
rel = add_relationship(doc, type="consumed_by", from_id=mid,
|
||||
to_id=f"module:{c}", evidence_refs=[f"ast-import:{m}"])
|
||||
_authority_observed(doc, rel, "ast-import-projector")
|
||||
for d in deps:
|
||||
rel = add_relationship(doc, type="depends_on", from_id=mid,
|
||||
to_id=f"module:{d}", evidence_refs=[f"ast-import:{m}"])
|
||||
_authority_observed(doc, rel, "ast-import-projector")
|
||||
# WIRED: importers exist, or registry wiring (canary registry)
|
||||
if consumers or m in registry:
|
||||
_derive("WIRED", f"ast-imports:{len(consumers)}" if consumers
|
||||
else "canary-registry")
|
||||
# EXERCISED: dedicated test or canary exercise
|
||||
if m in tested:
|
||||
_derive("EXERCISED", f"tests/test_{m}.py")
|
||||
elif m in registry:
|
||||
_derive("EXERCISED", "canary-registry-exercise")
|
||||
|
||||
# 3. runtime/deployment observation (dynamic, bounded)
|
||||
obs = inputs["deployed_obs"]
|
||||
if obs:
|
||||
_authority_observed(doc, None, "deploy-identity-projector") # generation bump
|
||||
deployed_sources = set(inputs["deployed_sources"])
|
||||
dep = add_entity(doc, id="deployment:current", kind="deployment_surface",
|
||||
identities=obs, evidence_refs=["_build_identity.json"],
|
||||
maturity="LIVE_PROVEN")
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] == "module" and e["identities"].get("source", "") in deployed_sources:
|
||||
rel = add_relationship(doc, type="deployed_as", from_id=e["id"],
|
||||
to_id="deployment:current",
|
||||
evidence_refs=[f"deployed-tree:{obs['deployed_sha']}"])
|
||||
_authority_observed(doc, rel, "deploy-identity-projector")
|
||||
doc["observations"].append({"kind": "deployed_identity", **obs, "observed_at": ts})
|
||||
|
||||
# 4. meta-scaffold surface projection (H-meta): rust crates, Go
|
||||
# surfaces, configs, workflows — every part of rune modeled.
|
||||
projected = project_meta_surfaces(doc, inputs)
|
||||
if projected:
|
||||
_authority_observed(doc, None, "meta-surface-projector")
|
||||
|
||||
# 4b. manifest canaries as first-class entities (H-meta): the seeded
|
||||
# path skips ingest, and ingest only made the EDGE — the canary
|
||||
# entity itself was never created, leaving obligated_canary edges
|
||||
# pointing at nothing (observed live: zero canary entities in the
|
||||
# spine before this projector).
|
||||
manifest = inputs.get("config_manifest")
|
||||
if manifest:
|
||||
canary_files = set(inputs["canary_files"])
|
||||
for name, u in manifest.get("umbrellas", {}).items():
|
||||
for canary in u.get("canaries", []):
|
||||
mid = f"module:{canary}"
|
||||
add_entity(doc, id=mid, kind="module", tags=["code", "canary"],
|
||||
identities={"source": f"src/rune/{canary}.py"},
|
||||
evidence_refs=[f"umbrella-manifest#{name}"],
|
||||
maturity="PRESENT" if canary in canary_files else "DECLARED")
|
||||
add_relationship(doc, type="obligated_canary",
|
||||
from_id=f"subsystem:{name}", to_id=mid,
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
|
||||
|
||||
# reconcile: facts owned by projectors that no longer observe them
|
||||
# become STALE (marked, never deleted — history survives; open-world
|
||||
# preservation applies to everything, staleness is the truth marker)
|
||||
staled = reconcile_authorities(doc)
|
||||
for s in staled:
|
||||
doc["observations"].append({"kind": "fact_staled", **s, "observed_at": ts})
|
||||
|
||||
# exact-duplicate observation suppression (carry-forward + this pass
|
||||
# both record deployed_identity etc.); distinct repeats are history
|
||||
seen_obs = set()
|
||||
unique_obs = []
|
||||
for o in doc["observations"]:
|
||||
key = json.dumps(_sorted(o), sort_keys=True)
|
||||
if key not in seen_obs:
|
||||
seen_obs.add(key)
|
||||
unique_obs.append(o)
|
||||
doc["observations"] = unique_obs
|
||||
doc["generated_at"] = ts
|
||||
return doc
|
||||
|
||||
|
||||
def _ingest_umbrellas_data(doc: dict, data: dict) -> int:
|
||||
"""The original ingest_umbrellas with the file read lifted out (H3-spine
|
||||
gather boundary). Raises ValueError on a Spine-generated view, exactly
|
||||
like the file-driven original."""
|
||||
if GENERATED_VIEW_MARKER in data:
|
||||
raise ValueError(
|
||||
"the manifest is a Spine-generated compatibility view; refusing to "
|
||||
"ingest it (Phase A reads the manual authority exactly once). "
|
||||
"Structural membership now lives in the Spine store.")
|
||||
count = 0
|
||||
KNOWN_FIELDS = {"description", "modules", "depends_on", "mirrors", "canaries"}
|
||||
for name, u in data.get("umbrellas", {}).items():
|
||||
@@ -219,125 +593,57 @@ def ingest_umbrellas(doc: dict, manifest_path: Path | None = None) -> int:
|
||||
|
||||
def build(manifest_path: Path | None = None) -> dict:
|
||||
"""Deterministic full projection from authorities. Same inputs →
|
||||
byte-identical doc (modulo generated_at)."""
|
||||
doc = new_doc()
|
||||
|
||||
# 1. subsystem structure — from the Spine store if it already owns
|
||||
# membership (Phase B+), else from the manual manifest (Phase A).
|
||||
existing = load()
|
||||
seeded = False
|
||||
if existing:
|
||||
rels = [r for r in existing["relationships"] if r["type"] == "belongs_to"]
|
||||
if rels:
|
||||
seeded = True
|
||||
# CARRY EVERYTHING FORWARD — open-world requirement. Filtering
|
||||
# by known kinds/types here would silently destroy tasks,
|
||||
# contracts, capabilities, and any future entity class on every
|
||||
# refresh (the exact TOO-CLOSED failure the contract forbids;
|
||||
# observed live: task bindings vanished through build()).
|
||||
# Module-level facts (imports, tests, maturity) are RE-DERIVED
|
||||
# below onto the carried state; everything else passes through.
|
||||
doc["entities"].extend(json.loads(json.dumps(existing["entities"])))
|
||||
doc["relationships"].extend(json.loads(json.dumps(existing["relationships"])))
|
||||
doc["observations"].extend(json.loads(json.dumps(
|
||||
existing.get("observations", []))))
|
||||
if not seeded:
|
||||
ingest_umbrellas(doc, manifest_path)
|
||||
|
||||
# 2. module-level structural facts from actual code
|
||||
graph = _import_graph()
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
# T21.4 §21.4G: the ENTRY POINT is invisible to the import graph
|
||||
# (nothing imports __main__) but is a real runtime path — seed it
|
||||
# deterministically whenever the file exists.
|
||||
if (SRC_DIR / "__main__.py").is_file() and "__main__" not in mods:
|
||||
add_entity(doc, id="module:__main__", kind="module", maturity="PRESENT",
|
||||
identities={"path": "src/rune/__main__.py",
|
||||
"role": "TUI entry point (python3 -m rune)"},
|
||||
evidence_refs=["entry-point:python3 -m rune"])
|
||||
mods.add("__main__")
|
||||
for m in sorted(mods):
|
||||
mid = f"module:{m}"
|
||||
e = entity(doc, mid)
|
||||
# Re-derivation uses the HIGH-WATER path (add_entity upgrades only,
|
||||
# never logs): fresh evidence may be lower than the store's earned
|
||||
# state on any given pass, and that is not a demotion event — it is
|
||||
# a build. Explicit promote() logging is for evidence-gated API calls.
|
||||
def _derive(state: str, ev: str) -> None:
|
||||
add_entity(doc, id=mid, kind="module", maturity=state,
|
||||
evidence_refs=[ev])
|
||||
|
||||
if (SRC_DIR / f"{m}.py").is_file():
|
||||
assert e is not None # membership loop guarantees presence
|
||||
e["identities"]["source"] = f"src/rune/{m}.py"
|
||||
_derive("PRESENT", f"source:{m}.py")
|
||||
consumers = sorted(c for c, deps in graph.items() if m in deps and c != m)
|
||||
deps = sorted(d for d in graph.get(m, set()) if d in mods and d != m)
|
||||
for c in consumers:
|
||||
rel = add_relationship(doc, type="consumed_by", from_id=mid,
|
||||
to_id=f"module:{c}", evidence_refs=[f"ast-import:{m}"])
|
||||
_authority_observed(doc, rel, "ast-import-projector")
|
||||
for d in deps:
|
||||
rel = add_relationship(doc, type="depends_on", from_id=mid,
|
||||
to_id=f"module:{d}", evidence_refs=[f"ast-import:{m}"])
|
||||
_authority_observed(doc, rel, "ast-import-projector")
|
||||
# WIRED: importers exist, or registry wiring (canary registry)
|
||||
if consumers or m in registry:
|
||||
_derive("WIRED", f"ast-imports:{len(consumers)}" if consumers
|
||||
else "canary-registry")
|
||||
# EXERCISED: dedicated test or canary exercise
|
||||
if m in tested:
|
||||
_derive("EXERCISED", f"tests/test_{m}.py")
|
||||
elif m in registry:
|
||||
_derive("EXERCISED", "canary-registry-exercise")
|
||||
|
||||
# 3. runtime/deployment observation (dynamic, bounded)
|
||||
obs = _deployed_observation()
|
||||
if obs:
|
||||
_authority_observed(doc, None, "deploy-identity-projector") # generation bump
|
||||
dep = add_entity(doc, id="deployment:current", kind="deployment_surface",
|
||||
identities=obs, evidence_refs=["_build_identity.json"],
|
||||
maturity="LIVE_PROVEN")
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] == "module" and (DEPLOYED_LINK / e["identities"].get("source", "")).is_file():
|
||||
rel = add_relationship(doc, type="deployed_as", from_id=e["id"],
|
||||
to_id="deployment:current",
|
||||
evidence_refs=[f"deployed-tree:{obs['deployed_sha']}"])
|
||||
_authority_observed(doc, rel, "deploy-identity-projector")
|
||||
doc["observations"].append({"kind": "deployed_identity", **obs, "observed_at": now()})
|
||||
|
||||
# reconcile: facts owned by projectors that no longer observe them
|
||||
# become STALE (marked, never deleted — history survives; open-world
|
||||
# preservation applies to everything, staleness is the truth marker)
|
||||
staled = reconcile_authorities(doc)
|
||||
for s in staled:
|
||||
doc["observations"].append({"kind": "fact_staled", **s, "observed_at": now()})
|
||||
|
||||
# exact-duplicate observation suppression (carry-forward + this pass
|
||||
# both record deployed_identity etc.); distinct repeats are history
|
||||
seen_obs = set()
|
||||
unique_obs = []
|
||||
for o in doc["observations"]:
|
||||
key = json.dumps(_sorted(o), sort_keys=True)
|
||||
if key not in seen_obs:
|
||||
seen_obs.add(key)
|
||||
unique_obs.append(o)
|
||||
doc["observations"] = unique_obs
|
||||
doc["generated_at"] = now()
|
||||
byte-identical doc (modulo generated_at). H3-spine: gathers the scans
|
||||
once, then delegates to the Rust core when built (Python fallback —
|
||||
strangler law: the old path stays load-bearing until deletion is proven)."""
|
||||
inputs = _gather_build_inputs(manifest_path)
|
||||
core = _rust_core()
|
||||
# The unseeded path is the one-time Phase A migration: it stays on the
|
||||
# Python core so the public ingest_umbrellas seam (monkeypatched by
|
||||
# isolation tests) is honored byte-for-byte. Steady-state (seeded)
|
||||
# builds delegate to the Rust core; the parity battery covers BOTH
|
||||
# paths, including an explicit unseeded-ingest parity section.
|
||||
seeded = bool(inputs["existing"]) and any(
|
||||
r["type"] == "belongs_to" for r in inputs["existing"]["relationships"])
|
||||
doc = (core.project(inputs) if core is not None and seeded
|
||||
else _py_build_from_inputs(inputs))
|
||||
# both cores funnel through the same post-projection passes (the
|
||||
# contract-discovery write-back + language placement) — the doc-level
|
||||
# transforms live in Python until the #2464 top-up ports them
|
||||
_apply_contract_discoveries(doc)
|
||||
_place_languages(doc)
|
||||
return doc
|
||||
|
||||
|
||||
# ── drift ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def drift(doc: dict) -> list[dict]:
|
||||
"""Architectural drift classes. Reuses authorities' verdicts; never
|
||||
duplicates their logic. Deterministic output."""
|
||||
def _gather_drift_inputs() -> dict:
|
||||
"""Every I/O read drift() needs (zero-dep split law — same gathered dict
|
||||
feeds the Rust core and the Python fallback)."""
|
||||
try:
|
||||
src_version = (ROOT / "pyproject.toml").read_text()
|
||||
import re
|
||||
m = re.search(r'^version\s*=\s*"([^"]+)"', src_version, re.M)
|
||||
src_version = m.group(1) if m else ""
|
||||
except OSError:
|
||||
src_version = ""
|
||||
return {
|
||||
"registry": _canary_registry(),
|
||||
"graph": {m: sorted(deps) for m, deps in _import_graph().items()},
|
||||
"tested": sorted(_test_backed_modules()),
|
||||
"src_stems": [p.stem for p in SRC_DIR.glob("*.py")],
|
||||
"deployed_obs": _deployed_observation(),
|
||||
"src_version": src_version,
|
||||
}
|
||||
|
||||
|
||||
def _py_drift_from_inputs(doc: dict, inputs: dict) -> list[dict]:
|
||||
"""Architectural drift classes from GATHERED inputs. Python core; the
|
||||
Rust core (drift.rs) must produce identical output on the same inputs."""
|
||||
out = []
|
||||
graph = _import_graph()
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
graph = inputs["graph"]
|
||||
registry = inputs["registry"]
|
||||
tested = set(inputs["tested"])
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
|
||||
# 1. registered but absent (canary registry names a non-existent module)
|
||||
@@ -349,11 +655,11 @@ def drift(doc: dict) -> list[dict]:
|
||||
# 2. present but unowned (runtime source file outside every subsystem).
|
||||
# Excludes canary files (governed by the canary contract, not umbrella
|
||||
# membership) and package plumbing (__init__/__main__).
|
||||
for p in SRC_DIR.glob("*.py"):
|
||||
if p.stem.startswith(("canary_", "__")):
|
||||
for stem in inputs["src_stems"]:
|
||||
if stem.startswith(("canary_", "__")):
|
||||
continue
|
||||
if p.stem not in mods:
|
||||
out.append({"class": "present_but_unowned", "entity": f"module:{p.stem}",
|
||||
if stem not in mods:
|
||||
out.append({"class": "present_but_unowned", "entity": f"module:{stem}",
|
||||
"detail": "src/rune runtime module with no subsystem membership"})
|
||||
|
||||
# 3. registered but no static importers — OBSERVATION, not a death
|
||||
@@ -368,14 +674,8 @@ def drift(doc: dict) -> list[dict]:
|
||||
"detail": f"zero static importers; dynamic wiring possible; {dyn}"})
|
||||
|
||||
# 4. deployed identity drift (source version vs deployment authority)
|
||||
obs = _deployed_observation()
|
||||
try:
|
||||
src_version = (ROOT / "pyproject.toml").read_text()
|
||||
import re
|
||||
m = re.search(r'^version\s*=\s*"([^"]+)"', src_version, re.M)
|
||||
src_version = m.group(1) if m else ""
|
||||
except OSError:
|
||||
src_version = ""
|
||||
obs = inputs["deployed_obs"]
|
||||
src_version = inputs["src_version"]
|
||||
if obs and src_version and obs["deployed_version"] != src_version:
|
||||
out.append({"class": "version_surfaces_disagree",
|
||||
"detail": f"source {src_version} != deployed {obs['deployed_version']}"})
|
||||
@@ -405,6 +705,17 @@ def drift(doc: dict) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def drift(doc: dict) -> list[dict]:
|
||||
"""Architectural drift classes. Reuses authorities' verdicts; never
|
||||
duplicates their logic. Deterministic output. H3-spine: gathers the
|
||||
scans once, delegates to the Rust core when built (Python fallback)."""
|
||||
inputs = _gather_drift_inputs()
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
return core.drift(doc, inputs)
|
||||
return _py_drift_from_inputs(doc, inputs)
|
||||
|
||||
|
||||
# ── snapshots / deltas ────────────────────────────────────────────────────
|
||||
|
||||
def snapshot(doc: dict, label: str = "") -> Path:
|
||||
@@ -421,9 +732,9 @@ def snapshot(doc: dict, label: str = "") -> Path:
|
||||
|
||||
# ── compatibility projection (Phase C) ────────────────────────────────────
|
||||
|
||||
def export_umbrellas(doc: dict) -> dict:
|
||||
"""Generate the legacy umbrella view FROM Spine. Output only — the old
|
||||
consumers (CI gate, henxels) read this during migration. Never ingest."""
|
||||
def _py_export_umbrellas(doc: dict) -> dict:
|
||||
"""Python core of export_umbrellas (H3-spine: the Rust core in query.rs
|
||||
must produce identical output modulo generated_at)."""
|
||||
umbrellas: dict = {}
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "subsystem":
|
||||
@@ -464,6 +775,16 @@ def export_umbrellas(doc: dict) -> dict:
|
||||
"umbrellas": dict(sorted(umbrellas.items()))}
|
||||
|
||||
|
||||
def export_umbrellas(doc: dict) -> dict:
|
||||
"""Generate the legacy umbrella view FROM Spine. Output only — the old
|
||||
consumers (CI gate, henxels) read this during migration. Never ingest.
|
||||
H3-spine: delegates to the Rust core when built (Python fallback)."""
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
return core.export_umbrellas(doc, now())
|
||||
return _py_export_umbrellas(doc)
|
||||
|
||||
|
||||
# ── contracts, task binding, impact frontier (T22 + graph addenda) ────────
|
||||
#
|
||||
# GRAPH SEPARATION: Spine is its own LOGICAL graph — current structural
|
||||
@@ -503,7 +824,16 @@ FRONTIER_LEVEL2_TYPES = {"verified_by", "obligated_canary"}
|
||||
|
||||
def query(doc: dict, entity_id: str) -> dict:
|
||||
"""INTRINSIC SELF-KNOWLEDGE: one deterministic call answering the
|
||||
representative structural question set. No LLM, no Memory, no rediscovery."""
|
||||
representative structural question set. No LLM, no Memory, no rediscovery.
|
||||
H3-spine: delegates to the Rust core when built (Python fallback)."""
|
||||
core = _rust_core()
|
||||
if core is not None:
|
||||
return core.query(doc, entity_id)
|
||||
return _py_query(doc, entity_id)
|
||||
|
||||
|
||||
def _py_query(doc: dict, entity_id: str) -> dict:
|
||||
"""Python core of query (parity oracle for the Rust core)."""
|
||||
e = entity(doc, entity_id)
|
||||
if e is None:
|
||||
return {"error": "not found", "id": entity_id}
|
||||
|
||||
Loaded 100 of 228 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user