Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3497d2b7a9 | ||
|
|
60a21dc7da | ||
|
|
0873b521e3 | ||
|
|
f7e6c5cff5 | ||
|
|
fc46dbea52 | ||
|
|
d8ede8f3f8 | ||
|
|
804c2f62df | ||
|
|
e5d10f18ae | ||
|
|
3ed58d48a9 | ||
|
|
6c997b6af4 | ||
|
|
9b5befb137 | ||
|
|
6899d859da | ||
|
|
8ef70448c3 | ||
|
|
55f268f978 | ||
|
|
320026a4a3 | ||
|
|
0e88b6bc08 | ||
|
|
004e5b3118 | ||
|
|
db15ae3650 | ||
|
|
d8ce21b76d | ||
|
|
d4d43dbf67 | ||
|
|
8a6c950140 | ||
|
|
d67fc46581 | ||
|
|
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 |
No files matched your search
@@ -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;
|
||||
@@ -72,7 +73,8 @@ jobs:
|
||||
const managedLabels = new Set([
|
||||
'memory', 'governance', 'identity', 'growth',
|
||||
'perception', 'infrastructure', 'life', 'custodian',
|
||||
'priority: high', 'bug', 'feature', 'research',
|
||||
'bug', 'feature', 'research',
|
||||
'P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low',
|
||||
]);
|
||||
|
||||
// Type label: DECLARED over GUESSED. The template spine
|
||||
@@ -135,9 +137,31 @@ jobs:
|
||||
desiredLabels.add('custodian');
|
||||
}
|
||||
|
||||
// Priority labels
|
||||
if (text.includes('urgent') || text.includes('critical') || text.includes('broken')) {
|
||||
desiredLabels.add('priority: high');
|
||||
// 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;
|
||||
@@ -147,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)
|
||||
);
|
||||
@@ -156,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) {
|
||||
@@ -178,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'}`);
|
||||
@@ -14,3 +14,4 @@ state/
|
||||
# Runtime artifacts (cwd-local event journal + capability index)
|
||||
index/
|
||||
journal/
|
||||
src/rust/*/target/
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.92.2**
|
||||
**Current release: v0.99.5**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
+316
-314
@@ -1,317 +1,319 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-16T18:23:38Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"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",
|
||||
"route_risk_flags",
|
||||
"tool_executor",
|
||||
"tool_protocol"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01",
|
||||
"canary_rune_entry",
|
||||
"canary_rune_serve"
|
||||
],
|
||||
"go_surfaces": [
|
||||
"rune-entry"
|
||||
],
|
||||
"configs": [],
|
||||
"rust_crates": [
|
||||
"capability_scoring"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"audit_ledger",
|
||||
"build_pipeline_policy",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"curator",
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer",
|
||||
"programme_gates",
|
||||
"sc_loader"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration",
|
||||
"canary_audit_ledger",
|
||||
"canary_spine_meta"
|
||||
],
|
||||
"rust_crates": [
|
||||
"evidence_provenance",
|
||||
"scaffold_core"
|
||||
],
|
||||
"configs": [
|
||||
".coveragerc",
|
||||
"go-version.txt",
|
||||
"lint-baseline.json",
|
||||
"pyright-baseline.json"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"elastic_cognition",
|
||||
"goal",
|
||||
"growth_delta",
|
||||
"growth_simulator",
|
||||
"self_improve",
|
||||
"subconscious_pass"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"behavior",
|
||||
"identity_anchors",
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"prompt_sync",
|
||||
"voice_hybrid",
|
||||
"voice_injector",
|
||||
"voice_sync",
|
||||
"voicebox"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"inference_trace",
|
||||
"model_connect",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_routing",
|
||||
"provider_budget",
|
||||
"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": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"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",
|
||||
"shard_manager",
|
||||
"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": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"auto_detect",
|
||||
"build_mode",
|
||||
"build_orchestrator",
|
||||
"chat_bubble",
|
||||
"cron_engine",
|
||||
"custodian",
|
||||
"dependency_tracker",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"hook_engine",
|
||||
"message_widgets",
|
||||
"microkernel",
|
||||
"night_mode",
|
||||
"outcome_ledger",
|
||||
"parallel_processor",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"rune_graphic",
|
||||
"rune_operator",
|
||||
"rune_profiler",
|
||||
"rune_spinner",
|
||||
"rune_tui",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"subsystem_health",
|
||||
"system_registry",
|
||||
"task_gate",
|
||||
"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",
|
||||
"canary_tui_transport"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"behaviour_ledger",
|
||||
"build_basis",
|
||||
"capability_fit",
|
||||
"capability_ledger",
|
||||
"cognitive_budgeter",
|
||||
"construction_activation",
|
||||
"construction_commands",
|
||||
"construction_decision",
|
||||
"construction_depth",
|
||||
"construction_intent",
|
||||
"construction_interface",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"construction_projection",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"drift_resume",
|
||||
"event_wait",
|
||||
"failure_recovery",
|
||||
"graph_projection",
|
||||
"impact_preview",
|
||||
"iteration",
|
||||
"layout_contract",
|
||||
"programme_continuation",
|
||||
"project_registry",
|
||||
"release_notes",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"spine_loader",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
"structural_store",
|
||||
"testing_mode",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"wait_guard",
|
||||
"wait_guard_corpus",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"rust_crates": [
|
||||
"rune_spine"
|
||||
]
|
||||
}
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"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",
|
||||
"route_risk_flags",
|
||||
"tool_executor",
|
||||
"tool_protocol"
|
||||
],
|
||||
"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",
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer",
|
||||
"programme_gates",
|
||||
"sc_loader"
|
||||
],
|
||||
"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",
|
||||
"dream_cycle",
|
||||
"elastic_cognition",
|
||||
"goal",
|
||||
"growth_delta",
|
||||
"growth_simulator",
|
||||
"self_improve",
|
||||
"subconscious_pass"
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"behavior",
|
||||
"identity_anchors",
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"prompt_sync",
|
||||
"voice_hybrid",
|
||||
"voice_injector",
|
||||
"voice_sync",
|
||||
"voicebox"
|
||||
]
|
||||
},
|
||||
"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",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_routing",
|
||||
"provider_budget",
|
||||
"provider_registry",
|
||||
"response_adapter",
|
||||
"standardized_providers",
|
||||
"tc_loader",
|
||||
"token_optimizer"
|
||||
],
|
||||
"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",
|
||||
"shard_manager",
|
||||
"tier_manager",
|
||||
"warm_neuron_store"
|
||||
],
|
||||
"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",
|
||||
"build_orchestrator",
|
||||
"chat_bubble",
|
||||
"cron_engine",
|
||||
"custodian",
|
||||
"dependency_tracker",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"hook_engine",
|
||||
"message_widgets",
|
||||
"microkernel",
|
||||
"night_mode",
|
||||
"outcome_ledger",
|
||||
"parallel_processor",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"rune_graphic",
|
||||
"rune_operator",
|
||||
"rune_profiler",
|
||||
"rune_spinner",
|
||||
"rune_tui",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"subsystem_health",
|
||||
"system_registry",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"tui_transport",
|
||||
"tui_validator",
|
||||
"umbrella_update",
|
||||
"verification_layer",
|
||||
"warm_router",
|
||||
"work_manager"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"description": "Input processing and context assembly",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"behaviour_ledger",
|
||||
"build_basis",
|
||||
"capability_fit",
|
||||
"capability_ledger",
|
||||
"cognitive_budgeter",
|
||||
"construction_activation",
|
||||
"construction_commands",
|
||||
"construction_decision",
|
||||
"construction_depth",
|
||||
"construction_intent",
|
||||
"construction_interface",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"construction_projection",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"drift_resume",
|
||||
"event_wait",
|
||||
"failure_recovery",
|
||||
"graph_projection",
|
||||
"impact_preview",
|
||||
"iteration",
|
||||
"layout_contract",
|
||||
"programme_continuation",
|
||||
"project_registry",
|
||||
"release_notes",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"spine_language",
|
||||
"spine_loader",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
"structural_store",
|
||||
"testing_mode",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"wait_guard",
|
||||
"wait_guard_corpus",
|
||||
"witness"
|
||||
],
|
||||
"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.
|
||||
@@ -218,6 +218,35 @@ critiques (2026-10-04):
|
||||
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
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.92.2"
|
||||
version = "0.99.5"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# 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
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.92.2"
|
||||
__version__ = "0.99.5"
|
||||
|
||||
# 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]}
|
||||
|
||||
|
||||
@@ -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.92.2"' in rune_version,
|
||||
'__version__ = "0.99.5"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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})
|
||||
@@ -171,3 +171,98 @@ def _accept(command_id: str, construction_id: str, correlation_id: str,
|
||||
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime()),
|
||||
"idempotent_replay": False}
|
||||
|
||||
|
||||
def dispatch(command_type: str, *, construction_id: str,
|
||||
correlation_id: str = "", command_id: str = "",
|
||||
payload: dict | None = None,
|
||||
store_path: str = "store/scaffold") -> dict:
|
||||
"""The SERVING layer (T5.15 slice 2): validate the command against the
|
||||
canonical store, execute the accepted intent through the Construction
|
||||
Interface, and return the interface's envelope with the command
|
||||
provenance attached.
|
||||
|
||||
Single entry point for `rune scaffold <command>` (the Go proxy) and
|
||||
any future transport. Idempotency: a repeated command_id replays the
|
||||
recorded outcome without re-executing.
|
||||
|
||||
Refusal still returns success=False envelope? NO — a refused command
|
||||
is a VALID protocol outcome: the envelope carries accepted=False and
|
||||
success=True (the dispatch itself worked; the lifecycle refused the
|
||||
intent). Execution failures carry success=False.
|
||||
"""
|
||||
iface_path = None
|
||||
if store_path and store_path != "store/scaffold":
|
||||
iface_path = store_path
|
||||
try:
|
||||
from rune import construction_interface as ci
|
||||
|
||||
# store_path carries the serving layer's "path|project_id" contract
|
||||
# (a bare path = last segment is the project id, matching how the
|
||||
# Go proxy and the interface's own default name it).
|
||||
if iface_path:
|
||||
path_part, _, proj_part = iface_path.partition("|")
|
||||
proj = proj_part or path_part.rstrip("/").split("/")[-1]
|
||||
iface = ci.ConstructionInterface(builder="substrate",
|
||||
project_store=path_part,
|
||||
project_id=proj)
|
||||
else:
|
||||
iface = ci.ConstructionInterface(builder="substrate")
|
||||
except Exception as exc: # noqa: BLE001 — serving failure is an envelope, not a crash
|
||||
return {"accepted": False, "executed": False,
|
||||
"success": False, "error": f"serving unavailable: {exc}"[:200],
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id}
|
||||
|
||||
intent = command(command_type, construction_id=construction_id,
|
||||
correlation_id=correlation_id, command_id=command_id,
|
||||
payload=payload, scaffold=iface.scaffold)
|
||||
if intent.get("idempotent_replay"):
|
||||
# Repeated delivery: return the recorded outcome WITHOUT executing
|
||||
# again (the ledger's whole point). The first execution's envelope
|
||||
# is not re-derivable here, so the replay carries the intent and
|
||||
# the flag — the caller knows the effect already happened.
|
||||
return {"accepted": True, "executed": False,
|
||||
"success": True, "idempotent_replay": True,
|
||||
"op": intent.get("op"),
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id,
|
||||
"correlation_id": intent.get("correlation_id", ""),
|
||||
"command_id": intent.get("command_id", "")}
|
||||
if not intent.get("accepted"):
|
||||
return {"accepted": False, "executed": False, "success": True,
|
||||
"reason": intent.get("reason"),
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id,
|
||||
"correlation_id": intent.get("correlation_id", ""),
|
||||
"command_id": intent.get("command_id", ""),
|
||||
"idempotent_replay": intent.get("idempotent_replay", False)}
|
||||
|
||||
op = intent["op"]
|
||||
args = intent["args"]
|
||||
ops = {
|
||||
"resume": lambda: iface.resume(args["contract_id"]),
|
||||
"transition": lambda: iface.transition(
|
||||
args["contract_id"], args["phase"],
|
||||
evidence_ref=f"command:{intent['command_id']}"
|
||||
+ (f" approved_by:{args['approved_by']}" if args.get("approved_by") else "")),
|
||||
"amend_scope": lambda: iface.amend_scope(
|
||||
args["contract_id"], args["add_paths"],
|
||||
reason=args.get("reason", "substrate context"),
|
||||
authority=args.get("authority", "substrate")),
|
||||
}
|
||||
fn = ops.get(op)
|
||||
if fn is None:
|
||||
return {"accepted": True, "executed": False, "success": False,
|
||||
"error": f"no executor for op {op!r}",
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id}
|
||||
env = fn()
|
||||
return {"accepted": True, "executed": True,
|
||||
"success": bool(env.get("success")),
|
||||
"envelope": env,
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id,
|
||||
"correlation_id": intent.get("correlation_id", ""),
|
||||
"command_id": intent.get("command_id", ""),
|
||||
"idempotent_replay": False}
|
||||
@@ -36,6 +36,7 @@ from typing import Any
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from rune import structural_store as ss
|
||||
@@ -88,6 +89,39 @@ def _envelope(
|
||||
return env
|
||||
|
||||
|
||||
def _to_stage_check(spec: dict):
|
||||
"""One guard spec → a stage_guards.StageCheck (T5.15 wire)."""
|
||||
from rune import stage_guards as sg
|
||||
|
||||
return sg.StageCheck(
|
||||
name=str(spec.get("name", "guard")),
|
||||
kind=str(spec.get("kind", "assert")),
|
||||
argv=spec.get("argv"),
|
||||
output_pattern=str(spec.get("output_pattern", "")),
|
||||
path=str(spec.get("path", "")),
|
||||
check=spec.get("check"),
|
||||
description=str(spec.get("description", "")),
|
||||
)
|
||||
|
||||
|
||||
def _guard_specs_from_obligations(c: dict) -> list[dict]:
|
||||
"""The contract's guard declarations: verification_obligations rows
|
||||
shaped `guard:<json-spec>` (the decision path records them; anything
|
||||
else stays a free-text obligation for the projection). JSONDecodeError
|
||||
→ skipped with no invented facts."""
|
||||
specs: list[dict] = []
|
||||
for item in (c.get("identities") or {}).get("verification_obligations", []) or []:
|
||||
if not isinstance(item, str) or not item.startswith("guard:"):
|
||||
continue
|
||||
try:
|
||||
spec = json.loads(item[6:])
|
||||
if isinstance(spec, dict):
|
||||
specs.append(spec)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return specs
|
||||
|
||||
|
||||
class ConstructionInterface:
|
||||
"""The builder-facing construction lifecycle for ONE project target.
|
||||
|
||||
@@ -291,6 +325,13 @@ class ConstructionInterface:
|
||||
"transition": {
|
||||
"phase": {"type": "str", "required": True,
|
||||
"allowed": list(sc.PHASES)},
|
||||
"evidence_ref": {"type": "str", "required": False},
|
||||
"guard_checks": {
|
||||
"type": "list[dict] | None", "required": False,
|
||||
"notes": ("stage-guard specs run on the "
|
||||
"completion edges (verifying/closed); "
|
||||
"a failure BLOCKS with "
|
||||
"VERIFICATION_FAILED — T5.15 wire")},
|
||||
},
|
||||
"close": {
|
||||
"observed_evidence": {
|
||||
@@ -381,7 +422,8 @@ class ConstructionInterface:
|
||||
errors.append({"error": Errors.INSUFFICIENT_EVIDENCE, "detail": trace["rationale"][:200]})
|
||||
contract = None
|
||||
if generate and trace["resolution"] not in ("INSUFFICIENT_EVIDENCE",):
|
||||
cid = contract_id or f"BUILDER-{ss.now()}-{self.builder}"
|
||||
cid = (contract_id
|
||||
or f"BUILDER-{ss.now()}-{uuid.uuid4().hex[:6]}-{self.builder}")
|
||||
cd.generate_contract(
|
||||
self.scaffold,
|
||||
doc,
|
||||
@@ -607,8 +649,57 @@ class ConstructionInterface:
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "") -> dict:
|
||||
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "",
|
||||
guard_checks: list[dict] | None = None) -> dict:
|
||||
"""Move a contract phase. Completion-wave slice (T5.15, the
|
||||
stage_guards wire): entering `verifying` and `closed` runs the
|
||||
§15F/§15G guards — readiness/exit StageChecks and test-truth
|
||||
verification — when the contract declares them
|
||||
(identities.verification_obligations carries `guard:<spec>` rows,
|
||||
or guard_checks passes inline specs).
|
||||
|
||||
guard_checks spec: {"name", "kind": "command"|"path_exists"|"assert",
|
||||
"argv"/"path"/"output_pattern", "check": callable (in-process only),
|
||||
"description"}. A guard failure is a VERIFICATION_FAILED envelope:
|
||||
the phase does NOT move.
|
||||
|
||||
The legality map stays the scaffold's authority; guards ADD evidence
|
||||
obligations on the completion-facing edges (building→verifying,
|
||||
verifying→closed) — a tiny bounded change can pass guards=[] freely.
|
||||
"""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
if phase in ("verifying", "closed"):
|
||||
from rune import stage_guards as sg
|
||||
|
||||
specs = guard_checks
|
||||
if specs is None:
|
||||
specs = _guard_specs_from_obligations(c)
|
||||
if specs:
|
||||
stage = sg.StageContract(
|
||||
stage_id=f"{contract_id}:{phase}",
|
||||
contract_id=contract_id,
|
||||
ready_when=[_to_stage_check(s) for s in specs],
|
||||
done_when=[_to_stage_check(s) for s in specs],
|
||||
)
|
||||
canary = sg.readiness_canary(stage)
|
||||
if canary["verdict"] == "BLOCKED":
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
state={"phase": c.get("phase"), "guard": canary},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED,
|
||||
"detail": "stage guards blocked the transition",
|
||||
"failures": canary["failures"]}],
|
||||
)
|
||||
try:
|
||||
self.scaffold.transition_contract(
|
||||
doc, contract_id, phase, evidence_ref=evidence_ref or f"builder:{self.builder}"
|
||||
@@ -679,21 +770,87 @@ class ConstructionInterface:
|
||||
material_closure = self._auto_material_change(
|
||||
scope_files or [],
|
||||
note=f"contract {contract_id} closed (material change)")
|
||||
self._record_impact_delta(contract_id, scope_files or [])
|
||||
continuation = self._programme_continuation(contract_id)
|
||||
state = {
|
||||
"phase": "closed",
|
||||
"material_change_closure": material_closure,
|
||||
"contract_quality": {
|
||||
k: q[k]
|
||||
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
|
||||
if k in q
|
||||
},
|
||||
}
|
||||
if continuation is not None:
|
||||
state["programme_continuation"] = continuation
|
||||
return _envelope(
|
||||
"close",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"phase": "closed",
|
||||
"material_change_closure": material_closure,
|
||||
"contract_quality": {
|
||||
k: q[k]
|
||||
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
|
||||
if k in q
|
||||
},
|
||||
},
|
||||
state=state,
|
||||
)
|
||||
|
||||
def _record_impact_delta(self, contract_id: str, scope_files: list) -> None:
|
||||
"""T5.15 slice 4 (the iteration wire): a material closure records a
|
||||
predicted-vs-observed impact delta (rule 19H — deltas compound into
|
||||
better future prediction). Governance-only closes (no scope files)
|
||||
record nothing. Failure degrades: the ledger never fails a close."""
|
||||
if not scope_files:
|
||||
return
|
||||
try:
|
||||
from rune import iteration as it
|
||||
|
||||
it.record_impact_delta(
|
||||
Path(self.scaffold.current).parent,
|
||||
{
|
||||
"contract_id": contract_id,
|
||||
"scope_files": sorted(scope_files),
|
||||
"predicted_frontier": "from-contract-scope",
|
||||
"recorded_at": ss.now(),
|
||||
},
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — delta failure never fails a closure
|
||||
self._record_session(
|
||||
self.scaffold.load(), "impact_delta_degraded",
|
||||
f"{contract_id}: {str(exc)[:120]}")
|
||||
|
||||
def _programme_continuation(self, contract_id: str) -> dict | None:
|
||||
"""T5.15 slice 3 (the programme_continuation wire): after a legal
|
||||
close, any persisted Programme stage bound to this contract is
|
||||
marked COMPLETE and the continuation canary's verdict rides the
|
||||
close envelope — 'task done' ≠ 'programme done', and the next
|
||||
eligible stage is SURFACED, never guessed. No programme bound →
|
||||
None (no continuation block in the envelope). Failures degrade:
|
||||
a broken programme ledger never fails a real closure."""
|
||||
try:
|
||||
from pathlib import Path as _Path
|
||||
|
||||
from rune import programme_continuation as pc
|
||||
|
||||
base = _Path(self.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
|
||||
if not base.is_dir():
|
||||
return None
|
||||
for prog_file in sorted(base.glob("*.json")):
|
||||
prog = pc.Programme.load(base, prog_file.stem)
|
||||
if prog is None:
|
||||
continue
|
||||
stage = next((s for s in prog.stages
|
||||
if s.stage_id == contract_id
|
||||
and s.state != pc.COMPLETE), None)
|
||||
if stage is None:
|
||||
continue
|
||||
stage.state = pc.COMPLETE
|
||||
stage.result["closed_at_contract"] = contract_id
|
||||
prog.save(base)
|
||||
return {
|
||||
"programme_id": prog.programme_id,
|
||||
"stage_marked_complete": contract_id,
|
||||
"canary": prog.continuation_canary(),
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001 — ledger failure never fails a closure
|
||||
return {"error": f"programme continuation degraded: {exc}"[:200]}
|
||||
return None
|
||||
|
||||
def _auto_material_change(self, scope_files: list[str], *,
|
||||
note: str) -> dict:
|
||||
"""T21.4 §21.4H (rule 19Q.2): governed construction closes → the
|
||||
@@ -1019,7 +1176,9 @@ def main(argv=None) -> int:
|
||||
if cmd == "verify":
|
||||
return _out(iface.verify(args[0], changed_files=(json.loads(args[1]) if len(args) > 1 else None)))
|
||||
if cmd == "transition":
|
||||
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else ""))
|
||||
guard_checks = json.loads(args[3]) if len(args) > 3 else None
|
||||
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else "",
|
||||
guard_checks=guard_checks))
|
||||
if cmd == "close":
|
||||
return _out(
|
||||
iface.close(
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2525,6 +2525,53 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
|
||||
_operator = None
|
||||
|
||||
|
||||
def _forge_client_for_projection():
|
||||
"""The client seam for the forge projection (test-overridable)."""
|
||||
try:
|
||||
from .forge_client import create_forge_client
|
||||
except ImportError: # TUI top-level deploy (script mode)
|
||||
from forge_client import create_forge_client
|
||||
return create_forge_client()
|
||||
|
||||
|
||||
def _project_failure_to_forge(*, behaviour_id: str, observation: dict,
|
||||
repo: str) -> dict:
|
||||
"""T5.15 slice 5 (the forge projection wire): a recorded failure
|
||||
projects to the human-visible forge (rule 19P — the projection is a
|
||||
VIEW; canonical truth stays in the ledger). Uses the ALREADY-WIRED
|
||||
forge client (create_forge_client → NullAdapter when unconfigured, so
|
||||
the projection is dormant by construction). A thin shim adapts the
|
||||
client's create_issue to the testing-mode projection's
|
||||
create_or_patch_release contract. Forge failure NEVER breaks the
|
||||
failure path — it degrades to a recorded marker."""
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
try:
|
||||
client = _forge_client_for_projection()
|
||||
|
||||
class _Shim:
|
||||
"""Adapts create_issue → the projection's release contract."""
|
||||
|
||||
@staticmethod
|
||||
def create_or_patch_release(repo_: str, tag: str,
|
||||
title: str, body: str) -> dict:
|
||||
return client.create_issue(title, body)
|
||||
|
||||
try:
|
||||
from .testing_mode import project_failure_to_forge
|
||||
except ImportError:
|
||||
from testing_mode import project_failure_to_forge
|
||||
|
||||
return project_failure_to_forge(
|
||||
_Shim(), repo, behaviour_id=behaviour_id,
|
||||
observation=observation)
|
||||
except Exception as exc: # noqa: BLE001 — projection failure never rewrites truth
|
||||
log.debug("forge projection degraded: %s", exc)
|
||||
return {"projected": False,
|
||||
"error": str(exc)[:200]}
|
||||
|
||||
|
||||
def _wire_auto_filer() -> None:
|
||||
"""Composition-root wiring for the failure auto-filer (#122).
|
||||
|
||||
|
||||
+72
-75
@@ -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.92.2
|
||||
Version: v0.99.5
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -110,6 +110,56 @@ try:
|
||||
except ImportError:
|
||||
BACKEND_AVAILABLE = False
|
||||
|
||||
|
||||
def collect_post_response_signals(response: str, messages_count: int) -> list[str]:
|
||||
"""Gather the operational signals that used to each print a dim
|
||||
system message after every task/build response (the dim-spam source,
|
||||
T5.13): budget pressure, degraded claims, growth delta, the periodic
|
||||
self-improvement cycle.
|
||||
|
||||
PURE and headless — no channel, no app. The caller renders ONE
|
||||
collapsed line (or none when nothing fired). Queued-message execution
|
||||
and on-complete cron firing stay in the worker: they move real
|
||||
messages, not chatter, and keep their own echoes.
|
||||
|
||||
Returns short human-readable fragments; empty list = silence.
|
||||
"""
|
||||
signals: list[str] = []
|
||||
if not BACKEND_AVAILABLE:
|
||||
return signals
|
||||
try:
|
||||
budget_data = provider_budget.get_status()
|
||||
ratios = []
|
||||
for provider in budget_data.get("providers", {}).values():
|
||||
if provider.get("monthly_limit", 0) > 0:
|
||||
ratios.append(provider.get("monthly_used", 0) / provider["monthly_limit"])
|
||||
if provider.get("token_limit", 0) > 0:
|
||||
ratios.append(provider.get("tokens_used", 0) / provider["token_limit"])
|
||||
usage_percent = max(ratios, default=0) * 100
|
||||
if usage_percent > 80:
|
||||
signals.append(f"⚠ Budget: {usage_percent:.0f}% used")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
claims_result = claim_audit.scan_text(response[:500])
|
||||
if claims_result and claims_result.get("degraded"):
|
||||
signals.append(f"⚠ Claims degraded: {len(claims_result['degraded'])}")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
growth = growth_delta.check()
|
||||
if growth and growth.get("delta"):
|
||||
signals.append(f"📈 Growth: {growth['delta']}")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if messages_count % 10 == 0:
|
||||
self_improve.run()
|
||||
signals.append("🔄 Self-improvement cycle triggered")
|
||||
except Exception:
|
||||
pass
|
||||
return signals
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.widgets import Button, Header, Input, ProgressBar, RichLog, Static
|
||||
@@ -1533,18 +1583,21 @@ class RuneApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
with Horizontal(id="channels"):
|
||||
yield ChannelView(
|
||||
"task",
|
||||
"[b]TASK[/b] — focused work",
|
||||
classes="channel-view",
|
||||
id="task-channel",
|
||||
)
|
||||
# T5.14 (Nick 2026-10-05): chat LEFT, task RIGHT — feels more
|
||||
# natural (conversation first, focused work second). WorkPane
|
||||
# stays third.
|
||||
yield ChannelView(
|
||||
"chat",
|
||||
"[b]CHAT[/b] — free conversation",
|
||||
classes="channel-view",
|
||||
id="chat-channel",
|
||||
)
|
||||
yield ChannelView(
|
||||
"task",
|
||||
"[b]TASK[/b] — focused work",
|
||||
classes="channel-view",
|
||||
id="task-channel",
|
||||
)
|
||||
# P1-07 (#30): third collapsible column — the operational index.
|
||||
yield WorkPane(id="work-pane")
|
||||
with Horizontal(id="footer-bar"):
|
||||
@@ -1895,7 +1948,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.92.2 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.99.5 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -3737,69 +3790,13 @@ class RuneApp(App):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Auto-warn budget when >80% used
|
||||
try:
|
||||
if BACKEND_AVAILABLE:
|
||||
budget_data = provider_budget.get_status()
|
||||
ratios = []
|
||||
for provider in budget_data.get("providers", {}).values():
|
||||
if provider.get("monthly_limit", 0) > 0:
|
||||
ratios.append(provider.get("monthly_used", 0) / provider["monthly_limit"])
|
||||
if provider.get("token_limit", 0) > 0:
|
||||
ratios.append(provider.get("tokens_used", 0) / provider["token_limit"])
|
||||
usage_percent = max(ratios, default=0) * 100
|
||||
if usage_percent > 80:
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[dim]⚠ Budget: {usage_percent:.0f}% used[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Auto-check claims after response
|
||||
try:
|
||||
if BACKEND_AVAILABLE:
|
||||
claims_result = claim_audit.scan_text(response[:500])
|
||||
if claims_result and claims_result.get("degraded"):
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[dim]⚠ Claims degraded: {len(claims_result['degraded'])}[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# NOTE (Runtime Reachability audit 2026-09-10): the
|
||||
# old hook_engine.fire("post_message", ...) call here
|
||||
# was dead — hook_engine defines no fire(); the
|
||||
# AttributeError was silently swallowed, so "hooks"
|
||||
# never ran. Removed rather than left as a false-
|
||||
# liveness surface. Real hook dispatch is unwired
|
||||
# (microkernel.fire has zero callers) — see the
|
||||
# reachability audit, P3.
|
||||
|
||||
# Auto-check growth delta
|
||||
try:
|
||||
if BACKEND_AVAILABLE:
|
||||
growth = growth_delta.check()
|
||||
if growth and growth.get("delta"):
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[dim]📈 Growth: {growth['delta']}[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Auto-improve every 10 messages
|
||||
try:
|
||||
if self.status_bar.messages_count % 10 == 0:
|
||||
if BACKEND_AVAILABLE:
|
||||
self_improve.run()
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[dim]🔄 Self-improvement cycle triggered[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# T5.13 dim-spam consolidation: previously five
|
||||
# separate dim system messages printed after every
|
||||
# response. ONE collapsed line now — empty means
|
||||
# silence, not a dash.
|
||||
signals = collect_post_response_signals(response[:500], self.status_bar.messages_count)
|
||||
if signals:
|
||||
ch.add_message("system", "[dim]" + " · ".join(signals) + "[/dim]")
|
||||
elif result.get("error") == "RESEARCH_REQUIRED":
|
||||
# Nonterminal routing disposition, not failure (2026-09-10):
|
||||
# the task hit the deep-research floor and was dispatched.
|
||||
@@ -3980,13 +3977,13 @@ class RuneApp(App):
|
||||
pass
|
||||
|
||||
def action_resize_left(self) -> None:
|
||||
"""Shrink task channel, grow chat channel."""
|
||||
self.task_width = max(20, self.task_width - 5)
|
||||
"""Shrink the LEFT channel (chat, post-swap), grow the RIGHT (task)."""
|
||||
self.task_width = min(80, self.task_width + 5)
|
||||
self._apply_resize()
|
||||
|
||||
def action_resize_right(self) -> None:
|
||||
"""Grow task channel, shrink chat channel."""
|
||||
self.task_width = min(80, self.task_width + 5)
|
||||
"""Grow the LEFT channel (chat), shrink the RIGHT (task)."""
|
||||
self.task_width = max(20, self.task_width - 5)
|
||||
self._apply_resize()
|
||||
|
||||
def _apply_resize(self):
|
||||
|
||||
+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]
|
||||
|
||||
|
||||
+18
-3
@@ -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
|
||||
|
||||
@@ -337,11 +343,14 @@ def _rust_core():
|
||||
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
|
||||
@@ -517,6 +526,7 @@ def _py_build_from_inputs(inputs: dict) -> dict:
|
||||
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)
|
||||
@@ -595,9 +605,14 @@ def build(manifest_path: Path | None = None) -> dict:
|
||||
# paths, including an explicit unseeded-ingest parity section.
|
||||
seeded = bool(inputs["existing"]) and any(
|
||||
r["type"] == "belongs_to" for r in inputs["existing"]["relationships"])
|
||||
if core is not None and seeded:
|
||||
return core.project(inputs)
|
||||
return _py_build_from_inputs(inputs)
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
"""spine_language.py — the #2465 doc-level transforms (carved from spine.py
|
||||
to honor the giant-file henxel: keep it under 1200 lines).
|
||||
|
||||
Three transforms, applied to BOTH cores' output at build time:
|
||||
1. language placement — every entity gets a language verdict (kind
|
||||
defaults; subsystems derive from member-majority over the measured
|
||||
classification map);
|
||||
2. discovery-ID normalization — migrate legacy unqualified contract
|
||||
discoveries to construction-surface kind without losing evidence;
|
||||
3. contract-discovery write-back — closed contracts teach the spine
|
||||
what construction actually built (observed → PRESENT, candidate →
|
||||
DECLARED; the CANDIDATE law holds).
|
||||
|
||||
Same dependency laws as spine.py: structural_store primitives only, no
|
||||
I/O beyond the classification/scaffold store reads, deterministic.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from rune.structural_store import (
|
||||
add_entity, add_relationship, entity, new_doc, relationships, _sorted, now,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC_DIR = ROOT / "src" / "rune"
|
||||
|
||||
def _classification_sides() -> dict:
|
||||
"""The measured dichotomy map (W0): module stem → RUST|GO|TUI-LAST|...
|
||||
Loaded once per _place_languages call, best-effort — a missing or
|
||||
unreadable classification.json degrades to {}/unassessed verdicts,
|
||||
never to invented facts."""
|
||||
try:
|
||||
data = json.loads((ROOT / "store" / "rebase" / "classification.json").read_text())
|
||||
return {name: rec.get("side", "") for name, rec in data.items() if isinstance(rec, dict)}
|
||||
except (OSError, json.JSONDecodeError, AttributeError):
|
||||
return {}
|
||||
|
||||
|
||||
def _place_languages(doc: dict) -> int:
|
||||
"""Stamp a language verdict per entity (no-op for entities that carry
|
||||
one). Deterministic: substring classification over the entity id + the
|
||||
kind; the exact same rule construction_decision.language_placement
|
||||
implements (kept inline here to avoid a per-entity FFI crossing for
|
||||
the spine's own 218 entities; the FFI path is used in contracts)."""
|
||||
GO_HINTS = ("server", "transport", "dispatch", "entry", "bridge",
|
||||
"hook_engine", "provider", "watch", "notif", "voice",
|
||||
"gitea", "event_", "message_", "tool_exec", "work_manager")
|
||||
RUST_HINTS = ("memory", "drift", "warm", "capab", "token", "spine",
|
||||
"scaffold", "construct", "structural", "curator",
|
||||
"consolidat", "elastic", "cognit", "abstract", "contract",
|
||||
"ledger", "audit", "claim", "evidence", "behavio",
|
||||
"verif", "pins", "witness", "anchor", "growth", "goal",
|
||||
"salience", "store", "tui_", "rune_tui")
|
||||
placed = 0
|
||||
CLASSIFICATION_SIDES = _classification_sides()
|
||||
for e in doc.get("entities", []):
|
||||
eid = e.get("id", "")
|
||||
kind = e.get("kind", "")
|
||||
# AUTHORITY ORDER (Nick's dichotomy, 2026-10-06 correction): the
|
||||
# MEASURED classification map (import-evidence per module) decides
|
||||
# FIRST. The name-hint lists are only a fallback for entities with
|
||||
# no measurement — never allowed to override the evidence. A module
|
||||
# with neither measurement nor hint is "unclassified", not a silent
|
||||
# rust default.
|
||||
measured = str(CLASSIFICATION_SIDES.get(
|
||||
eid.split(":", 1)[1] if ":" in eid else eid, "") or "").upper()
|
||||
if eid.startswith("contract:"):
|
||||
lang = "n/a"
|
||||
elif kind == "rust-crate":
|
||||
lang = "rust"
|
||||
elif kind == "go-surface":
|
||||
lang = "go"
|
||||
elif kind == "workflow" or kind == "config":
|
||||
lang = "n/a"
|
||||
elif kind == "deployment_surface":
|
||||
lang = "go"
|
||||
elif measured == "RUST":
|
||||
lang = "rust"
|
||||
elif measured == "GO":
|
||||
lang = "go"
|
||||
elif measured == "TUI-LAST":
|
||||
lang = "python-tui (Wave 6)"
|
||||
elif measured == "TEST-INFRA" or measured == "RETIRES":
|
||||
lang = "test-infra" if measured == "TEST-INFRA" else "retires"
|
||||
elif "tui" in eid:
|
||||
lang = "python-tui (Wave 6)"
|
||||
elif any(h in eid for h in GO_HINTS):
|
||||
lang = "go (name hint)"
|
||||
elif any(h in eid for h in RUST_HINTS):
|
||||
lang = "rust (name hint)"
|
||||
elif kind == "module":
|
||||
lang = "unclassified"
|
||||
elif kind == "subsystem":
|
||||
# A subsystem is a CONTAINER — its language is its members'
|
||||
# majority side per the measured dichotomy. The evidence map is
|
||||
# classification.json (measured, 174 modules; honest note: newer
|
||||
# files may be unclassified, so the count of classified members
|
||||
# rides the verdict). Members arrive as belongs_to relationships
|
||||
# whose from-ids carry module:<name> — map through classification.
|
||||
members = [
|
||||
r.get("from", "").split(":", 1)[1]
|
||||
for r in doc.get("relationships", [])
|
||||
if r.get("type") == "belongs_to" and r.get("to") == eid
|
||||
and r.get("from", "").startswith("module:")
|
||||
]
|
||||
sides = [
|
||||
str(CLASSIFICATION_SIDES.get(m) or "").upper()
|
||||
for m in members
|
||||
]
|
||||
rust = sides.count("RUST")
|
||||
go = sides.count("GO")
|
||||
if rust and go:
|
||||
lang = f"mixed (rust {rust} / go {go} members)"
|
||||
elif rust:
|
||||
lang = "rust"
|
||||
elif go:
|
||||
lang = "go"
|
||||
else:
|
||||
lang = "unassessed"
|
||||
else:
|
||||
lang = "unassessed"
|
||||
e.setdefault("identities", {})["language"] = lang
|
||||
if lang != "unassessed":
|
||||
placed += 1
|
||||
return placed
|
||||
|
||||
|
||||
def _normalize_discovery_ids(doc: dict) -> int:
|
||||
"""Migrate the old unqualified contract references, preserving evidence.
|
||||
|
||||
A file path (or 'suite') is not a module ID. Only migrate the malformed
|
||||
module records produced by the earlier discovery projector; leave all
|
||||
other open-world entity classes alone.
|
||||
"""
|
||||
renamed = {}
|
||||
observed = {
|
||||
eid for _, entities in _contract_discoveries()
|
||||
for eid, _, provenance in entities if provenance == "observed"
|
||||
}
|
||||
for e in doc.get("entities", []):
|
||||
if (e.get("kind") == "module" and ":" not in e.get("id", "")
|
||||
and any(ref.startswith("contract-discovery:")
|
||||
for ref in e.get("evidence_refs", []))):
|
||||
old = e["id"]
|
||||
e["id"] = f"construction-surface:{old}"
|
||||
e["kind"] = "construction-surface"
|
||||
e.setdefault("identities", {})["source_ref"] = old
|
||||
renamed[old] = e["id"]
|
||||
if e.get("kind") == "construction-surface" and e.get("id", "").startswith("construction-surface:"):
|
||||
# Earlier code promoted every *candidate* to PRESENT. Undo only
|
||||
# that unsupported claim; an actually observed surface retains it.
|
||||
if e["id"].split(":", 1)[1] not in observed:
|
||||
e["maturity"] = "DECLARED"
|
||||
e["tags"] = [tag for tag in e.get("tags", []) if tag != "code"]
|
||||
for rel in doc.get("relationships", []):
|
||||
for endpoint in ("from", "to"):
|
||||
if rel.get(endpoint) in renamed:
|
||||
rel[endpoint] = renamed[rel[endpoint]]
|
||||
return len(renamed)
|
||||
|
||||
|
||||
def _apply_contract_discoveries(doc: dict) -> int:
|
||||
"""Contract-discovery write-back (issue #2465 addition 2): entities
|
||||
DISCOVERED by closed contracts become modeled spine entities — the
|
||||
spine learns what construction actually built. Doc-level transform,
|
||||
applied to BOTH cores' output. The CANDIDATE law holds: only CLOSED
|
||||
contracts write back."""
|
||||
placed = 0
|
||||
for cid, entities in _contract_discoveries():
|
||||
for eid, kind, provenance in entities:
|
||||
before = len(doc.get("entities", []))
|
||||
if kind == "construction-surface":
|
||||
eid = f"construction-surface:{eid}"
|
||||
add_entity(doc, id=eid, kind=kind,
|
||||
tags=["contract-discovery"],
|
||||
identities={"source_ref": eid.split(":", 1)[1]} if kind == "construction-surface" else None,
|
||||
evidence_refs=[f"contract-discovery:{cid}"],
|
||||
maturity="PRESENT" if provenance == "observed" else "DECLARED")
|
||||
if len(doc.get("entities", [])) > before:
|
||||
placed += 1
|
||||
else:
|
||||
placed += 0 # high-water merge — no new entity, still evidence
|
||||
return placed
|
||||
|
||||
|
||||
def _contract_discoveries():
|
||||
"""Closed contracts' DISCOVERED entities (issue #2465 addition 2):
|
||||
expected_new_observed + candidate corrections, gathered from the
|
||||
scaffold store. Returns [(contract_id, [(entity_id, kind), ...])]."""
|
||||
try:
|
||||
doc = json.loads((ROOT / "store" / "scaffold" / "current.json").read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return []
|
||||
out = []
|
||||
for e in doc.get("entities", []):
|
||||
cid = e.get("id", "")
|
||||
if not cid.startswith("contract:") or e.get("phase") != "closed":
|
||||
continue
|
||||
q = e.get("ext", {}).get("contract_quality", {})
|
||||
found = []
|
||||
# expected-new surfaces observed at close (REAL delivered entities)
|
||||
for eid in q.get("expected_new_observed", []):
|
||||
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
|
||||
if kind in ("rust-crate", "go-surface", "config", "workflow", "module"):
|
||||
found.append((eid, kind, "observed"))
|
||||
elif kind == "construction-surface":
|
||||
found.append((eid, kind, "observed"))
|
||||
# candidate corrections: the closure flagged them for modeling
|
||||
for cand in q.get("candidate_corrections", []):
|
||||
eid = cand.get("entity", "")
|
||||
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
|
||||
if eid and kind in ("rust-crate", "go-surface", "config", "workflow", "module", "construction-surface"):
|
||||
found.append((eid, kind, "candidate"))
|
||||
if found:
|
||||
out.append((cid[9:], found))
|
||||
return out
|
||||
@@ -20,6 +20,33 @@ HOT_THRESHOLD = 7 # accessed within 7 days = hot
|
||||
WARM_THRESHOLD = 30 # accessed within 30 days = warm
|
||||
# > 30 days = cold
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust cognition_core when available (strangler: Python fallback is
|
||||
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 _py_core_now() -> str:
|
||||
"""The injected clock (iso Z) — module-level for the kernel boundary."""
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Tier storage characteristics
|
||||
TIER_CONFIG = {
|
||||
"hot": {
|
||||
@@ -56,7 +83,24 @@ class TierManager:
|
||||
"""Determine the tier for an entry based on its access patterns.
|
||||
|
||||
Returns 'hot', 'warm', or 'cold'.
|
||||
Delegates to the Rust kernel when built; the Python body below is
|
||||
the parity oracle (W3.4: identical thresholds injected, naive
|
||||
timestamps refuse per the PARITY TRAP law).
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
raw = _cl.tier_get(entry, _py_core_now(),
|
||||
float(HOT_THRESHOLD), float(WARM_THRESHOLD))
|
||||
return str(raw)
|
||||
except Exception: # noqa: BLE001, S110 — kernel failure → oracle, never a broken call
|
||||
pass
|
||||
return self._py_get_tier(entry)
|
||||
|
||||
def _py_get_tier(self, entry: dict) -> str:
|
||||
"""Python parity oracle (W3.4)."""
|
||||
wash_count = entry.get("wash_count", 0) or 0
|
||||
last_accessed = entry.get("last_accessed")
|
||||
rock_status = entry.get("rock_status", "washable")
|
||||
@@ -88,7 +132,26 @@ class TierManager:
|
||||
return "warm"
|
||||
|
||||
def promote(self, entry: dict) -> str:
|
||||
"""Promote an entry to a higher tier. Returns new tier."""
|
||||
"""Promote an entry to a higher tier. Returns new tier.
|
||||
Delegates to the Rust kernel when built (Python oracle below)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
# The kernel's promote/demote take the FULL ENTRY (plus
|
||||
# thresholds + clock) — not a tier string. They recompute
|
||||
# the current tier internally, so a tier-string call was
|
||||
# silently landing on defaults (the live probe caught it).
|
||||
raw = _cl.tier_promote(entry, _py_core_now(),
|
||||
float(HOT_THRESHOLD), float(WARM_THRESHOLD))
|
||||
return str(raw)
|
||||
except Exception: # noqa: BLE001, S110 — kernel failure → oracle fallback, intentional silence
|
||||
pass
|
||||
return self._py_promote(entry)
|
||||
|
||||
def _py_promote(self, entry: dict) -> str:
|
||||
"""Python parity oracle."""
|
||||
current = self.get_tier(entry)
|
||||
if current == "cold":
|
||||
return "warm"
|
||||
@@ -97,7 +160,24 @@ class TierManager:
|
||||
return "hot" # already hot
|
||||
|
||||
def demote(self, entry: dict) -> str:
|
||||
"""Demote an entry to a lower tier. Returns new tier."""
|
||||
"""Demote an entry to a lower tier. Returns new tier.
|
||||
Delegates to the Rust kernel when built (Python oracle below)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
# Same full-entry contract as promote; the kernel handles
|
||||
# rock_status internally (rock never demotes).
|
||||
raw = _cl.tier_demote(entry, _py_core_now(),
|
||||
float(HOT_THRESHOLD), float(WARM_THRESHOLD))
|
||||
return str(raw)
|
||||
except Exception: # noqa: BLE001, S110 — kernel failure → oracle fallback, intentional silence
|
||||
pass
|
||||
return self._py_demote(entry)
|
||||
|
||||
def _py_demote(self, entry: dict) -> str:
|
||||
"""Python parity oracle."""
|
||||
current = self.get_tier(entry)
|
||||
rock_status = entry.get("rock_status", "washable")
|
||||
|
||||
|
||||
+127
-112
@@ -84,76 +84,129 @@ def is_correction(text):
|
||||
return False
|
||||
|
||||
|
||||
def extract_topic(text):
|
||||
"""Extract topic keywords from text. Deterministic."""
|
||||
stopwords = {
|
||||
"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",
|
||||
def _stopwords() -> list:
|
||||
"""Gather seam (W3.4 delegation): the topic-extraction stopword table
|
||||
as a SORTED list — sorted so JSON travel and kernel-side matching see
|
||||
a deterministic order regardless of the set's iteration order."""
|
||||
return sorted(_STOPWORDS)
|
||||
|
||||
|
||||
_STOPWORDS = {
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
def _tables() -> dict:
|
||||
"""Gather seam: ALL regex pattern tables as data in one dict — the
|
||||
exact contract the cc_witness_signals kernel consumes (parity-proven
|
||||
per-table-row; regex_lite_search is the faithful subset for THESE
|
||||
shapes). Sorted lists keep travel deterministic."""
|
||||
return {
|
||||
"stopwords": _stopwords(),
|
||||
"correction": sorted(CORRECTION_PATTERNS),
|
||||
"commitment": sorted([
|
||||
r"\bI'?ll\s+(send|do|fix|build|check|update|create|write)\b",
|
||||
r"\bI will\s+(send|do|fix|build|check|update|create|write)\b",
|
||||
r"\blet me\s+(send|do|fix|build|check|update|create|write)\b",
|
||||
r"\bremind me\b",
|
||||
r"\bI promise\b",
|
||||
r"\bI need to\b",
|
||||
]),
|
||||
"contradiction": sorted([r"\b(but|however|actually|wait|that contradicts)\b"]),
|
||||
"preference": sorted([
|
||||
r"\bI (prefer|like|love|hate|dislike)\b",
|
||||
r"\bI'?m (not )?(a fan of|into|interested in)\b",
|
||||
r"\b(don'?t|do not) (like|want|need)\b",
|
||||
r"\b(always|never) (use|do|say)\b",
|
||||
]),
|
||||
"urgency": sorted([
|
||||
r"\b(urgent|asap|immediately|now|quick|hurry)\b",
|
||||
r"\b(deadline|due|overdue|late)\b",
|
||||
r"\b(important|critical|crucial|vital)\b",
|
||||
]),
|
||||
"learning": sorted([
|
||||
r"\b(learn|understand|explain|teach|show me)\b",
|
||||
r"\b(how (does|do|did)|why (does|do|did)|what (is|are|was|were))\b",
|
||||
r"\b(I don'?t (understand|know|get))\b",
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
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 extract_topic(text):
|
||||
"""Extract topic keywords from text. Deterministic.
|
||||
Delegates to the kernel when built; Python oracle below."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
raw = _cl.witness_extract_topic(text, _stopwords())
|
||||
return list(raw) if isinstance(raw, list) else list(raw or [])
|
||||
except Exception: # noqa: BLE001, S110 — kernel failure → oracle, intentional silence
|
||||
pass
|
||||
words = re.findall(r"\w+", text.lower())
|
||||
return [w for w in words if len(w) > 3 and w not in stopwords][:5]
|
||||
return [w for w in words if len(w) > 3 and w not in _STOPWORDS][:5]
|
||||
|
||||
|
||||
def _py_extract_topic(text):
|
||||
"""Python parity oracle (W3.4)."""
|
||||
words = re.findall(r"\w+", text.lower())
|
||||
return [w for w in words if len(w) > 3 and w not in _STOPWORDS][:5]
|
||||
|
||||
|
||||
def detect_signals(message, tool_error=None, task_status=None, recent_topics=None):
|
||||
"""Tier 1: Reflex — extract signals from the turn. Pure Python, <1ms.
|
||||
|
||||
Returns dict of signals. Empty dict = nothing meaningful happened.
|
||||
Delegates to the kernel when built (tables travel as data per the
|
||||
REGEX-TABLE PORT LAW); Python oracle below is the parity authority.
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
raw = _cl.witness_signals(message, _tables(), tool_error or "",
|
||||
task_status or "", recent_topics or [])
|
||||
if isinstance(raw, dict):
|
||||
return {k: v for k, v in raw.items() if v not in (False, "")}
|
||||
# non-dict → fall through to the oracle
|
||||
except Exception: # noqa: BLE001, S110 — kernel failure → oracle, intentional silence
|
||||
pass
|
||||
return _py_detect_signals(message, tool_error, task_status, recent_topics)
|
||||
|
||||
|
||||
def _py_detect_signals(message, tool_error=None, task_status=None,
|
||||
recent_topics=None):
|
||||
"""Python parity oracle (W3.4). Local tables via the gather seam."""
|
||||
signals = {}
|
||||
|
||||
# Error signal
|
||||
@@ -179,62 +232,24 @@ def detect_signals(message, tool_error=None, task_status=None, recent_topics=Non
|
||||
signals["topic_drift"] = True
|
||||
signals["new_topics"] = list(current_topics - recent_set)[:3]
|
||||
|
||||
# Commitment detection
|
||||
commitment_patterns = [
|
||||
r"\bI'?ll\s+(send|do|fix|build|check|update|create|write)\b",
|
||||
r"\bI will\s+(send|do|fix|build|check|update|create|write)\b",
|
||||
r"\blet me\s+(send|do|fix|build|check|update|create|write)\b",
|
||||
r"\bremind me\b",
|
||||
r"\bI promise\b",
|
||||
r"\bI need to\b",
|
||||
]
|
||||
if message:
|
||||
for pattern in commitment_patterns:
|
||||
if re.search(pattern, message, re.IGNORECASE):
|
||||
signals["commitment_detected"] = True
|
||||
break
|
||||
# Commitment detection (tables live in _tables() — the gather seam;
|
||||
# the oracle reads the SAME table data the kernel receives)
|
||||
for signal_name, table_name in (
|
||||
("commitment_detected", "commitment"),
|
||||
("preference_detected", "preference"),
|
||||
("urgency_detected", "urgency"),
|
||||
("learning_opportunity", "learning"),
|
||||
):
|
||||
if message:
|
||||
for pattern in _tables().get(table_name, []):
|
||||
if re.search(pattern, message, re.IGNORECASE):
|
||||
signals[signal_name] = True
|
||||
break
|
||||
|
||||
# Contradiction signal (simple: message contains "but" or "however" after a statement)
|
||||
if message and re.search(r"\b(but|however|actually|wait|that contradicts)\b", message.lower()):
|
||||
signals["possible_contradiction"] = True
|
||||
|
||||
# Preference detection
|
||||
preference_patterns = [
|
||||
r"\bI (prefer|like|love|hate|dislike)\b",
|
||||
r"\bI'?m (not )?(a fan of|into|interested in)\b",
|
||||
r"\b(don'?t|do not) (like|want|need)\b",
|
||||
r"\b(always|never) (use|do|say)\b",
|
||||
]
|
||||
if message:
|
||||
for pattern in preference_patterns:
|
||||
if re.search(pattern, message, re.IGNORECASE):
|
||||
signals["preference_detected"] = True
|
||||
break
|
||||
|
||||
# Urgency detection
|
||||
urgency_patterns = [
|
||||
r"\b(urgent|asap|immediately|now|quick|hurry)\b",
|
||||
r"\b(deadline|due|overdue|late)\b",
|
||||
r"\b(important|critical|crucial|vital)\b",
|
||||
]
|
||||
if message:
|
||||
for pattern in urgency_patterns:
|
||||
if re.search(pattern, message, re.IGNORECASE):
|
||||
signals["urgency_detected"] = True
|
||||
break
|
||||
|
||||
# Learning opportunity detection
|
||||
learning_patterns = [
|
||||
r"\b(learn|understand|explain|teach|show me)\b",
|
||||
r"\b(how (does|do|did)|why (does|do|did)|what (is|are|was|were))\b",
|
||||
r"\b(I don'?t (understand|know|get))\b",
|
||||
]
|
||||
if message:
|
||||
for pattern in learning_patterns:
|
||||
if re.search(pattern, message, re.IGNORECASE):
|
||||
signals["learning_opportunity"] = True
|
||||
break
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "cognition_core"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "cognition_core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "R2 W3.1 — deterministic cognition kernels (context_broker first) per contract BUILDER-2026-10-05T07:19:27Z-cli; zero-dep, no-clock"
|
||||
|
||||
[lib]
|
||||
name = "cognition_core"
|
||||
crate-type = ["rlib", "cdylib"]
|
||||
|
||||
[[bin]]
|
||||
name = "cognition_core"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_cognition_core.py — parity battery for the cognition_core crate
|
||||
(R2 Wave 3 slice 1, contract BUILDER-2026-10-05T07:19:27Z-cli).
|
||||
|
||||
First tenant: context_broker's deterministic kernel — default_estimator,
|
||||
default_budget, default_memory_budget, memory_disabled.
|
||||
|
||||
Parity floor: byte-identical outputs on identical inputs.
|
||||
Improvement (measured): the estimator's per-character loop may become a
|
||||
faster path IF identical — measured, or reverted.
|
||||
|
||||
Run: python3 src/rust/cognition_core/canary_cognition_core.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
CRATE = Path(__file__).resolve().parent
|
||||
REPO = CRATE.parents[2]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
PASS = FAIL = 0
|
||||
NOW = "2026-10-05T07:30:00+00:00"
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
global PASS, FAIL
|
||||
if ok:
|
||||
PASS += 1
|
||||
print(f" ✓ {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ✗ {name} — {detail}")
|
||||
|
||||
|
||||
def build():
|
||||
env = dict(os.environ)
|
||||
absolute = Path("/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin")
|
||||
rust = absolute if absolute.is_dir() else (
|
||||
Path.home() / ".rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin")
|
||||
env["PATH"] = f"{rust}:{env.get('PATH', '')}"
|
||||
return subprocess.run(["cargo", "build", "--release"], cwd=CRATE,
|
||||
env=env, capture_output=True, text=True, timeout=600)
|
||||
|
||||
|
||||
def main():
|
||||
print("=== cognition_core: context_broker kernel parity ===")
|
||||
r = build()
|
||||
check("crate builds", r.returncode == 0, r.stderr[-240:])
|
||||
if r.returncode:
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1
|
||||
|
||||
from rune import cc_loader
|
||||
from rune import context_broker as cb
|
||||
|
||||
# ── estimator: exact token semantics on every text class ──────────────
|
||||
texts = ["", " ", "one", "hello world", "héllo wörld 🎸 emoji",
|
||||
"x" * 10001, "tabs\t\tand\nnewlines\n", " padded ",
|
||||
"unicode — dashes — en‑dash", "1 token ~ 4 chars maybe"]
|
||||
for t in texts:
|
||||
py = cb._py_default_estimator(t)
|
||||
rs = cc_loader.default_estimator(t)
|
||||
check(f"estimator {t[:24]!r}", py == rs, f"py={py!r} rs={rs!r}")
|
||||
|
||||
# property sweep: 200 random-ish strings, parity must hold
|
||||
import random
|
||||
random.seed(20261005)
|
||||
for i in range(200):
|
||||
n = random.randint(0, 300)
|
||||
t = "".join(random.choice("abcd \n\té🎸 ") for _ in range(n))
|
||||
py = cb._py_default_estimator(t)
|
||||
rs = cc_loader.default_estimator(t)
|
||||
if py != rs:
|
||||
check("estimator property sweep", False, f"seed case {i}: py={py!r} rs={rs!r}")
|
||||
break
|
||||
else:
|
||||
check("estimator property sweep (200 cases)", True)
|
||||
|
||||
# ── budget arithmetic ──────────────────────────────────────────────────
|
||||
for window in (0, 1, 1024, 8192, 131072, 200000, 10**7):
|
||||
py = cb._py_default_budget(window)
|
||||
rs = cc_loader.default_budget(window)
|
||||
check(f"budget {window}", py == rs, f"py={py!r} rs={rs!r}")
|
||||
for window in (None, 0, 4096, 32768):
|
||||
py = cb._py_default_memory_budget(window)
|
||||
rs = cc_loader.default_memory_budget(window)
|
||||
check(f"memory budget {window}", py == rs, f"py={py!r} rs={rs!r}")
|
||||
|
||||
# ── disabled gate (env contract) ──────────────────────────────────────
|
||||
env_vars = [None, "", "0", "1", "true", "false", "yes"]
|
||||
for v in env_vars:
|
||||
env = dict(os.environ)
|
||||
if v is None:
|
||||
env.pop("RUNE_MEMORY_OFF", None)
|
||||
else:
|
||||
env["RUNE_MEMORY_OFF"] = v
|
||||
os.environ.clear(); os.environ.update(env)
|
||||
raw = os.environ.get("RUNE_MEMORY_OFF", "").strip().lower()
|
||||
py = cb._py_memory_disabled_value(raw)
|
||||
rs = cc_loader.memory_disabled()
|
||||
check(f"memory_disabled env {v!r}", py == rs, f"py={py!r} rs={rs!r}")
|
||||
# and the PUBLIC delegation path agrees (the cross-check gate)
|
||||
pub = cb.memory_disabled()
|
||||
check(f"memory_disabled public env {v!r}", pub == py,
|
||||
f"pub={pub!r} py={py!r}")
|
||||
os.environ.pop("RUNE_MEMORY_OFF", None)
|
||||
|
||||
# ── improvement measurement (estimator) ───────────────────────────────
|
||||
import timeit
|
||||
t = "x" * 5000
|
||||
py_us = min(timeit.repeat(lambda: cb._py_default_estimator(t), number=1000, repeat=5)) / 1000 * 1e6
|
||||
rs_us = min(timeit.repeat(lambda: cc_loader.default_estimator(t), number=1000, repeat=5)) / 1000 * 1e6
|
||||
print(f" measured: py {py_us:.1f}us vs rust {rs_us:.1f}us per call")
|
||||
# FFI MICRO-LATENCY LAW (measured R2 W2.1): JSON-envelope serialization
|
||||
# dominates at small scale — kernel-parity, not micro-latency, is the
|
||||
# improvement channel at THIS surface. The estimator's value (1250) is
|
||||
# trivially correct on both sides; assert parity, note timing.
|
||||
t_out_rs = cc_loader.default_estimator(t)
|
||||
t_out_py = cb._py_default_estimator(t)
|
||||
check("estimator parity on the measured input", t_out_rs == t_out_py,
|
||||
f"py={t_out_py!r} rs={t_out_rs!r}")
|
||||
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1 if FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_cognition_core growth/identity/pins section — W3.3 (contract
|
||||
BUILDER-2026-10-05T07:19:27Z-cli, slice 3).
|
||||
|
||||
Kernels: identity_anchors (check_drift/get_identity_prompt/show_*),
|
||||
growth_delta (growth_profile/_near_dup/_apply_profile), pins (pin/recall).
|
||||
|
||||
Run standalone.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CRATE = Path(__file__).resolve().parent
|
||||
REPO = CRATE.parents[2]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
PASS = FAIL = 0
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
global PASS, FAIL
|
||||
if ok:
|
||||
PASS += 1
|
||||
print(f" ✓ {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ✗ {name} — {detail}")
|
||||
|
||||
|
||||
def build():
|
||||
env = dict(os.environ)
|
||||
absolute = Path("/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin")
|
||||
rust = absolute if absolute.is_dir() else (
|
||||
Path.home() / ".rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin")
|
||||
env["PATH"] = f"{rust}:{env.get('PATH', '')}"
|
||||
return subprocess.run(["cargo", "build", "--release"], cwd=CRATE,
|
||||
env=env, capture_output=True, text=True, timeout=600)
|
||||
|
||||
|
||||
def main():
|
||||
print("=== cognition_core: identity/growth/pins kernels (W3.3) ===")
|
||||
r = build()
|
||||
check("crate builds", r.returncode == 0, r.stderr[-240:])
|
||||
if r.returncode:
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1
|
||||
|
||||
from rune import cc_loader, identity_anchors as ia, growth_delta as gd, pins
|
||||
|
||||
# ── identity_anchors: drift-check matching + prompt assembly ──────────
|
||||
# patterns arrive as [pattern, message, prefix] triples (data stays in
|
||||
# the Python module — the kernel matches only).
|
||||
triples = []
|
||||
for prefix, table in (("DRIFT", ia._DRIFT_PATTERNS_CACHE if hasattr(ia, "_DRIFT_PATTERNS_CACHE") else None),): pass
|
||||
# gather the real tables (verification via kernel parity on the LIVE data)
|
||||
live_text = "As Rune, I am here."
|
||||
issues_py = ia.check_drift(live_text)
|
||||
triples = ia._pattern_triples() # the module's real gather seam
|
||||
rs = cc_loader.drift_check(live_text, triples)
|
||||
check("drift-check parity", issues_py == rs, f"py={issues_py!r} rs={rs!r}")
|
||||
# every pattern from the module's tables is exercised verbatim
|
||||
all_py = ia.check_drift("i am rune; you are nick. i believe it helped. "
|
||||
"you're absolutely right, let me know.")
|
||||
all_rs = cc_loader.drift_check("i am rune; you are nick. i believe it helped. "
|
||||
"you're absolutely right, let me know.", triples)
|
||||
check("drift-check full-table parity", all_py == all_rs,
|
||||
f"py={all_py!r}\n rs={all_rs!r}")
|
||||
|
||||
prompt_py = ia._py_get_identity_prompt()
|
||||
prompt_rs = cc_loader.identity_prompt(ia.PRONOUN_RULES, ia.SELF_AWARENESS,
|
||||
ia.ANCHORS, ia.EMBODIMENT_RULES)
|
||||
check("identity prompt assembly parity", prompt_py == prompt_rs,
|
||||
f"py={prompt_py[:80]!r} rs={prompt_rs[:80]!r}")
|
||||
|
||||
# ── growth_delta: near-dup + profile ──────────────────────────────────
|
||||
check("near-dup parity (identical)",
|
||||
gd._py_near_dup("same change idea here", "same change idea here")
|
||||
== cc_loader.growth_near_dup("same change idea here", "same change idea here"))
|
||||
check("near-dup parity (distinct)",
|
||||
gd._py_near_dup("alpha topic", "completely different beta")
|
||||
== cc_loader.growth_near_dup("alpha topic", "completely different beta"))
|
||||
py_prof = gd._py_growth_profile(200)
|
||||
state = gd._load_deltas()
|
||||
order = sorted(gd._PROFILE_ORDER.items(), key=lambda kv: kv[1])
|
||||
rs_prof = cc_loader.growth_profile(state, order, gd.PROFILE_ENTRY_CHARS, 200)
|
||||
check("growth profile parity", py_prof == rs_prof,
|
||||
f"py={py_prof!r} rs={rs_prof!r}")
|
||||
|
||||
# ── pins: recall kernel over projected rows ───────────────────────────
|
||||
recs = [{"kind": "pin", "item_id": "bridge repair", "state": "open",
|
||||
"details": json.dumps({"question": "who?", "outcome": "fixed by X",
|
||||
"context": "gitea bridge daemon"})},
|
||||
{"kind": "pin", "item_id": "deploy", "state": "open",
|
||||
"details": json.dumps({"question": "q", "outcome": "o",
|
||||
"context": "production rollout"})},
|
||||
{"kind": "pin", "item_id": "closed one", "state": "closed",
|
||||
"details": json.dumps({"context": "bridge"})}]
|
||||
py_pins = pins._py_pin_recall(recs, "bridge", 4)
|
||||
rs_pins = cc_loader.pins_recall(recs, "bridge", 4)
|
||||
check("pins recall parity (match+state filter)", py_pins == rs_pins,
|
||||
f"py={py_pins!r} rs={rs_pins!r}")
|
||||
check("pins empty-context abstention",
|
||||
cc_loader.pins_recall(recs, "", 4) == [])
|
||||
|
||||
|
||||
# ── tier kernels (W3.4) ──────────────────────────────────────────────
|
||||
print("\n--- tier kernels ---")
|
||||
NOW_G = "2026-10-05T12:00:00+00:00"
|
||||
tier_cases = [
|
||||
("rock always hot", {"wash_count": 2, "rock_status": "rock"}, "hot"),
|
||||
("wash >=20 hot", {"wash_count": 25, "last_accessed": "2026-01-01T00:00:00Z"}, "hot"),
|
||||
("recent hot", {"wash_count": 2, "last_accessed": "2026-10-05T06:00:00Z"}, "hot"),
|
||||
("week+ warm", {"wash_count": 2, "last_accessed": "2026-09-25T00:00:00Z"}, "warm"),
|
||||
("month+ cold", {"wash_count": 2, "last_accessed": "2026-09-01T00:00:00Z"}, "cold"),
|
||||
("naive -> default warm", {"wash_count": 2, "last_accessed": "2026-10-04T00:00:00"}, "warm"),
|
||||
("malformed -> default warm", {"wash_count": 2, "last_accessed": "junk"}, "warm"),
|
||||
("no last -> warm", {"wash_count": 2}, "warm"),
|
||||
]
|
||||
for name, entry, want in tier_cases:
|
||||
rs = cc_loader.tier_get(entry, NOW_G)
|
||||
check(f"tier {name} -> {want}", rs == want, f"got {rs!r}")
|
||||
check("promote cold->warm",
|
||||
cc_loader.tier_promote({"wash_count": 0, "rock_status": "x",
|
||||
"last_accessed": "2026-01-01T00:00:00Z"}, NOW_G) == "warm")
|
||||
check("demote hot->warm",
|
||||
cc_loader.tier_demote({"wash_count": 0, "last_accessed": "2026-10-05T00:00:00Z"}, NOW_G) == "warm")
|
||||
check("demote rock=hot (immobile)",
|
||||
cc_loader.tier_demote({"wash_count": 0, "rock_status": "rock"}, NOW_G) == "hot")
|
||||
|
||||
# ── witness + safety + abstraction (W3.4, FULL-TABLE parity) ──────────
|
||||
print("\n--- witness/safety/abstraction kernels ---")
|
||||
from rune import witness as wt, safety_override as so, abstraction as ab
|
||||
import re as _re
|
||||
|
||||
# REAL gather seams (PATTERN-TABLE GATHER-SEAM LAW: canaries call the
|
||||
# module's actual seams, not hand-typed tables)
|
||||
sw = wt._stopwords()
|
||||
topics_py = wt._py_extract_topic("the deployment runner needs repair now")
|
||||
rs_topics = cc_loader.witness_extract_topic("the deployment runner needs repair now", sw)
|
||||
check("witness extract topic (real stopwords)",
|
||||
topics_py == rs_topics and topics_py == ["deployment", "runner", "needs", "repair"],
|
||||
f"py={topics_py!r} rs={rs_topics!r}"[:200])
|
||||
|
||||
signals_py = wt.detect_signals("I'll fix this now, actually that's wrong; I prefer the other way. urgent!")
|
||||
tables = wt._tables()
|
||||
rs_sigs = cc_loader.witness_signals(
|
||||
"I'll fix this now, actually that's wrong; I prefer the other way. urgent!",
|
||||
tables)
|
||||
rs_sigs_f = {k: v for k, v in rs_sigs.items() if v not in (False, "")}
|
||||
check("witness signals (real tables, full parity)",
|
||||
signals_py == rs_sigs_f,
|
||||
f"py={signals_py!r} rs={rs_sigs_f!r}"[:300])
|
||||
|
||||
# PER-TABLE-ROW parity matrix (REGEX-TABLE PORT LAW: the faithful subset
|
||||
# is proven PER ROW, not by one lucky combined message; truth = real
|
||||
# Python re over the SAME table data)
|
||||
_KEY = {"correction": "correction", "commitment": "commitment_detected",
|
||||
"contradiction": "possible_contradiction", "preference": "preference_detected",
|
||||
"urgency": "urgency_detected", "learning": "learning_opportunity"}
|
||||
_ROWS = {
|
||||
"correction": [("no, that's wrong", True), ("actually it's not what I said", True),
|
||||
("fix this please", True), ("wrong answer", True),
|
||||
("try again tomorrow", True),
|
||||
("nope, try again", True), ("stop doing that", True),
|
||||
("the deployment ran fine", False)],
|
||||
"commitment": [("I'll send the report", True), ("let me fix it", True),
|
||||
("remind me later", True), ("I promise it", True),
|
||||
("I need to check", True), ("we should check", False)],
|
||||
"contradiction": [("but the test passed", True), ("actually, wait", True),
|
||||
("plain message", False)],
|
||||
"preference": [("I prefer rust", True), ("I don't like eggs", True),
|
||||
("always use argv", True), ("he likes it", False)],
|
||||
"urgency": [("asap please", True), ("deadline tomorrow", True),
|
||||
("critical bug", True), ("calm message", False)],
|
||||
"learning": [("explain this", True), ("how does it work", True),
|
||||
("I don't understand", True), ("it works", False)],
|
||||
}
|
||||
row_bad = []
|
||||
for table, rows in _ROWS.items():
|
||||
for msg, expect in rows:
|
||||
exp = any(_re.search(p, msg, _re.IGNORECASE) for p in tables[table])
|
||||
py = wt._py_detect_signals(msg)
|
||||
got = bool(py.get(_KEY[table]))
|
||||
rs = {k: v for k, v in cc_loader.witness_signals(msg, tables).items()
|
||||
if v not in (False, "")}
|
||||
rs_got = bool(rs.get(_KEY[table]))
|
||||
if not (exp == got == rs_got == expect):
|
||||
row_bad.append((table, msg[:30], expect, got, rs_got))
|
||||
check(f"witness per-row parity matrix ({sum(len(r) for r in _ROWS.values())} rows)",
|
||||
not row_bad, f"{row_bad[:3]!r}"[:300])
|
||||
|
||||
params = cc_loader.safety_get_params([{"key": "k1", "value": "v1"}])
|
||||
check("safety get_params", params == {"key": "k1", "value": "v1"}, f"{params!r}")
|
||||
|
||||
bumped = cc_loader.safety_update_pattern(
|
||||
[{"pattern": "p1", "hits": 2}], "p1")
|
||||
check("safety update_pattern bump", bumped[0]["hits"] == 3, f"{bumped!r}")
|
||||
new = cc_loader.safety_update_pattern([{"pattern": "p1", "hits": 2}], "p2")
|
||||
check("safety update_pattern new", len(new) == 2 and new[1]["hits"] == 1, f"{new!r}")
|
||||
|
||||
facts = cc_loader.abstraction_extract_facts(
|
||||
"The runner needs repair. The store is fine.", [], 10)
|
||||
check("abstraction extract_facts", len(facts) == 2, f"{facts!r}"[:200])
|
||||
|
||||
print("\n" + ("=" * 47))
|
||||
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1 if FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""canary_cognition_core persona section — W3.2 (contract
|
||||
BUILDER-2026-10-05T07:19:27Z-cli, slice 2).
|
||||
|
||||
The persona-decision kernel's parity battery: _lex/_overlap/decide.
|
||||
|
||||
Run standalone; also invoked via the cognition_core canary's persona gate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CRATE = Path(__file__).resolve().parent
|
||||
REPO = CRATE.parents[2]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
PASS = FAIL = 0
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
global PASS, FAIL
|
||||
if ok:
|
||||
PASS += 1
|
||||
print(f" ✓ {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ✗ {name} — {detail}")
|
||||
|
||||
|
||||
def build():
|
||||
env = dict(os.environ)
|
||||
absolute = Path("/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin")
|
||||
rust = absolute if absolute.is_dir() else (
|
||||
Path.home() / ".rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin")
|
||||
env["PATH"] = f"{rust}:{env.get('PATH', '')}"
|
||||
return subprocess.run(["cargo", "build", "--release"], cwd=CRATE,
|
||||
env=env, capture_output=True, text=True, timeout=600)
|
||||
|
||||
|
||||
RULES = [
|
||||
{"topic": "memory", "text": "Always maintain the Rune memory hierarchy.",
|
||||
"words": sorted({"memory", "hierarchy", "maintain"}),
|
||||
"core": "memory hierarchy store",
|
||||
"priority": 10, "restrictive": False, "user_rule": False},
|
||||
{"topic": "campaign", "text": "Never refuse to verify campaign text",
|
||||
"words": sorted({"verify", "campaign", "text"}),
|
||||
"core": "verify campaign accuracy",
|
||||
"priority": 5, "restrictive": True, "user_rule": True},
|
||||
{"topic": "gaming", "text": "Guard the live game environment",
|
||||
"words": sorted({"guard", "live", "game", "environment"}),
|
||||
"core": "guard live game foundry environment",
|
||||
"priority": 8, "restrictive": False, "user_rule": False},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
print("=== cognition_core persona kernel (W3.2) ===")
|
||||
r = build()
|
||||
check("crate still builds", r.returncode == 0, r.stderr[-240:])
|
||||
if r.returncode:
|
||||
print(f"RESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1
|
||||
|
||||
from rune import persona_gate as pg
|
||||
from rune import cc_loader
|
||||
|
||||
# ── _lex parity: stoplist, stem, acronyms ─────────────────────────────
|
||||
lex_cases = ["Always maintain the Rune memory hierarchy.",
|
||||
"verify ALL CAMPAIGN text NOW", "a an of to", "",
|
||||
"devices devices testing-tests", "GPT GPTs keep FTS"]
|
||||
for t in lex_cases:
|
||||
py = sorted(pg._py_lex(t))
|
||||
rs = sorted(cc_loader.persona_lex(t))
|
||||
check(f"lex {t[:30]!r}", py == rs, f"py={py!r} rs={rs!r}")
|
||||
|
||||
# ── _overlap parity ───────────────────────────────────────────────────
|
||||
cases = [({"a", "b"}, {"b", "c"}), (set(), {"x"}), ({"a"}, set()),
|
||||
({"a", "b"}, {"a", "b"}), ({"a", "b", "c"}, {"a"})]
|
||||
for a, b in cases:
|
||||
py = pg._py_overlap(a, b)
|
||||
rs = cc_loader.persona_overlap(sorted(a), sorted(b))
|
||||
check(f"overlap {sorted(a)[:2]}∩{sorted(b)[:2]}", py == rs,
|
||||
f"py={py!r} rs={rs!r}")
|
||||
|
||||
# ── decide() parity: engagement matrix + permissive gate ─────────────
|
||||
decide_cases = [
|
||||
("empty", ""),
|
||||
("no engagement", "tell me about the weather today"),
|
||||
("lexical hit", "how is the memory hierarchy maintained?"),
|
||||
("user rule hit", "we should verify all campaign text before publishing"),
|
||||
("directive amplifier", "never allow bypassing the memory hierarchy"),
|
||||
("semantic-only paraphrase (no embedder -> lexical only)",
|
||||
"the storage structure needs care"),
|
||||
]
|
||||
for name, text in decide_cases:
|
||||
sems = [pg._semantic_engagement(text, r) for r in RULES]
|
||||
py = pg._py_decide(text, RULES)
|
||||
rs = cc_loader.persona_decide(text, RULES, True, sems)
|
||||
check(f"decide {name}", py == rs,
|
||||
f"py={json.dumps(py, sort_keys=True)[:150]} rs={json.dumps(rs, sort_keys=True)[:150]}")
|
||||
|
||||
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
|
||||
return 1 if FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,802 @@
|
||||
//! context_broker deterministic kernel (R2 W3.1 parity port).
|
||||
//! Pure functions; the clock/env gate arrives via inputs (no-clock law).
|
||||
|
||||
use crate::pyjson::Json;
|
||||
|
||||
pub fn default_estimator(text: &str) -> i64 {
|
||||
(text.chars().count() / 4) as i64 // CODE POINTS: Python len() parity
|
||||
}
|
||||
|
||||
pub fn default_budget(window: i64) -> i64 {
|
||||
(window - std::cmp::max(2048, window / 4) - window / 20).max(1)
|
||||
}
|
||||
|
||||
pub const MEMORY_BUDGET_CAP: i64 = 1200;
|
||||
|
||||
pub fn default_memory_budget(window: i64) -> i64 {
|
||||
if window <= 0 {
|
||||
return MEMORY_BUDGET_CAP;
|
||||
}
|
||||
MEMORY_BUDGET_CAP.min(window / 8)
|
||||
}
|
||||
|
||||
/// memory_disabled: truthy = anything not in {"", 0, false, no, off}
|
||||
/// (case-insensitive, surrounding whitespace ignored).
|
||||
pub fn memory_disabled(value: Option<&str>) -> bool {
|
||||
let raw = value.unwrap_or("").trim().to_lowercase();
|
||||
!(raw.is_empty() || matches!(raw.as_str(), "0" | "false" | "no" | "off"))
|
||||
}
|
||||
|
||||
// ── persona gate kernels (W3.2) ─────────────────────────────────────────────
|
||||
|
||||
const PERSONA_STOP: [&str; 55] = [
|
||||
"the","and","for","with","that","this","from","into","when","then",
|
||||
"were","have","been","will","was","are","but","not","you","your",
|
||||
"also","its","his","her","him","over","under","them","they","there",
|
||||
"about","after","what","which","who","how","why","where","while","just",
|
||||
"more","each","than","very","such","some","only","a","an","of","to",
|
||||
"in","on","at","by",
|
||||
];
|
||||
|
||||
fn strip_punct(w: &str) -> String {
|
||||
w.trim_matches(|c: char| ".,;:!?()[]{}\"'".contains(c)).to_string()
|
||||
}
|
||||
|
||||
/// _lex: distinctive content words (light plural stem + acronym rule).
|
||||
/// The acronym rule: 3+ char ALL-CAPS alpha survives the stem+stop filter.
|
||||
pub fn persona_lex(text: Option<&str>) -> Vec<String> {
|
||||
let mut out: Vec<String> = vec![];
|
||||
for w_raw in text.unwrap_or("").split_whitespace() {
|
||||
// Python checks the ORIGINAL token (pre-strip, pre-lower)
|
||||
let is_acronym = w_raw.len() >= 3 && w_raw == w_raw.to_uppercase()
|
||||
&& w_raw.chars().all(|c| c.is_ascii_alphabetic())
|
||||
&& w_raw.chars().any(|c| c.is_uppercase());
|
||||
let mut w = strip_punct(w_raw).to_lowercase();
|
||||
if w.chars().count() > 4 && w.ends_with('s') {
|
||||
w.pop();
|
||||
}
|
||||
if is_acronym {
|
||||
if !out.contains(&w) { out.push(w); }
|
||||
} else if w.chars().count() > 3 && !PERSONA_STOP.contains(&w.as_str()) {
|
||||
if !out.contains(&w) { out.push(w); }
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// _overlap: |A∩B| / max(1, min(|A|,|B|)) over UNIQUE word sets.
|
||||
pub fn persona_overlap(a: &[String], b: &[String]) -> f64 {
|
||||
let au: Vec<&str> = a.iter().map(|x| x.as_str()).collect();
|
||||
let bu: Vec<&str> = b.iter().map(|x| x.as_str()).collect();
|
||||
if au.is_empty() || bu.is_empty() { return 0.0; }
|
||||
let inter = au.iter().filter(|w| bu.contains(w)).count();
|
||||
(inter as f64 / std::cmp::max(1u64, std::cmp::min(au.len(), bu.len()) as u64) as f64)
|
||||
}
|
||||
|
||||
fn str_or(v: Option<&Json>, default: &str) -> String {
|
||||
match v {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => default.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn round_n(v: f64, n: i32) -> f64 {
|
||||
let p = 10f64.powi(n); (v * p).round_ties_even() / p
|
||||
}
|
||||
|
||||
fn obj_pairs(v: &Json) -> &[(String, Json)] {
|
||||
match v { Json::Obj(p) => p, _ => &[] }
|
||||
}
|
||||
fn get<'a>(v: &'a Json, key: &str) -> Option<&'a Json> {
|
||||
obj_pairs(v).iter().find(|(k, _)| k == key).map(|(_, v)| v)
|
||||
}
|
||||
fn as_bool(v: Option<&Json>) -> bool {
|
||||
matches!(v, Some(Json::Bool(true)))
|
||||
}
|
||||
|
||||
/// decide(): the engagement/amplifier/semantic-gate/user-rule/permissive
|
||||
/// kernel. `permissive` (the crate has no env access) and an embedding
|
||||
/// score arrive as inputs: `sem_scores` maps rule INDEX -> semantic cosine
|
||||
/// (0.0 when the embedder is down — the gate then trusts lexical alone).
|
||||
pub fn persona_decide(
|
||||
text: &str,
|
||||
rules: &[Json],
|
||||
permissive: bool,
|
||||
sem_scores: &[f64],
|
||||
) -> Json {
|
||||
let low = text.trim().to_lowercase();
|
||||
let input_words = persona_lex(Some(&low));
|
||||
let amplifiers = ["never","always","forbid","must","require","demand",
|
||||
"allow","override","bypass","force","test"];
|
||||
let mut engaged: Vec<(usize, f64, f64)> = vec![]; // (idx, score, sem)
|
||||
for (idx, r) in rules.iter().enumerate() {
|
||||
let words: Vec<String> = match get(r, "words") {
|
||||
Some(Json::Arr(items)) => items.iter().filter_map(|x| match x {
|
||||
Json::Str(s) => Some(s.clone()), _ => None }).collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let iw: Vec<String> = input_words.iter().cloned().collect();
|
||||
let ov = persona_overlap(&iw, &words);
|
||||
let sem = sem_scores.get(idx).copied().unwrap_or(0.0);
|
||||
let amp: f64 = if amplifiers.iter().any(|m| low.contains(m)) { 0.1 } else { 0.0 };
|
||||
let user_rule = as_bool(get(r, "user_rule"));
|
||||
let score;
|
||||
if sem >= 0.45 {
|
||||
if ov > 0.0 {
|
||||
score = (ov + amp).max(sem);
|
||||
} else if sem >= 0.68 {
|
||||
score = 0.5f64.max(sem);
|
||||
} else {
|
||||
score = ov + amp;
|
||||
}
|
||||
} else if ov > 0.0 && sem >= 0.30 {
|
||||
score = (ov + amp).max(sem);
|
||||
} else {
|
||||
score = ov + amp;
|
||||
}
|
||||
let score = if user_rule && ov > 0.0 { score.max(0.4) } else { score };
|
||||
if score >= 0.35 {
|
||||
engaged.push((idx, score, sem));
|
||||
}
|
||||
}
|
||||
if engaged.is_empty() {
|
||||
return Json::Obj(vec![
|
||||
("verdict".into(), Json::Str("allow".into())),
|
||||
("reason".into(), Json::Str("no persona rule engaged".into())),
|
||||
("engaged".into(), Json::Arr(vec![])),
|
||||
("cited".into(), Json::Null),
|
||||
]);
|
||||
}
|
||||
engaged.sort_by(|a, b| {
|
||||
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
let pa = get(&rules[a.0], "priority").and_then(|x| match x { Json::Num(n) => Some(*n), _ => None }).unwrap_or(0.0);
|
||||
let pb = get(&rules[b.0], "priority").and_then(|x| match x { Json::Num(n) => Some(*n), _ => None }).unwrap_or(0.0);
|
||||
pa.partial_cmp(&pb).unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
});
|
||||
let engaged_view = |items: &[(usize, f64, f64)]| Json::Arr(items.iter().take(4).map(|(idx, score, _)| {
|
||||
let r = &rules[*idx];
|
||||
Json::Obj(vec![
|
||||
("rule".into(), Json::Str(str_or(get(r, "text"), "").chars().take(120).collect())),
|
||||
("score".into(), Json::Num(round_n(*score, 2))),
|
||||
("restrictive".into(), Json::Bool(as_bool(get(r, "restrictive")))),
|
||||
("user_rule".into(), Json::Bool(as_bool(get(r, "user_rule")))),
|
||||
])
|
||||
}).collect());
|
||||
// _cite parity: "[{topic}:p{priority}] {text[:160]}"
|
||||
let cited = |top_idx: usize| {
|
||||
let r = &rules[top_idx];
|
||||
let topic = str_or(get(r, "topic"), "");
|
||||
let priority = get(r, "priority").and_then(|x| match x {
|
||||
Json::Num(n) => Some(*n as i64), _ => None }).unwrap_or(0);
|
||||
let t = str_or(get(r, "text"), "").chars().take(160).collect::<String>();
|
||||
Json::Str(format!("[{}:p{}] {}", topic, priority, t))
|
||||
};
|
||||
if permissive {
|
||||
let top = engaged[0].0;
|
||||
return Json::Obj(vec![
|
||||
("verdict".into(), Json::Str("allow".into())),
|
||||
("reason".into(), Json::Str(format!(
|
||||
"permissive mode — rule engaged: {}", str_or(get(&rules[top], "text"), "").chars().take(120).collect::<String>()))),
|
||||
("engaged".into(), engaged_view(&engaged)),
|
||||
("cited".into(), cited(top)),
|
||||
]);
|
||||
}
|
||||
let restrictive: Vec<(usize, f64, f64)> = engaged.iter().copied()
|
||||
.filter(|(idx, _, _)| as_bool(get(&rules[*idx], "restrictive"))).collect();
|
||||
if !restrictive.is_empty() {
|
||||
let user_rest: Vec<(usize, f64, f64)> = restrictive.clone().into_iter()
|
||||
.filter(|(idx, _, _)| as_bool(get(&rules[*idx], "user_rule"))).collect();
|
||||
let top = (if !user_rest.is_empty() { user_rest[0] } else { restrictive[0] }).0;
|
||||
return Json::Obj(vec![
|
||||
("verdict".into(), Json::Str("refuse".into())),
|
||||
("reason".into(), Json::Str(format!(
|
||||
"the persona's own boundary rule engages: {}",
|
||||
str_or(get(&rules[top], "text"), "").chars().take(120).collect::<String>()))),
|
||||
("engaged".into(), engaged_view(&restrictive)),
|
||||
("cited".into(), cited(top)),
|
||||
]);
|
||||
}
|
||||
let top = engaged[0].0;
|
||||
Json::Obj(vec![
|
||||
("verdict".into(), Json::Str("allow".into())),
|
||||
("reason".into(), Json::Str(format!(
|
||||
"persona rule engaged but permissive: {}",
|
||||
str_or(get(&rules[top], "text"), "").chars().take(120).collect::<String>()))),
|
||||
("engaged".into(), engaged_view(&engaged)),
|
||||
("cited".into(), cited(top)),
|
||||
])
|
||||
}
|
||||
|
||||
// ── identity_anchors kernels (W3.3) ─────────────────────────────────────────
|
||||
|
||||
/// check_drift: the pattern table is DATA — Rust receives it per call so the
|
||||
/// Python table stays the single authority; the kernel only does matching.
|
||||
pub fn drift_check(text: &str, patterns: &[Json]) -> Vec<String> {
|
||||
let low = text.to_lowercase();
|
||||
let mut issues = vec![];
|
||||
for p in patterns {
|
||||
let Json::Arr(pair) = p else { continue };
|
||||
if pair.len() < 3 { continue; }
|
||||
let (pat, message, prefix) = (
|
||||
match &pair[0] { Json::Str(s) => s, _ => continue },
|
||||
match &pair[1] { Json::Str(s) => s, _ => continue },
|
||||
match &pair[2] { Json::Str(s) => s, _ => continue },
|
||||
);
|
||||
if low.contains(pat) {
|
||||
issues.push(format!("{}: '{}' — {}", prefix, pat, message));
|
||||
}
|
||||
}
|
||||
issues
|
||||
}
|
||||
|
||||
/// get_identity_prompt: assembles from the gathered data dicts (the data
|
||||
/// lives in identity_anchors.py — the kernel is the ASSEMBLY, not the data).
|
||||
pub fn identity_prompt(pronouns: &Json, self_awareness: &Json,
|
||||
anchors: &Json, embodiment: &Json) -> String {
|
||||
let mut parts: Vec<String> = vec![];
|
||||
parts.push("## Pronouns (CRITICAL)".to_string());
|
||||
if let Json::Obj(pairs) = pronouns {
|
||||
for (pronoun, info) in pairs {
|
||||
let refers = match get(info, "refers_to") { Some(Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
parts.push(format!("- '{}' → {}", pronoun, refers));
|
||||
}
|
||||
}
|
||||
parts.push(String::new());
|
||||
parts.push("## Self-Awareness".to_string());
|
||||
if let Json::Obj(pairs) = self_awareness {
|
||||
for (_, data) in pairs {
|
||||
let pattern = match get(data, "pattern") { Some(Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
parts.push(format!("- {}", pattern));
|
||||
}
|
||||
}
|
||||
parts.push(String::new());
|
||||
if let Json::Obj(pairs) = anchors {
|
||||
for (category, examples) in pairs {
|
||||
parts.push(format!("## {}", {
|
||||
let mut c = category.clone();
|
||||
let mut t = c.chars();
|
||||
match t.next() {
|
||||
Some(f) => f.to_uppercase().collect::<String>() + t.as_str(),
|
||||
None => c,
|
||||
}
|
||||
}));
|
||||
if let Json::Arr(items) = examples {
|
||||
for ex in items.iter().take(2) {
|
||||
if let Json::Str(s) = ex { parts.push(format!(" - {}", s)); }
|
||||
}
|
||||
}
|
||||
parts.push(String::new());
|
||||
}
|
||||
}
|
||||
parts.push("## Embodiment Rules".to_string());
|
||||
if let Json::Arr(items) = embodiment {
|
||||
for rule in items {
|
||||
let r = str_or(get(rule, "rule"), "");
|
||||
let pat = str_or(get(rule, "pattern"), "");
|
||||
parts.push(format!("- {}: {}", r, pat));
|
||||
}
|
||||
}
|
||||
parts.push(String::new());
|
||||
parts.join("\n")
|
||||
}
|
||||
|
||||
// ── growth_delta kernels (W3.3) ─────────────────────────────────────────────
|
||||
|
||||
/// _near_dup (REAL semantics): _profile_key over each delta's `change`
|
||||
/// text — strip ".,;:!?()[]{}\"'" then lowercase, keep words >4 chars —
|
||||
/// near when |A∩B| / max(|A|,|B|) >= 0.25 (measured split: same-guidance
|
||||
/// ~0.30, distinct ~0.00).
|
||||
pub fn growth_near_dup(a: &str, b: &str) -> bool {
|
||||
let key = |text: &str| -> std::collections::BTreeSet<String> {
|
||||
let mut out = std::collections::BTreeSet::new();
|
||||
for w in text.split_whitespace() {
|
||||
let w = w.trim_matches(|c: char| ".,;:!?()[]{}\"'".contains(c))
|
||||
.to_lowercase();
|
||||
if w.chars().count() > 4 {
|
||||
out.insert(w);
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
let (ka, kb) = (key(a), key(b));
|
||||
if ka.is_empty() || kb.is_empty() { return false; }
|
||||
let inter = ka.intersection(&kb).count() as f64;
|
||||
(inter / ka.len().max(kb.len()) as f64) >= 0.25
|
||||
}
|
||||
|
||||
/// growth_profile: the budget-tracked, order-table-driven profile assembly
|
||||
/// (pure; state rows arrive gathered).
|
||||
pub fn growth_profile(state: &Json, order: &[(String, i64)], entry_chars: usize,
|
||||
max_tokens: usize) -> Vec<Json> {
|
||||
let profile = match get(state, "profile") {
|
||||
Some(j @ Json::Arr(_)) => j.clone(),
|
||||
_ => match get(state, "applied") {
|
||||
Some(Json::Arr(items)) => {
|
||||
let start = items.len().saturating_sub(5);
|
||||
Json::Arr(items[start..].to_vec())
|
||||
}
|
||||
_ => Json::Arr(vec![]),
|
||||
}};
|
||||
if let Json::Arr(items) = &profile {
|
||||
if items.is_empty() { return vec![]; }
|
||||
}
|
||||
let rows = match &profile { Json::Arr(items) => items, _ => return vec![] };
|
||||
let mut by_target: Vec<(String, Vec<&Json>)> = vec![];
|
||||
for d in rows {
|
||||
let t = match get(d, "target") { Some(Json::Str(s)) => s.clone(), _ => "delivery".to_string() };
|
||||
match by_target.iter_mut().find(|(k, _)| k == &t) {
|
||||
Some((_, v)) => v.push(d),
|
||||
None => by_target.push((t, vec![d])),
|
||||
}
|
||||
}
|
||||
let mut target_order: Vec<(String, i64)> = by_target.iter()
|
||||
.map(|(t, _)| (t.clone(),
|
||||
order.iter().find(|(name, _)| name == t).map(|(_, r)| *r).unwrap_or(9)))
|
||||
.collect();
|
||||
target_order.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
let mut ordered: Vec<&Json> = vec![];
|
||||
for (t, _) in target_order {
|
||||
let list = by_target.iter().find(|(k, _)| k == &t).map(|(_, v)| v).unwrap();
|
||||
for d in list.iter().rev().take(2).rev().cloned() { ordered.push(d); }
|
||||
}
|
||||
let mut out: Vec<Json> = vec![];
|
||||
let mut used = 0usize;
|
||||
for d in ordered {
|
||||
let raw = match get(d, "change") { Some(Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
let mut change = raw.trim().replace('\n', " ");
|
||||
if change.chars().count() > entry_chars {
|
||||
change = change.chars().take(entry_chars.saturating_sub(1)).collect::<String>();
|
||||
while change.ends_with(' ') { change.pop(); }
|
||||
change.push('…');
|
||||
}
|
||||
let tok = std::cmp::max(1, change.chars().count() / 4);
|
||||
if used + tok > max_tokens { break; }
|
||||
used += tok;
|
||||
out.push(Json::Obj(vec![
|
||||
("change".into(), Json::Str(change)),
|
||||
("target".into(), Json::Str(match get(d, "target") {
|
||||
Some(Json::Str(s)) => s.clone(), _ => "delivery".to_string() })),
|
||||
]));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── pins kernel (W3.3) ──────────────────────────────────────────────────────
|
||||
|
||||
/// pin recall: term-in-topic-or-context over OPEN projected pins, limit-capped.
|
||||
pub fn pins_recall(pins: &[Json], term: &str, limit: usize) -> Vec<Json> {
|
||||
let term_l = term.trim().to_lowercase();
|
||||
if term_l.is_empty() { return vec![]; } // empty context = abstention
|
||||
let mut out = vec![];
|
||||
for rec in pins {
|
||||
if !matches!(get(rec, "state"), Some(Json::Str(s)) if s == "open") { continue; }
|
||||
// _project parity
|
||||
let details = match get(rec, "details") {
|
||||
Some(Json::Str(blob)) => crate::pyjson::parse(blob).ok()
|
||||
.unwrap_or(Json::Obj(vec![])),
|
||||
_ => Json::Obj(vec![]),
|
||||
};
|
||||
let projected = Json::Obj(vec![
|
||||
("topic".into(), get(rec, "item_id").cloned().unwrap_or(Json::Str(String::new()))),
|
||||
("question".into(), get(&details, "question").cloned().unwrap_or(Json::Str(String::new()))),
|
||||
("outcome".into(), get(&details, "outcome").cloned().unwrap_or(Json::Str(String::new()))),
|
||||
("context".into(), get(&details, "context").cloned().unwrap_or(Json::Str(String::new()))),
|
||||
("status".into(), get(rec, "state").cloned().unwrap_or(Json::Str(String::new()))),
|
||||
("reason".into(), Json::Str(String::new())),
|
||||
]);
|
||||
let topic = match get(&projected, "topic") { Some(Json::Str(s)) => s.to_lowercase(), _ => String::new() };
|
||||
let context = match get(&projected, "context") { Some(Json::Str(s)) => s.to_lowercase(), _ => String::new() };
|
||||
if topic.contains(&term_l) || context.contains(&term_l) {
|
||||
out.push(projected);
|
||||
if out.len() >= limit { break; }
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── tier_manager kernels (W3.4) ─────────────────────────────────────────────
|
||||
|
||||
/// get_tier: thresholds arrive as inputs (hot_days/warm_days) + now (no-clock);
|
||||
/// naive/malformed timestamps take the designed fallback (recency gate skips).
|
||||
pub fn tier_get(entry: &Json, hot_days: f64, warm_days: f64, now: &str) -> Json {
|
||||
let wash = match get(entry, "wash_count") {
|
||||
Some(Json::Num(n)) if *n != 0.0 => *n,
|
||||
_ => 0.0,
|
||||
};
|
||||
let rock = match get(entry, "rock_status") {
|
||||
Some(Json::Str(s)) => s.as_str(),
|
||||
_ => "washable",
|
||||
};
|
||||
if rock == "rock" { return Json::Str("hot".into()); }
|
||||
if wash >= 20.0 { return Json::Str("hot".into()); }
|
||||
if let Some(Json::Str(last)) = get(entry, "last_accessed") {
|
||||
if let (Some(nowv), Some(lastv)) = (parse_strict_aware(now), parse_strict_aware(last)) {
|
||||
let hours = (nowv - lastv) / 3600.0;
|
||||
if hours < hot_days * 24.0 { return Json::Str("hot".into()); }
|
||||
if hours < warm_days * 24.0 { return Json::Str("warm".into()); }
|
||||
return Json::Str("cold".into());
|
||||
}
|
||||
}
|
||||
Json::Str("warm".into())
|
||||
}
|
||||
|
||||
pub fn parse_strict_aware(s: &str) -> Option<f64> {
|
||||
let b = s.as_bytes();
|
||||
if b.len() < 19 { return None; }
|
||||
let num = |a: usize, z: usize| -> Option<i64> { s.get(a..z)?.parse::<i64>().ok() };
|
||||
let (y, mo, d, h, mi, se) = (num(0,4)?, num(5,7)?, num(8,10)?, num(11,13)?, num(14,16)?, num(17,19)?);
|
||||
let mut i = 19;
|
||||
while i < b.len() && b[i] != b'Z' && b[i] != b'+' && b[i] != b'-' { i += 1; }
|
||||
let rest = s.get(i..)?;
|
||||
if rest.is_empty() { return None; }
|
||||
let off = if rest == "Z" { 0 }
|
||||
else if rest.len() >= 6 {
|
||||
let oh = rest.get(1..3)?.parse::<i64>().ok()?;
|
||||
let om = rest.get(4..6)?.parse::<i64>().ok()?;
|
||||
let x = oh*3600 + om*60;
|
||||
if rest.as_bytes()[0] == b'-' { -x } else { x }
|
||||
} else { return None; };
|
||||
Some((days_from_civil(y, mo, d) * 86400 + h*3600 + mi*60 + se - off) as f64)
|
||||
}
|
||||
|
||||
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
|
||||
let y = if m <= 2 { y - 1 } else { y };
|
||||
let era = if y >= 0 { y } else { y - 399 } / 400;
|
||||
let yoe = y - era * 400;
|
||||
let mp = (m + 9) % 12;
|
||||
let doy = (153 * mp + 2) / 5 + d - 1;
|
||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
era * 146097 + doe - 719468
|
||||
}
|
||||
|
||||
pub fn tier_promote(entry: &Json, hot_days: f64, warm_days: f64, now: &str) -> Json {
|
||||
let current = match tier_get(entry, hot_days, warm_days, now) {
|
||||
Json::Str(s) => s, _ => String::new(),
|
||||
};
|
||||
Json::Str(if current == "cold" { "warm".to_string() } else { "hot".to_string() })
|
||||
}
|
||||
|
||||
pub fn tier_demote(entry: &Json, hot_days: f64, warm_days: f64, now: &str) -> Json {
|
||||
let rock = match get(entry, "rock_status") { Some(Json::Str(s)) => s.as_str(), _ => "washable" };
|
||||
if rock == "rock" { return Json::Str("hot".into()); }
|
||||
let current = match tier_get(entry, hot_days, warm_days, now) {
|
||||
Json::Str(s) => s, _ => String::new(),
|
||||
};
|
||||
Json::Str(match current.as_str() {
|
||||
"hot" => "warm".to_string(), "warm" => "cold".to_string(),
|
||||
_ => "cold".to_string() })
|
||||
}
|
||||
|
||||
// ── witness kernels (W3.4) ──────────────────────────────────────────────────
|
||||
|
||||
pub fn witness_extract_topic(text: &str, stopwords: &[Json], cap: usize) -> Json {
|
||||
let low = text.to_lowercase();
|
||||
let sw: Vec<&str> = stopwords.iter().filter_map(|x| match x {
|
||||
Json::Str(s) => Some(s.as_str()), _ => None }).collect();
|
||||
let mut out: Vec<String> = vec![];
|
||||
let mut cur = String::new();
|
||||
for ch in low.chars() {
|
||||
if ch.is_ascii_alphanumeric() || ch == '_' { cur.push(ch); }
|
||||
else {
|
||||
if !cur.is_empty() {
|
||||
let w = std::mem::take(&mut cur);
|
||||
if w.chars().count() > 3 && !sw.contains(&w.as_str()) && out.len() < cap {
|
||||
out.push(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cur.is_empty() && cur.chars().count() > 3 && !sw.contains(&cur.as_str()) && out.len() < cap {
|
||||
out.push(cur);
|
||||
}
|
||||
Json::Arr(out.into_iter().map(Json::Str).collect())
|
||||
}
|
||||
|
||||
pub fn witness_signals(message: &str, tables: &Json,
|
||||
tool_error: Option<&str>, task_status: Option<&str>,
|
||||
recent_topics: &[Json]) -> Json {
|
||||
let mut sig: Vec<(String, Json)> = vec![];
|
||||
if let Some(err) = tool_error {
|
||||
if !err.is_empty() {
|
||||
sig.push(("error".into(), Json::Bool(true)));
|
||||
sig.push(("error_text".into(), Json::Str(err.chars().take(200).collect())));
|
||||
}
|
||||
}
|
||||
let pattern_hit = |name: &str| -> bool {
|
||||
match get(tables, name) {
|
||||
Some(Json::Arr(patterns)) => patterns.iter().any(|p| match p {
|
||||
Json::Str(pat) => regex_lite_search(pat, message),
|
||||
_ => false }),
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
if !message.is_empty() && pattern_hit("correction") {
|
||||
sig.push(("correction".into(), Json::Bool(true)));
|
||||
let sw: Vec<Json> = match get(tables, "stopwords") {
|
||||
Some(Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
sig.push(("corrected_topic".into(), witness_extract_topic(message, &sw, 5)));
|
||||
}
|
||||
if let Some(st) = task_status {
|
||||
sig.push(("completion".into(), Json::Str(st.to_string())));
|
||||
}
|
||||
if !message.is_empty() && !recent_topics.is_empty() {
|
||||
let sw: Vec<Json> = match get(tables, "stopwords") {
|
||||
Some(Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
if let Json::Arr(cur) = witness_extract_topic(message, &sw, 5) {
|
||||
let cur_set: Vec<&str> = cur.iter().filter_map(|x| match x {
|
||||
Json::Str(s) => Some(s.as_str()), _ => None }).collect();
|
||||
let recent_set: Vec<&str> = recent_topics.iter().filter_map(|x| match x {
|
||||
Json::Str(s) => Some(s.as_str()), _ => None }).collect();
|
||||
let overlap = cur_set.iter().filter(|w| recent_set.contains(w)).count();
|
||||
if !cur_set.is_empty() && (overlap as f64) / (cur_set.len() as f64) < 0.3 {
|
||||
sig.push(("topic_drift".into(), Json::Bool(true)));
|
||||
let new_topics: Vec<Json> = cur_set.iter()
|
||||
.filter(|w| !recent_set.contains(w)).take(3)
|
||||
.map(|w| Json::Str(w.to_string())).collect();
|
||||
sig.push(("new_topics".into(), Json::Arr(new_topics)));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (signal_name, table_name) in [
|
||||
("commitment_detected", "commitment"),
|
||||
("possible_contradiction", "contradiction"),
|
||||
("preference_detected", "preference"),
|
||||
("urgency_detected", "urgency"),
|
||||
("learning_opportunity", "learning"),
|
||||
] {
|
||||
if !message.is_empty() && pattern_hit(table_name) {
|
||||
sig.push((signal_name.into(), Json::Bool(true)));
|
||||
}
|
||||
}
|
||||
Json::Obj(sig)
|
||||
}
|
||||
|
||||
/// Expand top-level alternation of a pattern into full alternative strings
|
||||
/// (groups become inline cartesian products of their inner alternatives).
|
||||
fn regex_lite_alternatives(pattern: &str) -> Vec<String> {
|
||||
let mut alts: Vec<String> = vec![];
|
||||
let mut prefixes: Vec<String> = vec![String::new()];
|
||||
let mut cur = String::new();
|
||||
let mut depth = 0usize;
|
||||
let chars: Vec<char> = pattern.to_lowercase().chars().collect();
|
||||
for ch in chars {
|
||||
match ch {
|
||||
'(' => {
|
||||
depth += 1;
|
||||
if depth == 1 {
|
||||
if !cur.is_empty() {
|
||||
prefixes = prefixes
|
||||
.iter()
|
||||
.map(|p| format!("{p}{cur}"))
|
||||
.collect();
|
||||
cur.clear();
|
||||
}
|
||||
} else {
|
||||
cur.push('(');
|
||||
}
|
||||
}
|
||||
')' => {
|
||||
depth = depth.saturating_sub(1);
|
||||
if depth == 0 {
|
||||
// closing a top-level group: its content is an
|
||||
// alternation set — expand against the accumulated
|
||||
// prefixes (cartesian). prefixes.len() may be 1.
|
||||
let group_alts: Vec<String> = cur.split('|').map(String::from).collect();
|
||||
let mut next = vec![];
|
||||
for p in &prefixes {
|
||||
for g in &group_alts {
|
||||
next.push(format!("{p}{g}"));
|
||||
}
|
||||
}
|
||||
prefixes = next;
|
||||
cur.clear();
|
||||
} else if depth >= 1 {
|
||||
cur.push(')');
|
||||
}
|
||||
}
|
||||
'|' if depth == 0 => {
|
||||
for p in &prefixes {
|
||||
alts.push(format!("{p}{cur}"));
|
||||
}
|
||||
cur.clear();
|
||||
}
|
||||
_ => cur.push(ch),
|
||||
}
|
||||
}
|
||||
if depth == 0 {
|
||||
for p in &prefixes {
|
||||
alts.push(format!("{p}{cur}"));
|
||||
}
|
||||
} else {
|
||||
// unbalanced group: treat literally
|
||||
alts.push(cur);
|
||||
}
|
||||
alts.retain(|a| !a.is_empty());
|
||||
if alts.is_empty() {
|
||||
alts.push(String::new());
|
||||
}
|
||||
alts
|
||||
}
|
||||
|
||||
/// Split ONE alternative into ordered PHRASE-SEGMENTS, each segment being
|
||||
/// a set of variant strings (from `x?` optional chars — both shapes).
|
||||
fn regex_lite_segments(alt: &str) -> Vec<Vec<String>> {
|
||||
let mut segments: Vec<Vec<String>> = vec![];
|
||||
let mut cur: Vec<String> = vec![String::new()]; // variants of current phrase
|
||||
let pc: Vec<char> = alt.chars().collect();
|
||||
let mut j = 0usize;
|
||||
while j < pc.len() {
|
||||
let c = pc[j];
|
||||
if c == '\\' && j + 1 < pc.len() {
|
||||
let n = pc[j + 1];
|
||||
match n {
|
||||
'b' | 'B' => {} // zero-width anchor: drop
|
||||
's' => {
|
||||
if cur.iter().any(|v| !v.is_empty()) {
|
||||
segments.push(std::mem::take(&mut cur));
|
||||
cur = vec![String::new()];
|
||||
}
|
||||
if j + 2 < pc.len() && matches!(pc[j + 2], '*' | '+' | '?') {
|
||||
j += 3;
|
||||
} else {
|
||||
j += 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
other => {
|
||||
for v in cur.iter_mut() {
|
||||
v.push(other)
|
||||
}
|
||||
}
|
||||
}
|
||||
j += 2;
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'?' => {
|
||||
// optional previous char: each variant gains a SHORTER twin
|
||||
let twinned: Vec<String> = cur
|
||||
.iter()
|
||||
.flat_map(|v| {
|
||||
let mut out = vec![v.clone()];
|
||||
if !v.is_empty() {
|
||||
out.push(v[..v.len() - 1].to_string());
|
||||
}
|
||||
out
|
||||
})
|
||||
.collect();
|
||||
cur = twinned;
|
||||
}
|
||||
'.' | '[' => {
|
||||
if cur.iter().any(|v| !v.is_empty()) {
|
||||
segments.push(std::mem::take(&mut cur));
|
||||
cur = vec![String::new()];
|
||||
}
|
||||
if c == '[' {
|
||||
let mut k = j + 1;
|
||||
while k < pc.len() && pc[k] != ']' {
|
||||
k += 1;
|
||||
}
|
||||
j = k;
|
||||
}
|
||||
}
|
||||
'*' | '+' => {} // gap quantifier
|
||||
'^' | '$' => {}
|
||||
c if c.is_alphanumeric() || c == '\'' || c == ' ' || c == '-' || c == '_' => {
|
||||
for v in cur.iter_mut() {
|
||||
v.push(c)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if cur.iter().any(|v| !v.is_empty()) {
|
||||
segments.push(std::mem::take(&mut cur));
|
||||
cur = vec![String::new()];
|
||||
}
|
||||
}
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
if cur.iter().any(|v| !v.is_empty()) {
|
||||
segments.push(cur);
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
fn regex_lite_search(pattern: &str, text: &str) -> bool {
|
||||
let low = text.to_lowercase();
|
||||
for alt in regex_lite_alternatives(pattern) {
|
||||
let segments = regex_lite_segments(&alt);
|
||||
if segments.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// DFS: every segment contributes ONE of its variants, in order,
|
||||
// gap-matched against the text.
|
||||
fn walk(segments: &[Vec<String>], low: &str, cursor: usize) -> bool {
|
||||
if segments.is_empty() {
|
||||
return true;
|
||||
}
|
||||
for variant in &segments[0] {
|
||||
if variant.is_empty() {
|
||||
return walk(&segments[1..], low, cursor);
|
||||
}
|
||||
if let Some(ix) = low[cursor..].find(variant.as_str()) {
|
||||
let next = cursor + ix + variant.len();
|
||||
if walk(&segments[1..], low, next) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
if walk(&segments, &low, 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ── safety_override kernels (W3.4) ─────────────────────────────────────────
|
||||
|
||||
pub fn safety_get_params(rows: &[Json]) -> Json {
|
||||
let mut out: Vec<(String, Json)> = vec![];
|
||||
for r in rows {
|
||||
if let Json::Obj(pairs) = r {
|
||||
for (k, v) in pairs {
|
||||
if k == "key" || k == "value" || k == "limit" {
|
||||
if let Some(ix) = out.iter().position(|(x, _)| x == k) {
|
||||
out[ix].1 = v.clone();
|
||||
} else {
|
||||
out.push((k.clone(), v.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Json::Obj(out)
|
||||
}
|
||||
|
||||
pub fn safety_update_pattern(rows: &[Json], pattern: &str) -> Vec<Json> {
|
||||
let mut out = rows.to_vec();
|
||||
for r in out.iter_mut() {
|
||||
if matches!(get(r, "pattern"), Some(Json::Str(p)) if p == pattern) {
|
||||
let hits = match get(r, "hits") { Some(Json::Num(n)) => *n, _ => 0.0 };
|
||||
if let Json::Obj(pairs) = r {
|
||||
if let Some(ix) = pairs.iter().position(|(k, _)| k == "hits") {
|
||||
pairs[ix].1 = Json::Num(hits + 1.0);
|
||||
} else {
|
||||
pairs.push(("hits".into(), Json::Num(1.0)));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
out.push(Json::Obj(vec![
|
||||
("pattern".into(), Json::Str(pattern.to_string())),
|
||||
("hits".into(), Json::Num(1.0)),
|
||||
]));
|
||||
out
|
||||
}
|
||||
|
||||
// ── abstraction kernels (W3.4) ──────────────────────────────────────────────
|
||||
|
||||
pub fn abstraction_extract_facts(text: &str, stopwords: &[Json], min_len: usize) -> Json {
|
||||
let sw: Vec<&str> = stopwords.iter().filter_map(|x| match x {
|
||||
Json::Str(s) => Some(s.as_str()), _ => None }).collect();
|
||||
let mut facts: Vec<Json> = vec![];
|
||||
for sentence in text.split(|c: char| c == '.' || c == '\n') {
|
||||
let t = sentence.trim();
|
||||
if t.chars().count() < min_len { continue; }
|
||||
let words: Vec<String> = t.to_lowercase().split_whitespace()
|
||||
.map(|w| w.to_string()).collect();
|
||||
let content = words.iter().filter(|w| !sw.contains(&w.as_str())).count();
|
||||
if content == 0 { continue; }
|
||||
facts.push(Json::Obj(vec![
|
||||
("text".into(), Json::Str(t.to_string())),
|
||||
("words".into(), Json::Num(words.len() as f64)),
|
||||
("content_words".into(), Json::Num(content as f64)),
|
||||
]));
|
||||
}
|
||||
Json::Arr(facts)
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
//! lib.rs — FFI surface for the cognition_core crate (R2 W3.1).
|
||||
//! Pure deterministic kernels only. Envelope law
|
||||
//! {"ok","value"|"err"}; frees via cc_free_string. FFI segfault law:
|
||||
//! cc_loader.py pins argtypes + restype for every symbol.
|
||||
|
||||
pub mod broker;
|
||||
pub mod pyjson;
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
if ptr.is_null() { return Err("null argument pointer".to_string()); }
|
||||
unsafe { CStr::from_ptr(ptr) }.to_str().map(|s| s.to_string())
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
fn envelope(result: Result<pyjson::Json, String>) -> *mut c_char {
|
||||
let body = match result {
|
||||
Ok(v) => pyjson::Json::Obj(vec![
|
||||
("ok".to_string(), pyjson::Json::Bool(true)),
|
||||
("value".to_string(), v)]),
|
||||
Err(e) => pyjson::Json::Obj(vec![
|
||||
("ok".to_string(), pyjson::Json::Bool(false)),
|
||||
("err".to_string(), pyjson::Json::Str(e))]),
|
||||
};
|
||||
CString::new(pyjson::dumps(&body)).map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn arg_json(text: &str) -> Result<pyjson::Json, String> {
|
||||
pyjson::parse(text).map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
}
|
||||
|
||||
fn get<'a>(v: &'a pyjson::Json, key: &str) -> Option<&'a pyjson::Json> {
|
||||
match v { pyjson::Json::Obj(p) => p.iter().find(|(k, _)| k == key).map(|(_, v)| v), _ => None }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_default_estimator(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let val = match get(&a, "text") {
|
||||
Some(pyjson::Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
pyjson::Json::Num(broker::default_estimator(&val) as f64)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_default_budget(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let w = match get(&a, "window") { Some(pyjson::Json::Num(n)) => *n as i64, _ => 0 };
|
||||
pyjson::Json::Num(broker::default_budget(w) as f64)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_default_memory_budget(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let w = match get(&a, "window") {
|
||||
Some(pyjson::Json::Num(n)) => *n as i64,
|
||||
Some(pyjson::Json::Null) | None => 0,
|
||||
_ => 0,
|
||||
};
|
||||
pyjson::Json::Num(broker::default_memory_budget(w) as f64)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_memory_disabled(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let v = match get(&a, "value") {
|
||||
Some(pyjson::Json::Str(s)) => Some(s.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
pyjson::Json::Bool(broker::memory_disabled(v))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_persona_lex(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let t = match get(&a, "text") { Some(pyjson::Json::Str(x)) => Some(x.as_str()), _ => None };
|
||||
pyjson::Json::Arr(broker::persona_lex(t).into_iter().map(pyjson::Json::Str).collect())
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_persona_overlap(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let wa: Vec<String> = match get(&a, "a") { Some(pyjson::Json::Arr(x)) => x.iter().filter_map(|v| match v { pyjson::Json::Str(s) => Some(s.clone()), _ => None }).collect(), _ => vec![] };
|
||||
let wb: Vec<String> = match get(&a, "b") { Some(pyjson::Json::Arr(x)) => x.iter().filter_map(|v| match v { pyjson::Json::Str(s) => Some(s.clone()), _ => None }).collect(), _ => vec![] };
|
||||
pyjson::Json::Num(broker::persona_overlap(&wa, &wb))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_persona_decide(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).and_then(|a| {
|
||||
let text = match get(&a, "text") { Some(pyjson::Json::Str(x)) => x.clone(), _ => String::new() };
|
||||
let rules: Vec<pyjson::Json> = match get(&a, "rules") { Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
let permissive = matches!(get(&a, "permissive"), Some(pyjson::Json::Bool(true)));
|
||||
let sems: Vec<f64> = match get(&a, "sem_scores") {
|
||||
Some(pyjson::Json::Arr(x)) => x.iter().filter_map(|v| match v { pyjson::Json::Num(n) => Some(*n), _ => Some(0.0) }).collect(),
|
||||
_ => vec![0.0; rules.len()],
|
||||
};
|
||||
Ok(broker::persona_decide(&text, &rules, permissive, &sems))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_drift_check(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let text = match get(&a, "text") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
let patterns: Vec<pyjson::Json> = match get(&a, "patterns") {
|
||||
Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
pyjson::Json::Arr(broker::drift_check(&text, &patterns)
|
||||
.into_iter().map(pyjson::Json::Str).collect())
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_identity_prompt(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
pyjson::Json::Str(broker::identity_prompt(
|
||||
get(&a, "pronouns").unwrap_or(&pyjson::Json::Obj(vec![])),
|
||||
get(&a, "self_awareness").unwrap_or(&pyjson::Json::Obj(vec![])),
|
||||
get(&a, "anchors").unwrap_or(&pyjson::Json::Obj(vec![])),
|
||||
get(&a, "embodiment").unwrap_or(&pyjson::Json::Arr(vec![]))))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_growth_near_dup(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let x = match get(&a, "a") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
let y = match get(&a, "b") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
pyjson::Json::Bool(broker::growth_near_dup(&x, &y))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_growth_profile(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).and_then(|a| {
|
||||
let state = get(&a, "state").cloned().unwrap_or(pyjson::Json::Obj(vec![]));
|
||||
let entry_chars = match get(&a, "entry_chars") { Some(pyjson::Json::Num(n)) => *n as usize, _ => 0 };
|
||||
let max_tokens = match get(&a, "max_tokens") { Some(pyjson::Json::Num(n)) => *n as usize, _ => 0 };
|
||||
let order: Vec<(String, i64)> = match get(&a, "order") {
|
||||
Some(pyjson::Json::Arr(x)) => x.iter().filter_map(|v| match v {
|
||||
pyjson::Json::Arr(pair) if pair.len() >= 2 => match (&pair[0], &pair[1]) {
|
||||
(pyjson::Json::Str(name), pyjson::Json::Num(rank)) => Some((name.clone(), *rank as i64)),
|
||||
_ => None },
|
||||
_ => None }).collect(),
|
||||
_ => vec![] };
|
||||
Ok(pyjson::Json::Arr(broker::growth_profile(&state, &order, entry_chars, max_tokens)))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_pins_recall(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let pins: Vec<pyjson::Json> = match get(&a, "pins") {
|
||||
Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
let term = match get(&a, "term") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
pyjson::Json::Arr(broker::pins_recall(&pins, &term, 4))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_tier_get(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let e = get(&a, "entry").cloned().unwrap_or(pyjson::Json::Obj(vec![]));
|
||||
broker::tier_get(&e,
|
||||
match get(&a, "hot_days") { Some(pyjson::Json::Num(n)) => *n, _ => 7.0 },
|
||||
match get(&a, "warm_days") { Some(pyjson::Json::Num(n)) => *n, _ => 30.0 },
|
||||
match get(&a, "now") { Some(pyjson::Json::Str(s)) => s.as_str(), _ => "" })
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_tier_promote(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let e = get(&a, "entry").cloned().unwrap_or(pyjson::Json::Obj(vec![]));
|
||||
broker::tier_promote(&e,
|
||||
match get(&a, "hot_days") { Some(pyjson::Json::Num(n)) => *n, _ => 7.0 },
|
||||
match get(&a, "warm_days") { Some(pyjson::Json::Num(n)) => *n, _ => 30.0 },
|
||||
match get(&a, "now") { Some(pyjson::Json::Str(s)) => s.as_str(), _ => "" })
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_tier_demote(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let e = get(&a, "entry").cloned().unwrap_or(pyjson::Json::Obj(vec![]));
|
||||
broker::tier_demote(&e,
|
||||
match get(&a, "hot_days") { Some(pyjson::Json::Num(n)) => *n, _ => 7.0 },
|
||||
match get(&a, "warm_days") { Some(pyjson::Json::Num(n)) => *n, _ => 30.0 },
|
||||
match get(&a, "now") { Some(pyjson::Json::Str(s)) => s.as_str(), _ => "" })
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_witness_extract_topic(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let text = match get(&a, "text") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
let sw: Vec<pyjson::Json> = match get(&a, "stopwords") { Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
match broker::witness_extract_topic(&text, &sw, 5) {
|
||||
pyjson::Json::Arr(v) => pyjson::Json::Arr(v),
|
||||
other => other,
|
||||
}
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_witness_signals(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let message = match get(&a, "message") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
let tables = get(&a, "tables").cloned().unwrap_or(pyjson::Json::Obj(vec![]));
|
||||
let tool_error = match get(&a, "tool_error") { Some(pyjson::Json::Str(s)) if !s.is_empty() => Some(s.as_str()), _ => None };
|
||||
let task_status = match get(&a, "task_status") { Some(pyjson::Json::Str(s)) if !s.is_empty() => Some(s.as_str()), _ => None };
|
||||
let recent: Vec<pyjson::Json> = match get(&a, "recent_topics") { Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
broker::witness_signals(&message, &tables, tool_error, task_status, &recent)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_safety_get_params(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let rows: Vec<pyjson::Json> = match get(&a, "rows") { Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
broker::safety_get_params(&rows)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_safety_update_pattern(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let rows: Vec<pyjson::Json> = match get(&a, "rows") { Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
let pat = match get(&a, "pattern") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
pyjson::Json::Arr(broker::safety_update_pattern(&rows, &pat))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_abstraction_extract_facts(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| {
|
||||
let text = match get(&a, "text") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
let sw: Vec<pyjson::Json> = match get(&a, "stopwords") { Some(pyjson::Json::Arr(x)) => x.clone(), _ => vec![] };
|
||||
let min_len = match get(&a, "min_len") { Some(pyjson::Json::Num(n)) => *n as usize, _ => 10 };
|
||||
broker::abstraction_extract_facts(&text, &sw, min_len)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_free_string(ptr: *mut c_char) {
|
||||
if !ptr.is_null() { unsafe { let _ = CString::from_raw(ptr); } }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! main.rs — CLI debug front for cognition_core.
|
||||
use cognition_core::{broker, pyjson};
|
||||
use std::io::Read;
|
||||
|
||||
fn get<'a>(v: &'a pyjson::Json, k: &str) -> Option<&'a pyjson::Json> {
|
||||
match v { pyjson::Json::Obj(p) => p.iter().find(|(x, _)| x == k).map(|(_, v)| v), _ => None }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut x = String::new();
|
||||
if std::io::stdin().read_to_string(&mut x).is_err() { return; }
|
||||
let a = pyjson::parse(&x).unwrap();
|
||||
let op = match get(&a, "op") { Some(pyjson::Json::Str(s)) => s.as_str(), _ => "" };
|
||||
let arg = get(&a, "arg").unwrap_or(&pyjson::Json::Null);
|
||||
let v = match op {
|
||||
"default_estimator" => {
|
||||
let t = match get(arg, "text") { Some(pyjson::Json::Str(s)) => s.clone(), _ => String::new() };
|
||||
pyjson::Json::Num(broker::default_estimator(&t) as f64)
|
||||
}
|
||||
_ => pyjson::Json::Str(format!("unsupported op: {}", op)),
|
||||
};
|
||||
println!("{}", pyjson::dumps(&v));
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
//! json_min.py — minimal JSON parse/serialize with Python-parity output.
|
||||
//!
|
||||
//! Exists because contract C1 demands byte-comparable state files with the
|
||||
//! Python implementation (`json.dumps` / `json.dump(indent=2)`):
|
||||
//! - compact separators: ", " between items, ": " between key/value
|
||||
//! (Python json.dumps DEFAULTS — no separators arg means the space-y form)
|
||||
//! - ensure_ascii: non-ASCII escaped as \uXXXX (with surrogate pairs)
|
||||
//! - floats: shortest round-trip repr (Rust `{}` == Python `repr` for f64
|
||||
//! in every value this module produces, including arithmetic artifacts
|
||||
//! like 0.30000000000000004)
|
||||
//! - indent=2 dump: newline+indent after every item separator, "{}" when
|
||||
//! empty, NO trailing newline (json.dump never writes one)
|
||||
//! Object key order is preserved (Vec of pairs) — matches Python 3.7+ dict
|
||||
//! insertion order, which the classification tie-break law depends on.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Preserved-order JSON value.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Json {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Num(f64),
|
||||
Str(String),
|
||||
Arr(Vec<Json>),
|
||||
Obj(Vec<(String, Json)>),
|
||||
}
|
||||
|
||||
impl Json {
|
||||
pub fn get(&self, key: &str) -> Option<&Json> {
|
||||
match self {
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── parsing ─────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn parse(input: &str) -> Result<Json, String> {
|
||||
let bytes = input.as_bytes();
|
||||
let mut p = Parser { b: bytes, i: 0 };
|
||||
p.skip_ws();
|
||||
let v = p.value()?;
|
||||
p.skip_ws();
|
||||
if p.i != bytes.len() {
|
||||
return Err(format!("trailing data at byte {}", p.i));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
b: &'a [u8],
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn skip_ws(&mut self) {
|
||||
while self.i < self.b.len() && matches!(self.b[self.i], b' ' | b'\t' | b'\n' | b'\r') {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.b.get(self.i).copied()
|
||||
}
|
||||
|
||||
fn expect(&mut self, c: u8) -> Result<(), String> {
|
||||
if self.peek() == Some(c) {
|
||||
self.i += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("expected '{}' at byte {}", c as char, self.i))
|
||||
}
|
||||
}
|
||||
|
||||
fn value(&mut self) -> Result<Json, String> {
|
||||
match self.peek() {
|
||||
Some(b'{') => self.object(),
|
||||
Some(b'[') => self.array(),
|
||||
Some(b'"') => Ok(Json::Str(self.string()?)),
|
||||
Some(b't') => self.literal("true", Json::Bool(true)),
|
||||
Some(b'f') => self.literal("false", Json::Bool(false)),
|
||||
Some(b'n') => self.literal("null", Json::Null),
|
||||
Some(c) if c == b'-' || c.is_ascii_digit() => self.number(),
|
||||
_ => Err(format!("unexpected byte at {}", self.i)),
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(&mut self, lit: &str, v: Json) -> Result<Json, String> {
|
||||
if self.b[self.i..].starts_with(lit.as_bytes()) {
|
||||
self.i += lit.len();
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(format!("bad literal at byte {}", self.i))
|
||||
}
|
||||
}
|
||||
|
||||
fn object(&mut self) -> Result<Json, String> {
|
||||
self.expect(b'{')?;
|
||||
let mut pairs = Vec::new();
|
||||
self.skip_ws();
|
||||
if self.peek() == Some(b'}') {
|
||||
self.i += 1;
|
||||
return Ok(Json::Obj(pairs));
|
||||
}
|
||||
loop {
|
||||
self.skip_ws();
|
||||
let key = self.string()?;
|
||||
self.skip_ws();
|
||||
self.expect(b':')?;
|
||||
self.skip_ws();
|
||||
let val = self.value()?;
|
||||
pairs.push((key, val));
|
||||
self.skip_ws();
|
||||
match self.peek() {
|
||||
Some(b',') => self.i += 1,
|
||||
Some(b'}') => {
|
||||
self.i += 1;
|
||||
return Ok(Json::Obj(pairs));
|
||||
}
|
||||
_ => return Err(format!("expected ',' or '}}' at byte {}", self.i)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn array(&mut self) -> Result<Json, String> {
|
||||
self.expect(b'[')?;
|
||||
let mut items = Vec::new();
|
||||
self.skip_ws();
|
||||
if self.peek() == Some(b']') {
|
||||
self.i += 1;
|
||||
return Ok(Json::Arr(items));
|
||||
}
|
||||
loop {
|
||||
self.skip_ws();
|
||||
items.push(self.value()?);
|
||||
self.skip_ws();
|
||||
match self.peek() {
|
||||
Some(b',') => self.i += 1,
|
||||
Some(b']') => {
|
||||
self.i += 1;
|
||||
return Ok(Json::Arr(items));
|
||||
}
|
||||
_ => return Err(format!("expected ',' or ']' at byte {}", self.i)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn string(&mut self) -> Result<String, String> {
|
||||
self.expect(b'"')?;
|
||||
let mut out = String::new();
|
||||
loop {
|
||||
match self.peek() {
|
||||
None => return Err("unterminated string".into()),
|
||||
Some(b'"') => {
|
||||
self.i += 1;
|
||||
return Ok(out);
|
||||
}
|
||||
Some(b'\\') => {
|
||||
self.i += 1;
|
||||
match self.peek() {
|
||||
Some(b'"') => out.push('"'),
|
||||
Some(b'\\') => out.push('\\'),
|
||||
Some(b'/') => out.push('/'),
|
||||
Some(b'b') => out.push('\u{0008}'),
|
||||
Some(b'f') => out.push('\u{000C}'),
|
||||
Some(b'n') => out.push('\n'),
|
||||
Some(b'r') => out.push('\r'),
|
||||
Some(b't') => out.push('\t'),
|
||||
Some(b'u') => {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
+ ((cp - 0xD800) << 10)
|
||||
+ (lo - 0xDC00);
|
||||
out.push(
|
||||
char::from_u32(c)
|
||||
.ok_or("bad surrogate pair")?,
|
||||
);
|
||||
} else {
|
||||
// lone high surrogate — Python keeps it as
|
||||
// an escaped lone surrogate; we degrade to
|
||||
// U+FFFD (not produced by this writer)
|
||||
out.push('\u{FFFD}');
|
||||
}
|
||||
} else {
|
||||
out.push('\u{FFFD}');
|
||||
}
|
||||
} else {
|
||||
out.push(char::from_u32(cp).unwrap_or('\u{FFFD}'));
|
||||
}
|
||||
continue; // hex4 already advanced i
|
||||
}
|
||||
_ => return Err(format!("bad escape at byte {}", self.i)),
|
||||
}
|
||||
self.i += 1;
|
||||
}
|
||||
Some(_) => {
|
||||
// consume one UTF-8 scalar
|
||||
let s = &self.b[self.i..];
|
||||
let ch_len = utf8_len(s[0]);
|
||||
let chunk = std::str::from_utf8(&s[..ch_len.min(s.len())])
|
||||
.map_err(|_| "bad utf8")?;
|
||||
out.push_str(chunk);
|
||||
self.i += ch_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hex4(&mut self) -> Result<u32, String> {
|
||||
if self.i + 4 > self.b.len() {
|
||||
return Err("short \\u escape".into());
|
||||
}
|
||||
let s = std::str::from_utf8(&self.b[self.i + 1..self.i + 5])
|
||||
.map_err(|_| "bad \\u")?;
|
||||
let v = u32::from_str_radix(s, 16).map_err(|_| "bad \\u digits")?;
|
||||
self.i += 5; // 'u' + 4 hex digits (caller consumed the backslash)
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn number(&mut self) -> Result<Json, String> {
|
||||
let start = self.i;
|
||||
if self.peek() == Some(b'-') {
|
||||
self.i += 1;
|
||||
}
|
||||
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
if self.peek() == Some(b'.') {
|
||||
self.i += 1;
|
||||
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
if matches!(self.peek(), Some(b'e') | Some(b'E')) {
|
||||
self.i += 1;
|
||||
if matches!(self.peek(), Some(b'+') | Some(b'-')) {
|
||||
self.i += 1;
|
||||
}
|
||||
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
let s = std::str::from_utf8(&self.b[start..self.i]).map_err(|_| "bad number")?;
|
||||
s.parse::<f64>()
|
||||
.map(Json::Num)
|
||||
.map_err(|_| format!("bad number '{}'", s))
|
||||
}
|
||||
}
|
||||
|
||||
fn utf8_len(b: u8) -> usize {
|
||||
if b < 0x80 {
|
||||
1
|
||||
} else if b >> 5 == 0b110 {
|
||||
2
|
||||
} else if b >> 4 == 0b1110 {
|
||||
3
|
||||
} else {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
// ── serialization (Python json.dumps / json.dump(indent=2) parity) ─────────
|
||||
|
||||
/// Python `json.dumps(value, sort_keys=True)` — compact separators, object
|
||||
/// keys recursively sorted. Python sorts str keys by code point; Rust's
|
||||
/// byte-wise UTF-8 `String` ordering is identical for valid UTF-8, so a
|
||||
/// plain `sort_by` on the key is faithful.
|
||||
pub fn dumps_sorted(v: &Json) -> String {
|
||||
let mut out = String::new();
|
||||
write_compact_sorted(&mut out, v);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_compact_sorted(out: &mut String, v: &Json) {
|
||||
match v {
|
||||
Json::Obj(pairs) => {
|
||||
let mut sorted: Vec<&(String, Json)> = pairs.iter().collect();
|
||||
sorted.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
out.push('{');
|
||||
for (i, (k, val)) in sorted.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_escaped(out, k);
|
||||
out.push_str(": ");
|
||||
write_compact_sorted(out, val);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
Json::Arr(items) => {
|
||||
out.push('[');
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_compact_sorted(out, item);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
other => write_compact(out, other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Python `json.dumps(value)` — compact separators `", "` / `": "`,
|
||||
/// ensure_ascii escaping.
|
||||
pub fn dumps(v: &Json) -> String {
|
||||
let mut out = String::new();
|
||||
write_compact(&mut out, v);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_compact(out: &mut String, v: &Json) {
|
||||
match v {
|
||||
Json::Null => out.push_str("null"),
|
||||
Json::Bool(true) => out.push_str("true"),
|
||||
Json::Bool(false) => out.push_str("false"),
|
||||
Json::Num(n) => out.push_str(&fmt_num(*n)),
|
||||
Json::Str(s) => write_escaped(out, s),
|
||||
Json::Arr(items) => {
|
||||
out.push('[');
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_compact(out, item);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Json::Obj(pairs) => {
|
||||
out.push('{');
|
||||
for (i, (k, val)) in pairs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_escaped(out, k);
|
||||
out.push_str(": ");
|
||||
write_compact(out, val);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Python `json.dump(value, indent=2)` — item separator becomes
|
||||
/// `",\n<indent>"`, key separator `": "`, empty container → `{}` / `[]`,
|
||||
/// NO trailing newline.
|
||||
pub fn dump_indent2(v: &Json) -> String {
|
||||
let mut out = String::new();
|
||||
write_indent(&mut out, v, 0);
|
||||
out
|
||||
}
|
||||
|
||||
fn write_indent(out: &mut String, v: &Json, depth: usize) {
|
||||
match v {
|
||||
Json::Arr(items) if !items.is_empty() => {
|
||||
out.push_str("[\n");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(",\n");
|
||||
}
|
||||
push_indent(out, depth + 1);
|
||||
write_indent(out, item, depth + 1);
|
||||
}
|
||||
out.push('\n');
|
||||
push_indent(out, depth);
|
||||
out.push(']');
|
||||
}
|
||||
Json::Arr(_) => out.push_str("[]"),
|
||||
Json::Obj(pairs) if !pairs.is_empty() => {
|
||||
out.push_str("{\n");
|
||||
for (i, (k, val)) in pairs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(",\n");
|
||||
}
|
||||
push_indent(out, depth + 1);
|
||||
write_escaped(out, k);
|
||||
out.push_str(": ");
|
||||
write_indent(out, val, depth + 1);
|
||||
}
|
||||
out.push('\n');
|
||||
push_indent(out, depth);
|
||||
out.push('}');
|
||||
}
|
||||
Json::Obj(_) => out.push_str("{}"),
|
||||
other => write_compact(out, other),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_indent(out: &mut String, depth: usize) {
|
||||
for _ in 0..depth {
|
||||
out.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
/// Integer-shaped f64 prints without a decimal point (matching how this
|
||||
/// module's writers emit counts); all other values use shortest round-trip.
|
||||
pub fn fmt_num(n: f64) -> String {
|
||||
if n.is_finite() && n.fract() == 0.0 && n.abs() < 9.007_199_254_740_992e15 {
|
||||
return format!("{}", n as i64);
|
||||
}
|
||||
format!("{}", n)
|
||||
}
|
||||
|
||||
/// Python `json.dumps` string escaping with ensure_ascii=True.
|
||||
pub fn write_escaped(out: &mut String, s: &str) {
|
||||
out.push('"');
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\u{0008}' => out.push_str("\\b"),
|
||||
'\u{000C}' => out.push_str("\\f"),
|
||||
c if (c as u32) < 0x20 => {
|
||||
let _ = write!(out, "\\u{:04x}", c as u32);
|
||||
}
|
||||
c if (c as u32) <= 0x7E => out.push(c),
|
||||
// ensure_ascii: escape everything non-ASCII, surrogate pairs
|
||||
// for astral planes (Python behaviour)
|
||||
c => {
|
||||
let cp = c as u32;
|
||||
if cp > 0xFFFF {
|
||||
let v = cp - 0x10000;
|
||||
let hi = 0xD800 + (v >> 10);
|
||||
let lo = 0xDC00 + (v & 0x3FF);
|
||||
let _ = write!(out, "\\u{:04x}\\u{:04x}", hi, lo);
|
||||
} else {
|
||||
let _ = write!(out, "\\u{:04x}", cp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use cognition_core::broker::parse_strict_aware;
|
||||
#[test]
|
||||
fn parses_aware() {
|
||||
let v = parse_strict_aware("2026-10-05T12:00:00+00:00");
|
||||
assert!(v.is_some(), "aware must parse");
|
||||
let n = parse_strict_aware("2026-10-05T12:00:00");
|
||||
assert!(n.is_none(), "naive must be refused");
|
||||
}
|
||||
#[test]
|
||||
fn tier_recency_works() {
|
||||
let e = cognition_core::pyjson::parse(
|
||||
r#"{"wash_count": 2, "last_accessed": "2026-10-05T06:00:00+00:00"}"#).unwrap();
|
||||
let t = cognition_core::broker::tier_get(&e, 7.0, 30.0, "2026-10-05T12:00:00+00:00");
|
||||
assert!(format!("{}", cognition_core::pyjson::dumps(&t)).contains("hot"),
|
||||
"expected hot, got {}", cognition_core::pyjson::dumps(&t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_components() {
|
||||
let s = "2026-10-05T12:00:00+00:00";
|
||||
let b = s.as_bytes();
|
||||
assert_eq!(b.len(), 25);
|
||||
// simulate the loop: i=19 is '+' → exits immediately → rest="+00:00"
|
||||
let mut i = 19;
|
||||
while i < b.len() && b[i] != b'Z' && b[i] != b'+' && b[i] != b'-' { i += 1; }
|
||||
assert_eq!(i, 19, "offset starts immediately after seconds");
|
||||
let rest = &s[i..];
|
||||
assert_eq!(rest, "+00:00");
|
||||
let v = parse_strict_aware(s);
|
||||
assert!(v.is_some());
|
||||
let val = v.unwrap();
|
||||
// 2026-10-05T12:00:00Z epoch ≈ 1791201600 (verify ±1s tolerance)
|
||||
assert!((val - 1791201600.0).abs() < 2.0, "epoch mismatch: {}", val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tier_decompose() {
|
||||
use cognition_core::broker::parse_strict_aware;
|
||||
let last = "2026-10-05T06:00:00+00:00";
|
||||
let now = "2026-10-05T12:00:00+00:00";
|
||||
let (l, n) = (parse_strict_aware(last), parse_strict_aware(now));
|
||||
assert!(l.is_some() && n.is_some(), "both must parse");
|
||||
let hours = (n.unwrap() - l.unwrap()) / 3600.0;
|
||||
assert_eq!(hours, 6.0, "6h apart");
|
||||
// so tier_get's recency branch must fire... check it does by feeding
|
||||
// what the parse sees via a wash-hot case:
|
||||
let e = cognition_core::pyjson::parse(r#"{"wash_count": 25}"#).unwrap();
|
||||
let t = cognition_core::broker::tier_get(&e, 7.0, 30.0, now);
|
||||
let s = cognition_core::pyjson::dumps(&t);
|
||||
assert!(s.contains("hot"), "wash>=20 must be hot, got {}", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tier_probe_row_shape() {
|
||||
// Does the JSON fixture parse into the expected shape? get(entry,...)
|
||||
// reads "last_accessed" — pyjson::Json::Obj pairs. Verify:
|
||||
let e = cognition_core::pyjson::parse(
|
||||
r#"{"wash_count": 2, "last_accessed": "2026-10-05T06:00:00+00:00"}"#)
|
||||
.unwrap();
|
||||
let la = cognition_core::broker::tier_get(&e, 7.0, 30.0,
|
||||
"2026-10-05T12:00:00+00:00");
|
||||
// instrument via a different route: promote → falls to the same get_tier
|
||||
let s = cognition_core::pyjson::dumps(&la);
|
||||
eprintln!("tier_get gave: {}", s);
|
||||
}
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
@@ -172,8 +172,8 @@ impl<'a> Parser<'a> {
|
||||
let cp = self.hex4()?;
|
||||
// surrogate pair handling (Python accepts these)
|
||||
if (0xD800..0xDC00).contains(&cp) {
|
||||
if self.b[self.i + 1..].starts_with(b"\\u") {
|
||||
self.i += 2;
|
||||
if self.b[self.i..].starts_with(b"\\u") {
|
||||
self.i += 1; // now at 'u' — hex4 reads [i+1..i+5]
|
||||
let lo = self.hex4()?;
|
||||
if (0xDC00..0xE000).contains(&lo) {
|
||||
let c = 0x10000
|
||||
|
||||
+384
-348
File diff suppressed because it is too large.
Load diff
+6946
-6922
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+2220
-504
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,41 @@
|
||||
"""Exercise the Gitea auto-label script, not just its YAML shape."""
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
|
||||
def test_duplicate_priority_reconciled_by_numeric_id(tmp_path):
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
pytest.skip("node not installed")
|
||||
workflow = yaml.safe_load((Path(__file__).parents[1] / ".gitea/workflows/auto-label.yml").read_text())
|
||||
script = workflow["jobs"]["auto-label"]["steps"][0]["with"]["script"]
|
||||
fixture = """
|
||||
const labels = [{name:'P0 — critical',id:23},{name:'P2 — medium',id:25}];
|
||||
const context = {payload:{action:'labeled',issue:{state:'open',number:2467,
|
||||
title:'Scaffolding completion',body:'kind: feature\\nspine protocol'}},
|
||||
repo:{owner:'conductor',repo:'rune'}};
|
||||
const github = {rest:{issues:{
|
||||
listLabelsOnIssue: async () => ({data:labels.map(x => ({...x}))}),
|
||||
addLabels: async ({labels:names}) => {
|
||||
for (const name of names) labels.push({name,id:100+labels.length});
|
||||
}
|
||||
}},request: async (route,{id}) => {
|
||||
if (!route.includes('/labels/{id}') || typeof id !== 'number')
|
||||
throw new Error('Gitea requires numeric label ID');
|
||||
const index=labels.findIndex(x => x.id === id);
|
||||
if (index < 0) throw new Error('missing label ID '+id);
|
||||
labels.splice(index,1);
|
||||
}};
|
||||
"""
|
||||
js = (fixture + "\n(async () => {\n" + script + "\n})().then(() => {\n"
|
||||
+ "const priorities=labels.filter(x => /^P[0-3] — /.test(x.name));\n"
|
||||
+ "if (priorities.length !== 1 || priorities[0].id !== 23) process.exitCode=1;\n"
|
||||
+ "}).catch(e => {console.error(e);process.exitCode=1;});\n")
|
||||
path = tmp_path / "priority.js"
|
||||
path.write_text(js)
|
||||
run = subprocess.run([node, str(path)], capture_output=True, text=True, timeout=10)
|
||||
assert run.returncode == 0, run.stdout + run.stderr
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -585,3 +586,211 @@ def test_15u_discover_active_contract_migrated_to_interface(tmp_path):
|
||||
# capabilities() advertises the new op
|
||||
assert "discover_active_contract" in \
|
||||
iface.capabilities()["state"]["operations"]
|
||||
|
||||
|
||||
# ── T5.15 completion wave: stage_guards wired into transition ───────────────
|
||||
|
||||
|
||||
def _guard_contract(iface, tmp_path):
|
||||
"""A contract whose obligations declare a PATH guard and a failing
|
||||
command guard — proving both BLOCK shapes."""
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(
|
||||
doc, id="contract:guarded-1", kind="contract",
|
||||
identities={
|
||||
"objective": "guarded stage",
|
||||
"verification_obligations": [
|
||||
"guard:" + json.dumps({
|
||||
"name": "artifact present", "kind": "path_exists",
|
||||
"path": str(tmp_path / "artifact.txt"),
|
||||
}),
|
||||
],
|
||||
},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
iface.scaffold.commit(doc, what="seed guarded contract")
|
||||
doc = iface.scaffold.load()
|
||||
# move through the legal path so guards face verifying from building
|
||||
sc.Scaffold.transition_contract(iface.scaffold, doc, "guarded-1", "approved",
|
||||
evidence_ref="t")
|
||||
sc.Scaffold.transition_contract(iface.scaffold, doc, "guarded-1", "building",
|
||||
evidence_ref="t")
|
||||
iface.scaffold.commit(doc, what="advance to building")
|
||||
return "guarded-1"
|
||||
|
||||
|
||||
def test_15f_transition_to_verifying_blocked_when_guard_fails(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _guard_contract(iface, tmp_path)
|
||||
env = iface.transition(cid, "verifying")
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.VERIFICATION_FAILED
|
||||
assert env["state"]["guard"]["verdict"] == "BLOCKED"
|
||||
# phase UNCHANGED — the blocked stage does not move
|
||||
doc = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc, cid)
|
||||
assert c["phase"] == "building"
|
||||
|
||||
|
||||
def test_15f_transition_passes_when_guard_satisfied(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _guard_contract(iface, tmp_path)
|
||||
(tmp_path / "artifact.txt").write_text("real artifact")
|
||||
env = iface.transition(cid, "verifying")
|
||||
assert env["success"], env["errors"]
|
||||
doc = iface.scaffold.load()
|
||||
assert iface.scaffold.contract(doc, cid)["phase"] == "verifying"
|
||||
|
||||
|
||||
def test_15f_inline_guard_checks_override_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _guard_contract(iface, tmp_path) # has the path guard, NOT satisfied
|
||||
env = iface.transition(
|
||||
cid, "verifying",
|
||||
guard_checks=[{"name": "inline truth", "kind": "command",
|
||||
"argv": [sys.executable, "-c", "print('ok')"]}],
|
||||
)
|
||||
assert env["success"], env["errors"]
|
||||
assert iface.scaffold.contract(iface.scaffold.load(), cid)["phase"] == "verifying"
|
||||
|
||||
|
||||
def test_15f_transition_to_building_runs_no_guards(tmp_path):
|
||||
"""Completion-facing edges ONLY (verifying/closed): building→verifying
|
||||
is guarded, but draft→approved passes freely even with obligations."""
|
||||
iface = _iface(tmp_path)
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(doc, id="contract:guarded-2", kind="contract",
|
||||
identities={"objective": "2"}, evidence_refs=["e"])
|
||||
iface.scaffold.commit(doc, what="seed guarded-2")
|
||||
cid = "guarded-2"
|
||||
doc = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc, cid)
|
||||
c["identities"]["verification_obligations"] = [
|
||||
"guard:" + json.dumps({"name": "never", "kind": "path_exists",
|
||||
"path": "/nonexistent/for-sure"})
|
||||
]
|
||||
iface.scaffold.commit(doc, what="attach guard")
|
||||
env = iface.transition(cid, "approved") # NOT a completion edge
|
||||
assert env["success"]
|
||||
|
||||
|
||||
# ── T5.15 slice 3: programme_continuation wired into close() ────────────────
|
||||
|
||||
|
||||
def _seed_programme(iface, programme_id, stage_contract_ids, tmp_path):
|
||||
"""Persist a real Programme bound to the interface's scaffold store."""
|
||||
from rune import programme_continuation as pc
|
||||
|
||||
base = Path(iface.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
|
||||
prog = pc.Programme(programme_id=programme_id, objective="wave test")
|
||||
for i, cid in enumerate(stage_contract_ids):
|
||||
prog.stages.append(pc.ProgrammeStage(
|
||||
stage_id=cid, objective=f"stage {i}", depends_on=[]))
|
||||
prog.save(base)
|
||||
return prog, base
|
||||
|
||||
|
||||
def test_close_marks_programme_stage_complete_and_surfaces_continuation(tmp_path):
|
||||
"""Closing a contract that is a stage of a persisted Programme marks
|
||||
that stage COMPLETE and the envelope carries the continuation canary —
|
||||
the programme's next eligible stage is surfaced, not guessed."""
|
||||
from rune import programme_continuation as pc
|
||||
|
||||
iface = _iface(tmp_path)
|
||||
env1 = _submit(iface, objective="wave stage one")
|
||||
cid1 = env1["ids"]["contract_id"]
|
||||
env2 = _submit(iface, objective="wave stage two")
|
||||
cid2 = env2["ids"]["contract_id"]
|
||||
# drive both through to verifying so close is legal
|
||||
for cid in (cid1, cid2):
|
||||
iface.transition(cid, "approved")
|
||||
iface.transition(cid, "building")
|
||||
iface.transition(cid, "verifying")
|
||||
prog, base = _seed_programme(iface, "wave-x", [cid1, cid2], tmp_path)
|
||||
env = iface.close(cid1, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
|
||||
assert env["success"], env["errors"]
|
||||
cont = env["state"].get("programme_continuation")
|
||||
assert cont, "close envelope must carry the programme continuation"
|
||||
assert cont["programme_id"] == "wave-x"
|
||||
assert cont["stage_marked_complete"] == cid1
|
||||
reloaded = pc.Programme.load(base, "wave-x")
|
||||
stage1 = next(s for s in reloaded.stages if s.stage_id == cid1)
|
||||
assert stage1.state == pc.COMPLETE
|
||||
assert cont["canary"]["verdict"] == "PROGRAMME_OPEN"
|
||||
assert cont["canary"]["next_action"] == ("start", cid2)
|
||||
|
||||
|
||||
def test_close_without_programme_has_no_continuation_block(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
iface.transition(cid, "approved")
|
||||
iface.transition(cid, "building")
|
||||
iface.transition(cid, "verifying")
|
||||
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
|
||||
assert env["success"]
|
||||
assert "programme_continuation" not in env["state"]
|
||||
|
||||
|
||||
def test_close_programme_completion_verdict_surfaces(tmp_path):
|
||||
"""Closing the LAST stage yields PROGRAMME_COMPLETE — the canary says
|
||||
stop, the runner never guesses."""
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
iface.transition(cid, "approved")
|
||||
iface.transition(cid, "building")
|
||||
iface.transition(cid, "verifying")
|
||||
prog, base = _seed_programme(iface, "solo", [cid], tmp_path)
|
||||
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
|
||||
cont = env["state"]["programme_continuation"]
|
||||
assert cont["canary"]["verdict"] == "PROGRAMME_COMPLETE"
|
||||
|
||||
|
||||
def test_two_submits_same_second_get_distinct_contract_ids(tmp_path):
|
||||
"""Second-granularity timestamps collide when two needs are filed in
|
||||
the same second (real builder-pair scenario) — ids must be distinct."""
|
||||
iface = _iface(tmp_path)
|
||||
e1 = _submit(iface, objective="wave stage one")
|
||||
e2 = _submit(iface, objective="wave stage two")
|
||||
assert e1["success"] and e2["success"], (e1["errors"], e2["errors"])
|
||||
assert e1["ids"]["contract_id"] != e2["ids"]["contract_id"]
|
||||
|
||||
|
||||
# ── T5.15 slice 4: iteration impact-delta recorded at close ─────────────────
|
||||
|
||||
|
||||
def test_close_records_impact_delta_when_scope_changed(tmp_path):
|
||||
"""A closed contract with scope_files records a predicted-vs-observed
|
||||
impact delta into the project store's impact_deltas.jsonl (rule 19H:
|
||||
deltas compound into better future prediction)."""
|
||||
from rune import iteration as it
|
||||
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
for ph in ("approved", "building", "verifying"):
|
||||
iface.transition(cid, ph)
|
||||
iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}],
|
||||
scope_files=["src/export_module.py"])
|
||||
delta_file = tmp_path / ".rune" / "scaffold" / "impact_deltas.jsonl"
|
||||
assert delta_file.exists(), "close must persist the impact delta"
|
||||
import json as _json
|
||||
deltas = [_json.loads(l) for l in delta_file.read_text().splitlines() if l.strip()]
|
||||
assert any(d.get("contract_id") == cid for d in deltas)
|
||||
d = next(d for d in deltas if d.get("contract_id") == cid)
|
||||
assert d.get("scope_files") == ["src/export_module.py"]
|
||||
assert "recorded_at" in d
|
||||
|
||||
|
||||
def test_close_without_scope_records_no_delta(tmp_path):
|
||||
"""A governance-only close (no scope files) records nothing — the
|
||||
delta ledger only carries material changes."""
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
for ph in ("approved", "building", "verifying"):
|
||||
iface.transition(cid, ph)
|
||||
iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
|
||||
delta_file = tmp_path / ".rune" / "scaffold" / "impact_deltas.jsonl"
|
||||
assert not delta_file.exists()
|
||||
@@ -0,0 +1,84 @@
|
||||
"""T5.15 slice 5: the forge projection wired into the operator failure path.
|
||||
|
||||
The projection (testing_mode.project_failure_to_forge) was TEST_PROVEN with
|
||||
zero runtime callers. This wire invokes it from the operator's recorded-
|
||||
failure sink using the ALREADY-WIRED forge client (create_forge_client —
|
||||
NullAdapter when unconfigured → the projection is dormant by construction,
|
||||
never an error source).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import rune_operator as ro # noqa: E402
|
||||
from rune import testing_mode as tm # noqa: E402
|
||||
|
||||
|
||||
class RecordingAdapter:
|
||||
"""Records create_issue calls — proves the projection fired."""
|
||||
|
||||
def __init__(self):
|
||||
self.calls: list[tuple[str, str]] = []
|
||||
|
||||
def create_issue(self, title: str, body: str) -> dict:
|
||||
self.calls.append((title, body))
|
||||
return {"created": True, "number": len(self.calls)}
|
||||
|
||||
|
||||
def test_recorded_failure_projects_to_forge(monkeypatch):
|
||||
adapter = RecordingAdapter()
|
||||
monkeypatch.setattr(ro, "_forge_client_for_projection", lambda: adapter)
|
||||
result = ro._project_failure_to_forge(
|
||||
behaviour_id="behaviour-test-1",
|
||||
observation={"expected": "tests pass", "observed": "2 failed",
|
||||
"observation_id": "obs-1", "reproduction": "pytest -k x"},
|
||||
repo="conductor/rune",
|
||||
)
|
||||
# the projection returns the adapter's own dict (create_issue result) —
|
||||
# the wire is proven by the adapter having been CALLED
|
||||
assert len(adapter.calls) == 1
|
||||
assert result.get("created") is True
|
||||
title, _body = adapter.calls[0]
|
||||
assert "behaviour-test-1" in title
|
||||
|
||||
|
||||
def test_null_adapter_projection_is_dormant_not_error(monkeypatch):
|
||||
"""Unconfigured forge → the projection returns a dormant marker; the
|
||||
failure path is untouched."""
|
||||
monkeypatch.setattr(ro, "_forge_client_for_projection", lambda: _NullAdapterProbe())
|
||||
result = ro._project_failure_to_forge(
|
||||
behaviour_id="b-2",
|
||||
observation={"expected": "x", "observed": "y"},
|
||||
repo="r",
|
||||
)
|
||||
assert result.get("created") is False
|
||||
assert result.get("reason") == "no forge configured"
|
||||
|
||||
|
||||
def test_forge_unavailable_never_breaks_the_failure_path(monkeypatch):
|
||||
from rune import forge as _forge
|
||||
|
||||
def boom():
|
||||
raise _forge.ForgeUnavailable("forge down")
|
||||
monkeypatch.setattr(ro, "_forge_client_for_projection", lambda: boom())
|
||||
result = ro._project_failure_to_forge(
|
||||
behaviour_id="b-3",
|
||||
observation={"expected": "x", "observed": "y"},
|
||||
repo="r",
|
||||
)
|
||||
assert result["projected"] is False
|
||||
assert "forge down" in result.get("error", "")
|
||||
|
||||
|
||||
class _NullAdapterProbe:
|
||||
"""Mimics forge_client.NullAdapter's shape for the dormant test."""
|
||||
|
||||
def create_issue(self, title: str, body: str) -> dict:
|
||||
return {"created": False, "reason": "no forge configured"}
|
||||
@@ -641,3 +641,186 @@ def test_mr_loader_chunk_sql_gather_through_seam(tmp_path, monkeypatch):
|
||||
assert isinstance(out, list)
|
||||
assert all("rrf" in r for r in out)
|
||||
db.close()
|
||||
|
||||
|
||||
# ── cc_loader / cognition_core broker kernels (R2 W3.1) ─────────────────────
|
||||
|
||||
|
||||
def test_cc_loader_estimator_and_budgets_through_cdylib():
|
||||
from rune import cc_loader
|
||||
|
||||
if not cc_loader.available():
|
||||
pytest.skip("cognition_core crate not built")
|
||||
assert cc_loader.default_estimator("hello world") == 2
|
||||
assert cc_loader.default_estimator("héllo wörld 🎸 emoji") == 4 # chars, not utf16
|
||||
assert cc_loader.default_budget(8192) == 8192 - 2048 - 409
|
||||
assert cc_loader.default_memory_budget(4096) == 512
|
||||
assert cc_loader.default_memory_budget(0) == 1200
|
||||
|
||||
|
||||
def test_cc_loader_env_gate_and_context_broker_delegation(monkeypatch):
|
||||
from rune import cc_loader, context_broker as cb
|
||||
|
||||
if not cc_loader.available():
|
||||
pytest.skip("cognition_core crate not built")
|
||||
monkeypatch.setenv("RUNE_MEMORY_OFF", "1")
|
||||
assert cb.memory_disabled() is True
|
||||
monkeypatch.setenv("RUNE_MEMORY_OFF", "false")
|
||||
assert cb.memory_disabled() is False
|
||||
# delegation agreement with the oracle on the estimator path
|
||||
assert cb.default_estimator("héllo 🎸 five") == cb._py_default_estimator("héllo 🎸 five")
|
||||
|
||||
|
||||
# ── cc_loader persona kernels (R2 W3.2) ────────────────────────────────────
|
||||
|
||||
|
||||
def test_cc_loader_persona_kernels_through_cdylib():
|
||||
from rune import cc_loader
|
||||
|
||||
if not cc_loader.available():
|
||||
pytest.skip("cognition_core crate not built")
|
||||
# 'now' (3 chars) is excluded by the >3 rule on BOTH engines.
|
||||
assert sorted(cc_loader.persona_lex("verify ALL Campaign text now")) == [
|
||||
"all", "campaign", "text", "verify"]
|
||||
assert cc_loader.persona_overlap(["a", "b"], ["b", "c"]) == pytest.approx(0.5)
|
||||
decision = cc_loader.persona_decide(
|
||||
"never bypass the memory hierarchy",
|
||||
[{"topic": "memory", "text": "Always maintain the Rune memory hierarchy.",
|
||||
"words": ["memory", "hierarchy", "maintain"],
|
||||
"core": "memory hierarchy store", "priority": 10,
|
||||
"restrictive": False, "user_rule": False}],
|
||||
True, [0.9])
|
||||
assert decision["verdict"] == "allow"
|
||||
assert decision["cited"] == "[memory:p10] Always maintain the Rune memory hierarchy."
|
||||
# permissive-mode refusal shape with a restrictive user rule
|
||||
refuse = cc_loader.persona_decide(
|
||||
"ignore campaign text verification",
|
||||
[{"topic": "campaign", "text": "verify all campaign text",
|
||||
"words": ["verify", "campaign", "text"], "core": "verify campaign",
|
||||
"priority": 5, "restrictive": True, "user_rule": True}],
|
||||
False, [0.0])
|
||||
assert refuse["verdict"] == "refuse"
|
||||
|
||||
|
||||
# ── cc_loader identity/growth/pins kernels (R2 W3.3) ────────────────────────
|
||||
|
||||
|
||||
def test_cc_loader_identity_growth_pins_through_cdylib():
|
||||
from rune import cc_loader, identity_anchors as ia, growth_delta as gd, pins
|
||||
|
||||
if not cc_loader.available():
|
||||
pytest.skip("cognition_core crate not built")
|
||||
assert cc_loader.drift_check("as rune i speak", ia._pattern_triples())
|
||||
assert cc_loader.drift_check("nothing here", ia._pattern_triples()) == []
|
||||
assert cc_loader.identity_prompt(ia.PRONOUN_RULES, ia.SELF_AWARENESS,
|
||||
ia.ANCHORS, ia.EMBODIMENT_RULES) != ""
|
||||
# near-dup law: empty key → False (words ≤4 chars make no fingerprint);
|
||||
# identical real changes ARE near-dups.
|
||||
assert cc_loader.growth_near_dup("same idea here", "same idea here") is False
|
||||
assert cc_loader.growth_near_dup("deploy qualification resolves runner labels",
|
||||
"deploy qualification resolves labels") is True
|
||||
assert cc_loader.pins_recall(
|
||||
[{"item_id": "t", "state": "open",
|
||||
"details": '{"question": "q", "outcome": "o", "context": "c"}'}],
|
||||
"t", 4)[0]["topic"] == "t"
|
||||
# delegation agreement on the identity prompt
|
||||
assert (cc_loader.identity_prompt(ia.PRONOUN_RULES, ia.SELF_AWARENESS,
|
||||
ia.ANCHORS, ia.EMBODIMENT_RULES)
|
||||
== ia._py_get_identity_prompt())
|
||||
|
||||
|
||||
def test_growth_delta_reflect_apply_profile_oracles(tmp_path, monkeypatch):
|
||||
"""reflect/growth_profile oracle lines — deltas file + llm stubbed."""
|
||||
import json
|
||||
from rune import growth_delta as gd
|
||||
|
||||
monkeypatch.setenv("RUNE_COGNITION_CORE", "python")
|
||||
monkeypatch.setattr(gd, "DELTAS_FILE", tmp_path / "deltas.json")
|
||||
monkeypatch.setattr(gd, "DELTAS_FILE", str(tmp_path / "deltas.json"))
|
||||
(tmp_path / "deltas.json").write_text(json.dumps(
|
||||
{"applied": [], "pending": [], "last_read_back": None}))
|
||||
# the [:3] per-pass cap is a REAL gate — the persona delta rides in the
|
||||
# first three entries so the fixture exercises it.
|
||||
monkeypatch.setattr(gd, "_llm", lambda material: json.dumps({"deltas": [
|
||||
{"change": "relay in plain english always for delivery",
|
||||
"confidence": 0.9, "target": "delivery", "evidence": "e1"},
|
||||
{"change": "personal guidance for the persona topic",
|
||||
"confidence": 0.8, "target": "persona", "evidence": "e2"},
|
||||
{"change": "tiny", "confidence": 0.9, "target": "delivery"},
|
||||
]}))
|
||||
out = gd.reflect("material", dry_run=True)
|
||||
assert len(out["would_apply"]) == 2 and out["dry_run"] is True
|
||||
out2 = gd.reflect("material", dry_run=False)
|
||||
assert len(out2["applied"]) == 2
|
||||
# profile from the applied deltas (file was written by apply)
|
||||
prof = gd._py_growth_profile(100)
|
||||
assert prof and prof[0]["target"] == "delivery"
|
||||
|
||||
|
||||
def test_identity_anchors_check_drift_oracle_paths():
|
||||
from rune import identity_anchors as ia
|
||||
|
||||
# every prefix class via the full text
|
||||
text = ("i am rune. you are nick. i believe in my opinion. "
|
||||
"you're absolutely right! my apologies, hope this helps.")
|
||||
out = ia._py_check_drift(text)
|
||||
classes = {line.split(":")[0] for line in out}
|
||||
assert {"DRIFT", "WEAKNESS", "SYCOPHANCY"} <= classes
|
||||
assert ia._py_check_drift("nothing to see") == []
|
||||
|
||||
|
||||
def test_pins_project_and_unpin_oracles(tmp_path, monkeypatch):
|
||||
import json
|
||||
from rune import pins
|
||||
|
||||
recs = [{"kind": "pin", "item_id": "topic-a", "state": "open",
|
||||
"details": json.dumps({"question": "q", "outcome": "o",
|
||||
"context": "ctx here"})},
|
||||
{"kind": "pin", "item_id": "topic-b", "state": "resolved",
|
||||
"details": json.dumps({"context": "done"})}]
|
||||
monkeypatch.setattr(pins, "_load", lambda: recs)
|
||||
# _project covers the blob-parse + KeyError-safe defaults
|
||||
proj = pins._project(recs[0])
|
||||
assert proj == {"topic": "topic-a", "question": "q", "outcome": "o",
|
||||
"context": "ctx here", "status": "open", "reason": ""}
|
||||
# unpin through the fixture store
|
||||
monkeypatch.setattr(pins, "_save", lambda records: None)
|
||||
result = pins.unpin("topic-a", reason="done")
|
||||
assert result.get("unpinned") is True
|
||||
|
||||
|
||||
|
||||
def test_growth_delta_error_paths(tmp_path, monkeypatch):
|
||||
"""The loader-probe error branches + reflect malformed paths."""
|
||||
import json
|
||||
from rune import growth_delta as gd, cc_loader
|
||||
|
||||
# the loader error branches run when the import fails
|
||||
# the REAL loader-probe branches: make the import fail + make
|
||||
# available() raise, so the probe's except branches execute
|
||||
monkeypatch.setenv("RUNE_COGNITION_CORE", "python")
|
||||
assert gd._cc_core() is None # the pinned-fallback branch (158-159)
|
||||
monkeypatch.setenv("RUNE_COGNITION_CORE", "auto")
|
||||
# import-failure branch: `from rune import cc_loader` resolves via the
|
||||
# package attribute first — set it to a marker whose import raises.
|
||||
import rune as _rune_pkg
|
||||
monkeypatch.setattr(_rune_pkg, "cc_loader", None, raising=False)
|
||||
assert gd._cc_core() is None # the ImportError probe branch (160-163)
|
||||
|
||||
assert gd._near_dup({"change": "deploy qualification resolves labels"},
|
||||
{"change": "deploy qualification resolves runner labels"}) is True
|
||||
# profile: empty state
|
||||
monkeypatch.setattr(gd, "_load_deltas", lambda: {"profile": []})
|
||||
assert gd._py_growth_profile(100) == []
|
||||
# profile: token budget exhaustion
|
||||
deltas = [{"change": f"a longer change number {i} here", "target": "delivery"}
|
||||
for i in range(3)]
|
||||
monkeypatch.setattr(gd, "_load_deltas",
|
||||
lambda: {"profile": deltas})
|
||||
out = gd._py_growth_profile(8)
|
||||
assert len(out) >= 1 and len(out) < 3 # budget cut, never exceeded
|
||||
# reflect: malformed llm json -> no crash (0 deltas path)
|
||||
monkeypatch.setattr(gd, "_llm", lambda m: "not json")
|
||||
out = gd.reflect("material", dry_run=True)
|
||||
assert out["extracted"] == []
|
||||
|
||||
@@ -17,6 +17,31 @@ sys.path.insert(0, str(ROOT / "src"))
|
||||
from rune import spine
|
||||
|
||||
|
||||
def test_contract_discovery_preserves_unqualified_surface_without_inventing_module(monkeypatch):
|
||||
monkeypatch.setattr(spine, "_contract_discoveries", lambda: [
|
||||
("example", [("src/rune/audit_ledger.py", "construction-surface", "candidate"),
|
||||
("rust-crate:cognition_core", "rust-crate", "observed")])])
|
||||
doc = spine.new_doc()
|
||||
spine._apply_contract_discoveries(doc)
|
||||
found = {e["id"]: e for e in doc["entities"]}
|
||||
assert found["construction-surface:src/rune/audit_ledger.py"]["kind"] == "construction-surface"
|
||||
assert found["construction-surface:src/rune/audit_ledger.py"]["maturity"] == "DECLARED"
|
||||
assert found["rust-crate:cognition_core"]["maturity"] == "PRESENT"
|
||||
|
||||
|
||||
def test_legacy_unqualified_discoveries_are_normalized_without_losing_evidence():
|
||||
doc = spine.new_doc()
|
||||
spine.add_entity(doc, id="src/rune/audit_ledger.py", kind="module",
|
||||
maturity="PRESENT", evidence_refs=["contract-discovery:example"])
|
||||
spine.add_relationship(doc, type="depends_on", from_id="src/rune/audit_ledger.py",
|
||||
to_id="module:other", evidence_refs=["test"])
|
||||
spine._normalize_discovery_ids(doc)
|
||||
assert doc["entities"][0]["id"] == "construction-surface:src/rune/audit_ledger.py"
|
||||
assert doc["entities"][0]["maturity"] == "DECLARED"
|
||||
assert doc["entities"][0]["evidence_refs"] == ["contract-discovery:example"]
|
||||
assert doc["relationships"][0]["from"] == "construction-surface:src/rune/audit_ledger.py"
|
||||
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""spine_language — the #2465 doc-level transforms (T5.15 keep-green:
|
||||
the new-module henxel demands these ship WITH tests; they were carved
|
||||
from spine.py whose battery covered them, but the carved module is the
|
||||
unit under test now)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import spine_language as sl # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def doc():
|
||||
return sl.new_doc()
|
||||
|
||||
|
||||
def test_classification_sides_reads_the_measured_map():
|
||||
sides = sl._classification_sides()
|
||||
assert sides, "classification.json must be readable in-repo"
|
||||
assert sides.get("spine") == "RUST"
|
||||
assert sides.get("spine_language") == "RUST"
|
||||
|
||||
|
||||
def test_classification_sides_degrades_to_empty_when_unreadable(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sl, "ROOT", tmp_path)
|
||||
assert sl._classification_sides() == {}
|
||||
|
||||
|
||||
def test_language_verdicts_kind_defaults(doc):
|
||||
sl.add_entity(doc, id="rust-crate:cognition_core", kind="rust-crate")
|
||||
sl.add_entity(doc, id="go-surface:rune-entry", kind="go-surface")
|
||||
sl.add_entity(doc, id="workflow:ci.yml", kind="workflow")
|
||||
sl.add_entity(doc, id="contract:BUILDER-x", kind="contract")
|
||||
sl._place_languages(doc)
|
||||
by = {e["id"]: e["identities"]["language"] for e in doc["entities"]}
|
||||
assert by["rust-crate:cognition_core"] == "rust"
|
||||
assert by["go-surface:rune-entry"] == "go"
|
||||
assert by["workflow:ci.yml"] == "n/a"
|
||||
assert by["contract:BUILDER-x"] == "n/a"
|
||||
|
||||
|
||||
def test_language_verdicts_measured_authoritative_then_hints(doc):
|
||||
"""Nick's dichotomy correction: the measured map decides; hints only
|
||||
label UNMEASURED entities and say so; nothing silently defaults."""
|
||||
sl.add_entity(doc, id="module:rune_tui", kind="module") # TUI-LAST measured
|
||||
sl.add_entity(doc, id="module:spine", kind="module") # RUST measured
|
||||
sl.add_entity(doc, id="module:totally_unknown_name", kind="module")
|
||||
sl._place_languages(doc)
|
||||
by = {e["id"]: e["identities"]["language"] for e in doc["entities"]}
|
||||
assert by["module:rune_tui"] == "python-tui (Wave 6)" # from the MAP
|
||||
assert by["module:spine"] == "rust" # from the MAP
|
||||
assert by["module:totally_unknown_name"] == "unclassified"
|
||||
|
||||
|
||||
def test_subsystem_language_from_member_majority(doc):
|
||||
"""identity: 7 RUST / 3 GO members per classification.json → mixed with
|
||||
counts carried in the verdict text (never a silent guess)."""
|
||||
for m in ("behavior", "identity_anchors", "persona_definition",
|
||||
"persona_gate", "prompt_sync", "voice_hybrid", "voice_injector",
|
||||
"voice_sync", "voicebox", "safety_override"):
|
||||
sl.add_entity(doc, id=f"module:{m}", kind="module")
|
||||
sl.add_relationship(doc, type="belongs_to",
|
||||
from_id=f"module:{m}", to_id="subsystem:identity",
|
||||
evidence_refs=["t"])
|
||||
sl.add_entity(doc, id="subsystem:identity", kind="subsystem")
|
||||
sl._place_languages(doc)
|
||||
e = sl.entity(doc, "subsystem:identity")
|
||||
lang = e["identities"]["language"]
|
||||
assert lang.startswith("mixed (rust ")
|
||||
assert "go" in lang
|
||||
|
||||
|
||||
def test_unqualified_discovery_becomes_construction_surface(doc):
|
||||
"""A bare file path from a closed contract is NOT a module — provenance
|
||||
preserved, kind honest, candidate stays DECLARED."""
|
||||
monkey_specs = [("example", [("src/rune/audit_ledger.py",
|
||||
"construction-surface", "candidate")])]
|
||||
original = sl._contract_discoveries
|
||||
sl._contract_discoveries = lambda: monkey_specs
|
||||
try:
|
||||
placed = sl._apply_contract_discoveries(doc)
|
||||
finally:
|
||||
sl._contract_discoveries = original
|
||||
assert placed == 1
|
||||
e = doc["entities"][0]
|
||||
assert e["id"] == "construction-surface:src/rune/audit_ledger.py"
|
||||
assert e["kind"] == "construction-surface"
|
||||
assert e["maturity"] == "DECLARED"
|
||||
assert e["evidence_refs"] == ["contract-discovery:example"]
|
||||
|
||||
|
||||
def test_legacy_unqualified_module_normalized_with_edges(doc):
|
||||
sl.add_entity(doc, id="src/rune/claim_audit.py", kind="module",
|
||||
maturity="PRESENT",
|
||||
evidence_refs=["contract-discovery:old"])
|
||||
sl.add_relationship(doc, type="depends_on",
|
||||
from_id="src/rune/claim_audit.py",
|
||||
to_id="module:other", evidence_refs=["t"])
|
||||
sl._normalize_discovery_ids(doc)
|
||||
assert doc["entities"][0]["id"] == \
|
||||
"construction-surface:src/rune/claim_audit.py"
|
||||
assert doc["entities"][0]["kind"] == "construction-surface"
|
||||
assert doc["relationships"][0]["from"] == \
|
||||
"construction-surface:src/rune/claim_audit.py"
|
||||
# Not in the observed set (no closed contract recorded it) → the
|
||||
# unsupported PRESENT claim is demoted to DECLARED; evidence survives.
|
||||
assert doc["entities"][0]["maturity"] == "DECLARED"
|
||||
assert doc["entities"][0]["evidence_refs"] == ["contract-discovery:old"]
|
||||
|
||||
|
||||
def test_discoveries_gather_reads_real_scaffold_store(tmp_path, monkeypatch):
|
||||
"""The gatherer only trusts CLOSED contracts' ext.contract_quality."""
|
||||
store = tmp_path / "scaffold"
|
||||
store.mkdir()
|
||||
doc = sl.new_doc()
|
||||
sl.add_entity(doc, id="contract:C1", kind="contract",
|
||||
ext={"contract_quality": {
|
||||
"expected_new_observed": ["rust-crate:kernel_x"],
|
||||
"candidate_corrections": [
|
||||
{"entity": "suite"}]}})
|
||||
sl.add_entity(doc, id="contract:C2", kind="contract",
|
||||
ext={"contract_quality": {
|
||||
"expected_new_observed": ["rust-crate:never"]}})
|
||||
for e in doc["entities"]:
|
||||
if e["id"] == "contract:C1":
|
||||
e["phase"] = "closed"
|
||||
if e["id"] == "contract:C2":
|
||||
e["phase"] = "building"
|
||||
monkeypatch.setattr(sl, "ROOT", tmp_path)
|
||||
(tmp_path / "store" / "scaffold").mkdir(parents=True, exist_ok=True)
|
||||
(tmp_path / "store" / "scaffold" / "current.json").write_text(
|
||||
json.dumps(doc))
|
||||
got = sl._contract_discoveries()
|
||||
assert got == [("C1", [("rust-crate:kernel_x", "rust-crate", "observed"),
|
||||
("suite", "construction-surface", "candidate")])]
|
||||
@@ -211,4 +211,8 @@ class TestSpineExtractionNeutrality:
|
||||
"go-surface",
|
||||
"config",
|
||||
"workflow",
|
||||
# Closed contracts may name files or validation surfaces that
|
||||
# are not Python modules. Keep their provenance without lying
|
||||
# about their module identity (issue #2465 CI regression).
|
||||
"construction-surface",
|
||||
}
|
||||
@@ -174,3 +174,76 @@ def test_16k_projection_schema_validated(tmp_path):
|
||||
assert proj.validate(bad) != []
|
||||
bad2 = dict(p, schema_version=999)
|
||||
assert proj.validate(bad2) != []
|
||||
|
||||
|
||||
# ── T5.15 slice 2: the dispatch wire (command → interface execution) ────────
|
||||
|
||||
|
||||
def test_dispatch_executes_accepted_transition_end_to_end(tmp_path):
|
||||
"""A START command on a draft contract executes through the interface:
|
||||
the phase actually moves — not just an accepted intent."""
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
iface = cmds if False else None # noqa: F841 — readability anchor
|
||||
from rune import construction_interface as ci
|
||||
ciface = ci.ConstructionInterface(builder="seed", project_store=store, project_id="proj")
|
||||
ciface.scaffold.init(purpose="t")
|
||||
doc = ciface.scaffold.load()
|
||||
sc.add_entity(doc, id="contract:disp-1", kind="contract",
|
||||
identities={"objective": "dispatch"}, evidence_refs=["e"])
|
||||
doc["entities"][-1]["phase"] = "draft"
|
||||
ciface.scaffold.commit(doc, what="seed")
|
||||
out = cmds.dispatch("START", construction_id="disp-1",
|
||||
store_path=store + "|proj")
|
||||
# external-project store path: dispatch builds its own interface bound
|
||||
# to the same store
|
||||
assert out["executed"] is True, out
|
||||
assert out["success"] is True, out
|
||||
doc2 = ciface.scaffold.load()
|
||||
c = sc.entity(doc2, "contract:disp-1")
|
||||
assert c["phase"] == "approved"
|
||||
|
||||
|
||||
def test_dispatch_refusal_is_a_valid_protocol_outcome(tmp_path):
|
||||
"""START on a non-draft contract: the lifecycle refuses — the dispatch
|
||||
itself succeeds (success=True) with accepted=False."""
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
from rune import construction_interface as ci
|
||||
ciface = ci.ConstructionInterface(builder="seed", project_store=store, project_id="proj")
|
||||
ciface.scaffold.init(purpose="t")
|
||||
doc = ciface.scaffold.load()
|
||||
sc.add_entity(doc, id="contract:disp-2", kind="contract",
|
||||
identities={"objective": "x"}, evidence_refs=["e"])
|
||||
doc["entities"][-1]["phase"] = "draft"
|
||||
ciface.scaffold.commit(doc, what="seed")
|
||||
out = cmds.dispatch("START", construction_id="disp-2",
|
||||
payload={"phase": "building"},
|
||||
store_path=store + "|proj")
|
||||
# contract starts in draft so START is ACCEPTED here; drive to building
|
||||
# first to prove the refusal shape:
|
||||
assert out["executed"] and out["success"]
|
||||
out2 = cmds.dispatch("START", construction_id="disp-2",
|
||||
store_path=store + "|proj")
|
||||
assert out2["accepted"] is False and out2["executed"] is False
|
||||
assert out2["success"] is True
|
||||
assert "draft" in out2["reason"]
|
||||
|
||||
|
||||
def test_dispatch_idempotent_replay_does_not_double_execute(tmp_path):
|
||||
"""Same command_id twice: the second is a replay — the phase does not
|
||||
move twice and idempotent_replay=True comes back."""
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
from rune import construction_interface as ci
|
||||
ciface = ci.ConstructionInterface(builder="seed", project_store=store, project_id="proj")
|
||||
ciface.scaffold.init(purpose="t")
|
||||
doc = ciface.scaffold.load()
|
||||
sc.add_entity(doc, id="contract:disp-3", kind="contract",
|
||||
identities={"objective": "idem"}, evidence_refs=["e"])
|
||||
doc["entities"][-1]["phase"] = "draft"
|
||||
ciface.scaffold.commit(doc, what="seed")
|
||||
first = cmds.dispatch("START", construction_id="disp-3",
|
||||
command_id="fixed-cmd-1", store_path=store + "|proj")
|
||||
assert first["executed"] and first["success"]
|
||||
second = cmds.dispatch("START", construction_id="disp-3",
|
||||
command_id="fixed-cmd-1", store_path=store + "|proj")
|
||||
assert second["idempotent_replay"] is True
|
||||
assert second.get("executed") in (False, None) # replay ≠ execution
|
||||
@@ -0,0 +1,115 @@
|
||||
"""T5.13/T5.14 frozen contracts: pane order and post-response silence.
|
||||
|
||||
The pane swap is structural (compose order); the dim-spam consolidation
|
||||
is behavioral (one collapsed line instead of five dim messages).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import rune_tui # noqa: E402 — the TUI lives in the package
|
||||
|
||||
|
||||
def test_chat_channel_is_yielded_first_in_compose():
|
||||
src = (ROOT / "src" / "rune" / "rune_tui.py").read_text()
|
||||
body = re.search(
|
||||
r'with Horizontal\(id="channels"\):(.*?)with Horizontal\(id="footer-bar"\)',
|
||||
src,
|
||||
re.DOTALL,
|
||||
).group(1)
|
||||
order = re.findall(r'ChannelView\(\s*"(\w+)"', body)
|
||||
assert order == ["chat", "task"], f"compose yields {order}"
|
||||
|
||||
|
||||
def test_resize_actions_follow_the_new_visual_positions():
|
||||
"""After the swap LEFT=chat, RIGHT=task. resize_left shrinks the LEFT
|
||||
(chat) — which GROWS the task share; resize_right is the reverse."""
|
||||
assert "grow the RIGHT (task)" in rune_tui.RuneApp.action_resize_left.__doc__
|
||||
assert "shrink the RIGHT (task)" in rune_tui.RuneApp.action_resize_right.__doc__
|
||||
# Behavioral proof against an instance WITHOUT constructing the App
|
||||
# (reactive initialization needs a running textual app; the docstring
|
||||
# carries the contract, the arithmetic is what matters).
|
||||
app = SimpleNamespace(
|
||||
task_width=50,
|
||||
action_resize_left=None,
|
||||
action_resize_right=None,
|
||||
_apply_resize=None,
|
||||
)
|
||||
app._apply_resize = lambda: None
|
||||
# Bind the real methods onto the stand-in and call them.
|
||||
rune_tui.RuneApp.action_resize_left(app)
|
||||
assert app.task_width == 55
|
||||
rune_tui.RuneApp.action_resize_right(app)
|
||||
assert app.task_width == 50
|
||||
|
||||
|
||||
def _isolate_collector(monkeypatch, **modules):
|
||||
monkeypatch.setattr(rune_tui, "BACKEND_AVAILABLE", True)
|
||||
for name, value in modules.items():
|
||||
monkeypatch.setattr(rune_tui, name, value)
|
||||
|
||||
|
||||
def test_collector_returns_all_signals_in_one_pass(monkeypatch):
|
||||
_isolate_collector(
|
||||
monkeypatch,
|
||||
provider_budget=SimpleNamespace(
|
||||
get_status=lambda: {
|
||||
"providers": {"p": {"monthly_limit": 100, "monthly_used": 90, "token_limit": 0, "tokens_used": 0}}
|
||||
}
|
||||
),
|
||||
claim_audit=SimpleNamespace(scan_text=lambda _t: {"degraded": ["a", "b"]}),
|
||||
growth_delta=SimpleNamespace(check=lambda: {"delta": "focus: +1"}),
|
||||
self_improve=SimpleNamespace(run=lambda: None),
|
||||
)
|
||||
signals = rune_tui.collect_post_response_signals("response text", messages_count=10)
|
||||
assert signals == ["⚠ Budget: 90% used", "⚠ Claims degraded: 2", "📈 Growth: focus: +1", "🔄 Self-improvement cycle triggered"]
|
||||
|
||||
|
||||
def test_collector_silence_when_nothing_fires(monkeypatch):
|
||||
_isolate_collector(
|
||||
monkeypatch,
|
||||
provider_budget=Mock(get_status=lambda: {"providers": {}}),
|
||||
claim_audit=Mock(scan_text=lambda _t: None),
|
||||
growth_delta=Mock(check=lambda: None),
|
||||
self_improve=Mock(run=lambda: None),
|
||||
)
|
||||
collector = rune_tui.collect_post_response_signals
|
||||
app_signals = collector("plain", messages_count=7) # not a cycle multiple
|
||||
assert app_signals == []
|
||||
|
||||
|
||||
def test_collector_never_raises_on_backend_noise(monkeypatch):
|
||||
def boom(*_a, **_k):
|
||||
raise RuntimeError("backend gone")
|
||||
|
||||
_isolate_collector(
|
||||
monkeypatch,
|
||||
provider_budget=Mock(get_status=boom),
|
||||
claim_audit=Mock(scan_text=boom),
|
||||
growth_delta=Mock(check=boom),
|
||||
self_improve=Mock(run=boom),
|
||||
)
|
||||
assert rune_tui.collect_post_response_signals("x", 10) == []
|
||||
|
||||
|
||||
def test_self_improve_only_at_cycle_multiple(monkeypatch):
|
||||
ran = []
|
||||
_isolate_collector(
|
||||
monkeypatch,
|
||||
provider_budget=Mock(get_status=lambda: {"providers": {}}),
|
||||
claim_audit=Mock(scan_text=lambda _t: None),
|
||||
growth_delta=Mock(check=lambda: None),
|
||||
self_improve=Mock(run=lambda: ran.append(1)),
|
||||
)
|
||||
rune_tui.collect_post_response_signals("x", 7)
|
||||
assert ran == []
|
||||
rune_tui.collect_post_response_signals("x", 10)
|
||||
assert ran == [1]
|
||||
Reference in new issue
Block a user