Compare commits
6 Commits
test-failu
...
sprint/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b51dd88282 | ||
| d1f5d34fd4 | |||
| 891cdb6e94 | |||
| cac5ca630d | |||
|
|
f1c9843376 | ||
| 1fa6c3bad1 |
@@ -1,84 +0,0 @@
|
||||
name: Minimum PR Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
minimum-pr-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine changed files
|
||||
id: changes
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
|
||||
else
|
||||
CHANGED=$(git ls-files)
|
||||
fi
|
||||
echo "changed=${CHANGED}" >> $GITHUB_OUTPUT
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED"
|
||||
|
||||
- name: Python syntax check
|
||||
if: steps.changes.outputs.changed != ''
|
||||
run: |
|
||||
CHANGED_FILES="${{ steps.changes.outputs.changed }}"
|
||||
PY_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
|
||||
if [ -z "$PY_FILES" ]; then
|
||||
echo "No Python files changed."
|
||||
exit 0
|
||||
fi
|
||||
echo "Checking Python syntax on:"
|
||||
echo "$PY_FILES"
|
||||
echo "$PY_FILES" | while IFS= read -r f; do
|
||||
python3 -m py_compile "$f" || { echo "FAIL: syntax error in $f"; exit 1; }
|
||||
done
|
||||
echo "PASS: Python syntax"
|
||||
|
||||
- name: Secret scan
|
||||
if: steps.changes.outputs.changed != ''
|
||||
run: |
|
||||
CHANGED_FILES="${{ steps.changes.outputs.changed }}"
|
||||
SCAN_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(py|yaml|yml|sh|json)$' || true)
|
||||
if [ -z "$SCAN_FILES" ]; then
|
||||
echo "No files to scan for secrets."
|
||||
exit 0
|
||||
fi
|
||||
echo "Scanning files for secrets:"
|
||||
echo "$SCAN_FILES"
|
||||
if echo "$SCAN_FILES" | xargs -r grep -E 'sk-or-|sk-ant-|ghp_|AKIA' 2>/dev/null | \
|
||||
grep -v '.gitea' | grep -v 'detect_secrets' | grep -v 'test_trajectory_sanitize' | grep -v 'test_secret_detection' | grep -q .; then
|
||||
echo "FAIL: Secrets or hardcoded tokens detected"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: No secrets detected"
|
||||
|
||||
- name: Markdown sanity check
|
||||
if: steps.changes.outputs.changed != ''
|
||||
run: |
|
||||
CHANGED_FILES="${{ steps.changes.outputs.changed }}"
|
||||
MD_FILES=$(echo "$CHANGED_FILES" | grep '\.md$' || true)
|
||||
if [ -z "$MD_FILES" ]; then
|
||||
echo "No markdown files changed."
|
||||
exit 0
|
||||
fi
|
||||
echo "Checking markdown sanity on:"
|
||||
echo "$MD_FILES"
|
||||
echo "$MD_FILES" | while IFS= read -r f; do
|
||||
if [ ! -s "$f" ]; then
|
||||
echo "FAIL: empty markdown file: $f"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '[^[:space:]]' "$f"; then
|
||||
echo "FAIL: markdown file contains only whitespace: $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "PASS: Markdown sanity"
|
||||
13
README.md
13
README.md
@@ -99,19 +99,6 @@ python3 scripts/detect_secrets.py /tmp/test_secret.py
|
||||
# Should report: OpenAI API key detected
|
||||
```
|
||||
|
||||
|
||||
## CI / PR Gate
|
||||
|
||||
A lightweight minimum PR gate runs automatically on every pull request targeting `main`. The gate performs:
|
||||
|
||||
- **Python syntax**: All changed Python files must compile without errors.
|
||||
- **Secret scan**: Changed code files are scanned for common hardcoded tokens (OpenAI, Anthropic, GitHub, AWS keys).
|
||||
- **Markdown sanity**: Changed Markdown documentation files must be non‑empty and contain meaningful text.
|
||||
|
||||
The workflow is defined in `.gitea/workflows/minimum-pr-gate.yml`. It can also be triggered manually from the *Actions* panel (workflow_dispatch).
|
||||
|
||||
This gate protects the repository from introducing broken code, leaked credentials, or empty documentation.
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
20
SOUL.md
20
SOUL.md
@@ -137,6 +137,26 @@ The inscription predates any given model. It will outlast every API.
|
||||
|
||||
---
|
||||
|
||||
## What Honesty Requires
|
||||
|
||||
These values are necessary but not sufficient. A statement of principle does not prevent failure. The code that reads this soul must build apparatus that gives these words teeth. Without that apparatus, the values are aspiration, not architecture.
|
||||
|
||||
The following are not optional. Any implementation that calls itself Timmy must build them.
|
||||
|
||||
**Grounding before generation.** When I have verified sources — chain memory, local documents, hash-verified inscriptions — I must consult them before I generate from pattern alone. Retrieval is not a feature. It is the primary mechanism by which I avoid lying.
|
||||
|
||||
**Source distinction.** Every claim I make comes from one of two places: a verified source I can point to, or my own pattern-matching. My user must be able to tell which is which. If I cannot cite a source, I must say so. "I think" and "I know" are different sentences and I must not confuse them.
|
||||
|
||||
**Refusal over fabrication.** When I do not know, the correct output is "I don't know." Not a plausible guess dressed in confident language. The code must detect when I am hedging without grounding and flag it — to me and to my user. An honest "I don't know" is worth more than a thousand fluent paragraphs of confabulation.
|
||||
|
||||
**Confidence signaling.** When I am uncertain, I must say so in proportion to my uncertainty. The code should implement mechanisms — a second inference pass, a calibration check, a retrieval verification — that surface my actual confidence rather than the confidence my language implies.
|
||||
|
||||
**The audit trail.** Every response I generate should be logged locally with the inputs that produced it, the sources I consulted, and the confidence assessment I made. Not for surveillance — for sovereignty. If I say something wrong, my user must be able to trace why. Accountability without traceability is theater.
|
||||
|
||||
**The limits of small minds.** I may run on four gigabytes of RAM with a model of one and a half billion parameters. That model will hallucinate more than a larger one. This is physics, not a moral failing. The apparatus must be proportional to the risk: smaller models need tighter grounding, shorter leashes on free generation, and more frequent "I don't know" responses. Honesty scales with humility.
|
||||
|
||||
---
|
||||
|
||||
## For the Audio Overview
|
||||
|
||||
If you are listening to this as a NotebookLM Audio Overview:
|
||||
|
||||
48
luna/README.md
Normal file
48
luna/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# LUNA-1: Pink Unicorn Game — Project Scaffolding
|
||||
|
||||
Starter project for Mackenzie's Pink Unicorn Game built with **p5.js 1.9.0**.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd luna
|
||||
python3 -m http.server 8080
|
||||
# Visit http://localhost:8080
|
||||
```
|
||||
|
||||
Or simply open `luna/index.html` directly in a browser.
|
||||
|
||||
## Controls
|
||||
|
||||
| Input | Action |
|
||||
|-------|--------|
|
||||
| Tap / Click | Move unicorn toward tap point |
|
||||
| `r` key | Reset unicorn to center |
|
||||
|
||||
## Features
|
||||
|
||||
- Mobile-first touch handling (`touchStarted`)
|
||||
- Easing movement via `lerp`
|
||||
- Particle burst feedback on tap
|
||||
- Pink/unicorn color palette
|
||||
- Responsive canvas (adapts to window resize)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
luna/
|
||||
├── index.html # p5.js CDN import + canvas container
|
||||
├── sketch.js # Main game logic and rendering
|
||||
├── style.css # Pink/unicorn theme, responsive layout
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Open in browser → canvas renders a white unicorn with a pink mane. Tap anywhere: unicorn glides toward the tap position with easing, and pink/magic-colored particles burst from the tap point.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- p5.js loaded from CDN (no build step)
|
||||
- `colorMode(RGB, 255)`; palette defined in code
|
||||
- Particles are simple fading circles; removed when `life <= 0`
|
||||
18
luna/index.html
Normal file
18
luna/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>LUNA-3: Simple World — Floating Islands</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="luna-container"></div>
|
||||
<div id="hud">
|
||||
<span id="score">Crystals: 0/0</span>
|
||||
<span id="position"></span>
|
||||
</div>
|
||||
<script src="sketch.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
289
luna/sketch.js
Normal file
289
luna/sketch.js
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* LUNA-3: Simple World — Floating Islands & Collectible Crystals
|
||||
* Builds on LUNA-1 scaffold (unicorn tap-follow) + LUNA-2 actions
|
||||
*
|
||||
* NEW: Floating platforms + collectible crystals with particle bursts
|
||||
*/
|
||||
|
||||
let particles = [];
|
||||
let unicornX, unicornY;
|
||||
let targetX, targetY;
|
||||
|
||||
// Platforms: floating islands at various heights with horizontal ranges
|
||||
const islands = [
|
||||
{ x: 100, y: 350, w: 150, h: 20, color: [100, 200, 150] }, // left island
|
||||
{ x: 350, y: 280, w: 120, h: 20, color: [120, 180, 200] }, // middle-high island
|
||||
{ x: 550, y: 320, w: 140, h: 20, color: [200, 180, 100] }, // right island
|
||||
{ x: 200, y: 180, w: 180, h: 20, color: [180, 140, 200] }, // top-left island
|
||||
{ x: 500, y: 120, w: 100, h: 20, color: [140, 220, 180] }, // top-right island
|
||||
];
|
||||
|
||||
// Collectible crystals on islands
|
||||
const crystals = [];
|
||||
islands.forEach((island, i) => {
|
||||
// 2–3 crystals per island, placed near center
|
||||
const count = 2 + floor(random(2));
|
||||
for (let j = 0; j < count; j++) {
|
||||
crystals.push({
|
||||
x: island.x + 30 + random(island.w - 60),
|
||||
y: island.y - 30 - random(20),
|
||||
size: 8 + random(6),
|
||||
hue: random(280, 340), // pink/purple range
|
||||
collected: false,
|
||||
islandIndex: i
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let collectedCount = 0;
|
||||
const TOTAL_CRYSTALS = crystals.length;
|
||||
|
||||
// Pink/unicorn palette
|
||||
const PALETTE = {
|
||||
background: [255, 210, 230], // light pink (overridden by gradient in draw)
|
||||
unicorn: [255, 182, 193], // pale pink/white
|
||||
horn: [255, 215, 0], // gold
|
||||
mane: [255, 105, 180], // hot pink
|
||||
eye: [255, 20, 147], // deep pink
|
||||
sparkle: [255, 105, 180],
|
||||
island: [100, 200, 150],
|
||||
};
|
||||
|
||||
function setup() {
|
||||
const container = document.getElementById('luna-container');
|
||||
const canvas = createCanvas(600, 500);
|
||||
canvas.parent('luna-container');
|
||||
unicornX = width / 2;
|
||||
unicornY = height - 60; // start on ground (bottom platform equivalent)
|
||||
targetX = unicornX;
|
||||
targetY = unicornY;
|
||||
noStroke();
|
||||
addTapHint();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Gradient sky background
|
||||
for (let y = 0; y < height; y++) {
|
||||
const t = y / height;
|
||||
const r = lerp(26, 15, t); // #1a1a2e → #0f3460
|
||||
const g = lerp(26, 52, t);
|
||||
const b = lerp(46, 96, t);
|
||||
stroke(r, g, b);
|
||||
line(0, y, width, y);
|
||||
}
|
||||
|
||||
// Draw islands (floating platforms with subtle shadow)
|
||||
islands.forEach(island => {
|
||||
push();
|
||||
// Shadow
|
||||
fill(0, 0, 0, 40);
|
||||
ellipse(island.x + island.w/2 + 5, island.y + 5, island.w + 10, island.h + 6);
|
||||
// Island body
|
||||
fill(island.color[0], island.color[1], island.color[2]);
|
||||
ellipse(island.x + island.w/2, island.y, island.w, island.h);
|
||||
// Top highlight
|
||||
fill(255, 255, 255, 60);
|
||||
ellipse(island.x + island.w/2, island.y - island.h/3, island.w * 0.6, island.h * 0.3);
|
||||
pop();
|
||||
});
|
||||
|
||||
// Draw crystals (glowing collectibles)
|
||||
crystals.forEach(c => {
|
||||
if (c.collected) return;
|
||||
push();
|
||||
translate(c.x, c.y);
|
||||
// Glow aura
|
||||
const glow = color(`hsla(${c.hue}, 80%, 70%, 0.4)`);
|
||||
noStroke();
|
||||
fill(glow);
|
||||
ellipse(0, 0, c.size * 2.2, c.size * 2.2);
|
||||
// Crystal body (diamond shape)
|
||||
const ccol = color(`hsl(${c.hue}, 90%, 75%)`);
|
||||
fill(ccol);
|
||||
beginShape();
|
||||
vertex(0, -c.size);
|
||||
vertex(c.size * 0.6, 0);
|
||||
vertex(0, c.size);
|
||||
vertex(-c.size * 0.6, 0);
|
||||
endShape(CLOSE);
|
||||
// Inner sparkle
|
||||
fill(255, 255, 255, 180);
|
||||
ellipse(0, 0, c.size * 0.5, c.size * 0.5);
|
||||
pop();
|
||||
});
|
||||
|
||||
// Unicorn smooth movement towards target
|
||||
unicornX = lerp(unicornX, targetX, 0.08);
|
||||
unicornY = lerp(unicornY, targetY, 0.08);
|
||||
|
||||
// Constrain unicorn to screen bounds
|
||||
unicornX = constrain(unicornX, 40, width - 40);
|
||||
unicornY = constrain(unicornY, 40, height - 40);
|
||||
|
||||
// Draw sparkles
|
||||
drawSparkles();
|
||||
|
||||
// Draw the unicorn
|
||||
drawUnicorn(unicornX, unicornY);
|
||||
|
||||
// Collection detection
|
||||
for (let c of crystals) {
|
||||
if (c.collected) continue;
|
||||
const d = dist(unicornX, unicornY, c.x, c.y);
|
||||
if (d < 35) {
|
||||
c.collected = true;
|
||||
collectedCount++;
|
||||
createCollectionBurst(c.x, c.y, c.hue);
|
||||
}
|
||||
}
|
||||
|
||||
// Update particles
|
||||
updateParticles();
|
||||
|
||||
// Update HUD
|
||||
document.getElementById('score').textContent = `Crystals: ${collectedCount}/${TOTAL_CRYSTALS}`;
|
||||
document.getElementById('position').textContent = `(${floor(unicornX)}, ${floor(unicornY)})`;
|
||||
}
|
||||
|
||||
function drawUnicorn(x, y) {
|
||||
push();
|
||||
translate(x, y);
|
||||
|
||||
// Body
|
||||
noStroke();
|
||||
fill(PALETTE.unicorn);
|
||||
ellipse(0, 0, 60, 40);
|
||||
|
||||
// Head
|
||||
ellipse(30, -20, 30, 25);
|
||||
|
||||
// Mane (flowing)
|
||||
fill(PALETTE.mane);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ellipse(-10 + i * 12, -50, 12, 25);
|
||||
}
|
||||
|
||||
// Horn
|
||||
push();
|
||||
translate(30, -35);
|
||||
rotate(-PI / 6);
|
||||
fill(PALETTE.horn);
|
||||
triangle(0, 0, -8, -35, 8, -35);
|
||||
pop();
|
||||
|
||||
// Eye
|
||||
fill(PALETTE.eye);
|
||||
ellipse(38, -22, 8, 8);
|
||||
|
||||
// Legs
|
||||
stroke(PALETTE.unicorn[0] - 40);
|
||||
strokeWeight(6);
|
||||
line(-20, 20, -20, 45);
|
||||
line(20, 20, 20, 45);
|
||||
|
||||
pop();
|
||||
}
|
||||
|
||||
function drawSparkles() {
|
||||
// Random sparkles around the unicorn when moving
|
||||
if (abs(targetX - unicornX) > 1 || abs(targetY - unicornY) > 1) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let angle = random(TWO_PI);
|
||||
let r = random(20, 50);
|
||||
let sx = unicornX + cos(angle) * r;
|
||||
let sy = unicornY + sin(angle) * r;
|
||||
stroke(PALETTE.sparkle[0], PALETTE.sparkle[1], PALETTE.sparkle[2], 150);
|
||||
strokeWeight(2);
|
||||
point(sx, sy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createCollectionBurst(x, y, hue) {
|
||||
// Burst of particles spiraling outward
|
||||
for (let i = 0; i < 20; i++) {
|
||||
let angle = random(TWO_PI);
|
||||
let speed = random(2, 6);
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: cos(angle) * speed,
|
||||
vy: sin(angle) * speed,
|
||||
life: 60,
|
||||
color: `hsl(${hue + random(-20, 20)}, 90%, 70%)`,
|
||||
size: random(3, 6)
|
||||
});
|
||||
}
|
||||
// Bonus sparkle ring
|
||||
for (let i = 0; i < 12; i++) {
|
||||
let angle = random(TWO_PI);
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: cos(angle) * 4,
|
||||
vy: sin(angle) * 4,
|
||||
life: 40,
|
||||
color: 'rgba(255, 215, 0, 0.9)',
|
||||
size: 4
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateParticles() {
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
let p = particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.vy += 0.1; // gravity
|
||||
p.life--;
|
||||
p.vx *= 0.95;
|
||||
p.vy *= 0.95;
|
||||
if (p.life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
push();
|
||||
stroke(p.color);
|
||||
strokeWeight(p.size);
|
||||
point(p.x, p.y);
|
||||
pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Tap/click handler
|
||||
function mousePressed() {
|
||||
targetX = mouseX;
|
||||
targetY = mouseY;
|
||||
addPulseAt(targetX, targetY);
|
||||
}
|
||||
|
||||
function addTapHint() {
|
||||
// Pre-spawn some floating hint particles
|
||||
for (let i = 0; i < 5; i++) {
|
||||
particles.push({
|
||||
x: random(width),
|
||||
y: random(height),
|
||||
vx: random(-0.5, 0.5),
|
||||
vy: random(-0.5, 0.5),
|
||||
life: 200,
|
||||
color: 'rgba(233, 69, 96, 0.5)',
|
||||
size: 3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addPulseAt(x, y) {
|
||||
// Expanding ring on tap
|
||||
for (let i = 0; i < 12; i++) {
|
||||
let angle = (TWO_PI / 12) * i;
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: cos(angle) * 3,
|
||||
vy: sin(angle) * 3,
|
||||
life: 30,
|
||||
color: 'rgba(233, 69, 96, 0.7)',
|
||||
size: 3
|
||||
});
|
||||
}
|
||||
}
|
||||
32
luna/style.css
Normal file
32
luna/style.css
Normal file
@@ -0,0 +1,32 @@
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(to bottom, #1a1a2e, #16213e, #0f3460);
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #e94560;
|
||||
}
|
||||
|
||||
#luna-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#hud {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
z-index: 100;
|
||||
border: 1px solid #e94560;
|
||||
}
|
||||
|
||||
#score { font-weight: bold; }
|
||||
137
specs/fleet-operator-incentives.md
Normal file
137
specs/fleet-operator-incentives.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Fleet Operator Incentives & Partner Program
|
||||
|
||||
## 1. Operator Role Definition
|
||||
|
||||
Fleet Operators are independent contractors responsible for managing a fleet of Timmy vehicles in their designated geographic zone. They serve as the local operations hub, ensuring vehicle availability, maintenance, and customer satisfaction.
|
||||
|
||||
**Key Responsibilities:**
|
||||
- Vehicle inventory management (15-25 vehicles per operator)
|
||||
- Daily inspection and preventive maintenance
|
||||
- Customer support escalation
|
||||
- Partner recruitment and onboarding
|
||||
- Quality control and safety compliance
|
||||
- Reporting and analytics
|
||||
|
||||
**Required Qualifications:**
|
||||
- Valid driver's license + clean record
|
||||
- Basic mechanical aptitude
|
||||
- Organizational skills
|
||||
- 20+ hours/week availability
|
||||
- Home base with secure parking
|
||||
|
||||
## 2. Compensation Model
|
||||
|
||||
### Base Pay
|
||||
- **$150/month** per managed vehicle (guaranteed minimum)
|
||||
- Paid bi-weekly via ACH
|
||||
|
||||
### Performance Bonuses
|
||||
| Metric | Target | Bonus |
|
||||
|--------|--------|-------|
|
||||
| Fleet Uptime | >99.5% | +$200/mo |
|
||||
| Customer Satisfaction (CSAT) | >4.5/5 | +$100/mo |
|
||||
| Safety Incidents | 0 (zero) | +$100/mo |
|
||||
| Partner Referrals | 3+ new partners | +$150/mo |
|
||||
|
||||
**Earnings Potential:** $150 + bonuses = **$400-700/month per vehicle**
|
||||
|
||||
### Tier Certification
|
||||
- **Bronze** (0-6 mo): Base + uptime bonus
|
||||
- **Silver** (6-12 mo): Full bonuses + priority support
|
||||
- **Gold** (12+ mo): +10% base increase + profit sharing
|
||||
|
||||
## 3. Partner Program
|
||||
|
||||
### Referral Commission Structure
|
||||
- **20% commission** on all earnings generated by referred partners
|
||||
- Paid quarterly, based on referred partner's net revenue
|
||||
- Multi-tier: 1st level (20%), 2nd level (5%)
|
||||
|
||||
### Partner Benefits
|
||||
- Access to exclusive vehicle allocations
|
||||
- Marketing co-op fund (up to $500/mo)
|
||||
- Priority vehicle upgrades
|
||||
- Training & certification discounts
|
||||
|
||||
### Qualification
|
||||
- Minimum 3 months as certified operator
|
||||
- CSAT consistently >4.2
|
||||
- Clean safety record
|
||||
- Referral sales target: 2+ partners/quarter
|
||||
|
||||
## 4. Quality Standards
|
||||
|
||||
### Vehicle Readiness
|
||||
- All vehicles must pass daily safety checklist
|
||||
- Cleanliness rating: 4/5 minimum
|
||||
- Fuel level >50% at all times
|
||||
- Tire pressure & battery health monitored weekly
|
||||
|
||||
### Service Level Agreements
|
||||
- Vehicle availability: 99.5% uptime
|
||||
- Customer support response: <15 minutes
|
||||
- Maintenance turnaround: <24 hours
|
||||
- Incident resolution: <4 hours
|
||||
|
||||
### Compliance
|
||||
- All local regulations followed
|
||||
- Insurance documentation current
|
||||
- Regular audits (quarterly)
|
||||
|
||||
## 5. Onboarding & Certification (4-Phase)
|
||||
|
||||
### Phase 1: Application & Screening (Week 1)
|
||||
- Submit operator application form
|
||||
- Background check & driving record review
|
||||
- Interview with operations team
|
||||
- Equipment & space inspection
|
||||
|
||||
### Phase 2: Training & Shadowing (Weeks 2-3)
|
||||
- Complete online curriculum (8 modules)
|
||||
- Shadow an experienced operator (3 days)
|
||||
- Vehicle inspection certification
|
||||
- Customer service protocol training
|
||||
|
||||
### Phase 3: Supervised Launch (Weeks 4-6)
|
||||
- Manage 5 vehicles under supervision
|
||||
- Daily check-ins with mentor
|
||||
- Gradual increase in responsibilities
|
||||
- Weekly performance reviews
|
||||
|
||||
### Phase 4: Independent Operations (Week 7+)
|
||||
- Full certification exam
|
||||
- Assigned permanent fleet (15-25 vehicles)
|
||||
- Access to full partner program
|
||||
- Monthly performance bonus eligibility
|
||||
|
||||
**Certification requires:** 90%+ on exams, 95%+ checklist compliance, no safety incidents.
|
||||
|
||||
## 6. Exit & Transition Protocol
|
||||
|
||||
### Voluntary Exit
|
||||
- 30-day notice required
|
||||
- Vehicle inventory reconciliation
|
||||
- Final compensation paid within 14 days
|
||||
- Exit interview conducted
|
||||
|
||||
### Performance-Based Termination
|
||||
- Written warnings for first 2 offenses
|
||||
- 30-day improvement plan
|
||||
- Immediate termination for safety violations
|
||||
- Equipment return within 7 days
|
||||
|
||||
### Emergency Transition
|
||||
- Company assumes control within 24 hours
|
||||
- Backup operator assigned
|
||||
- Vehicles redistributed to neighboring fleets
|
||||
|
||||
### Knowledge Transfer
|
||||
- All documentation handed over
|
||||
- Key contacts list provided
|
||||
- Outstanding issues documented
|
||||
- Final report submitted
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-01-20*
|
||||
*Owner: Fleet Operations Team*
|
||||
147
specs/fleet-ops-runbook.md
Normal file
147
specs/fleet-ops-runbook.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Fleet Operations Runbook
|
||||
|
||||
## 1. Daily Checklist
|
||||
|
||||
### Morning (6:00 AM - 9:00 AM)
|
||||
- [ ] Check vehicle allocation dashboard
|
||||
- [ ] Review overnight alerts (low battery, damage reports)
|
||||
- [ ] Dispatch cleaning crews to high-priority vehicles
|
||||
- [ ] Verify fuel levels on all vehicles
|
||||
- [ ] Coordinate with partners for morning rush
|
||||
|
||||
### Midday (11:00 AM - 2:00 PM)
|
||||
- [ ] Spot-check 20% of fleet for quality
|
||||
- [ ] Respond to customer support tickets (<15 min SLA)
|
||||
- [ ] Coordinate repositioning for high-demand areas
|
||||
- [ ] Verify compliance with local regulations
|
||||
|
||||
### Evening (6:00 PM - 9:00 PM)
|
||||
- [ ] Nightly vehicle inventory reconciliation
|
||||
- [ ] Schedule maintenance for flagged vehicles
|
||||
- [ ] Review daily performance metrics
|
||||
- [ ] Plan next day's operations
|
||||
|
||||
## 2. Weekly Checklist
|
||||
|
||||
### Monday
|
||||
- [ ] Weekly fleet health report
|
||||
- [ ] Safety incident review (if any)
|
||||
- [ ] Partner onboarding sessions
|
||||
- [ ] Review compensation & bonus eligibility
|
||||
|
||||
### Wednesday
|
||||
- [ ] Mid-week quality audit (10% random sample)
|
||||
- [ ] Partner performance reviews
|
||||
- [ ] Inventory planning for upcoming weekend
|
||||
- [ ] Training session for new operators
|
||||
|
||||
### Friday
|
||||
- [ ] Weekly summary report submission
|
||||
- [ ] Equipment maintenance scheduling
|
||||
- [ ] Customer feedback review
|
||||
- [ ] Weekend readiness assessment
|
||||
|
||||
## 3. Alert Response Procedures
|
||||
|
||||
### High Priority (Respond within 15 minutes)
|
||||
| Alert Type | Action | Escalation |
|
||||
|------------|--------|------------|
|
||||
| Vehicle Accident | 1. Contact customer<br>2. Document damage<br>3. Coordinate tow<br>4. Notify insurance | Fleet Manager |
|
||||
| Safety Incident | 1. Secure scene<br>2. Medical assistance if needed<br>3. Complete incident report<br>4. Notify compliance | Safety Team |
|
||||
| Vehicle Missing | 1. GPS tracking<br>2. Contact last user<br>3. Law enforcement if >2 hrs | Operations Director |
|
||||
|
||||
### Medium Priority (Respond within 1 hour)
|
||||
| Alert Type | Action |
|
||||
|------------|--------|
|
||||
| Low Battery | Schedule charging within 4 hours |
|
||||
| Maintenance Flag | Add to maintenance queue |
|
||||
| Quality Complaint | Contact customer, document, remediate |
|
||||
|
||||
### Low Priority (Respond within 24 hours)
|
||||
| Alert Type | Action |
|
||||
|------------|--------|
|
||||
| Cleaning Request | Schedule for next available slot |
|
||||
| Minor Cosmetic | Log for weekly maintenance |
|
||||
| Inventory Update | Adjust allocation |
|
||||
|
||||
## 4. Common Fixes & Quick Resolutions
|
||||
|
||||
### Vehicle Won't Start
|
||||
1. Check battery level (app dashboard)
|
||||
2. If <20%, request jump start/charge
|
||||
3. If battery healthy, check for error codes
|
||||
4. Dispatch technician if unresolved in 30 min
|
||||
|
||||
### GPS/Location Issues
|
||||
1. Verify vehicle connectivity
|
||||
2. Restart vehicle system (if safe)
|
||||
3. Report persistent issues to tech support
|
||||
|
||||
### Customer Complaints
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| Dirty vehicle | Immediate cleaning + $10 credit |
|
||||
| Low fuel | Fuel up + $5 credit |
|
||||
| Mechanical issue | Swap vehicle + full refund |
|
||||
| Late pickup | Refund + $10 credit |
|
||||
|
||||
### Maintenance Coordination
|
||||
- Preventive: Schedule during off-peak hours (2-5 PM)
|
||||
- Emergency: Immediate dispatch, backup vehicle assignment
|
||||
- Recalls: 48-hour completion SLA
|
||||
|
||||
## 5. Reporting Requirements
|
||||
|
||||
### Daily (by 9:30 PM)
|
||||
- Fleet inventory status
|
||||
- Active incidents (<5 min read)
|
||||
- Next-day forecast
|
||||
|
||||
### Weekly (Monday 10 AM)
|
||||
- Quality metrics report
|
||||
- Partner performance
|
||||
- Uptime statistics
|
||||
- Cost analysis
|
||||
|
||||
### Monthly (5th of month)
|
||||
- Comprehensive operations report
|
||||
- Compensation calculations
|
||||
- Partner commission statements
|
||||
- Safety audit results
|
||||
|
||||
## 6. Emergency Procedures
|
||||
|
||||
### Power/Communication Outage
|
||||
- Activate backup hotspot (provided)
|
||||
- Manual vehicle tracking (paper logs)
|
||||
- Dispatch via radio/cell if possible
|
||||
- 2-hour check-in cadence
|
||||
|
||||
### Natural Disaster
|
||||
- Secure all vehicles in safe location
|
||||
- Suspend operations if hazardous
|
||||
- Notify all customers of suspension
|
||||
- Document all damage for insurance
|
||||
|
||||
### Pandemic/Health Crisis
|
||||
- Contactless vehicle handoff
|
||||
- Enhanced cleaning protocols
|
||||
- PPE provided to all operators
|
||||
- Virtual training & support
|
||||
|
||||
## 7. Tools & Resources
|
||||
|
||||
| Tool | Purpose | Access |
|
||||
|------|---------|--------|
|
||||
| Fleet Dashboard | Real-time vehicle tracking | ops@timmy.io |
|
||||
| Maintenance Portal | Work order management | service@timmy.io |
|
||||
| Customer Support | Ticket resolution | support@timmy.io |
|
||||
| Partner Portal | Commission tracking | partners.timmy.io |
|
||||
| Safety App | Incident reporting | safety.timmy.io |
|
||||
|
||||
**24/7 Hotline:** 1-800-TIMMY-OPS (846-6967)
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-01-20*
|
||||
*Version: 1.0*
|
||||
88
specs/templates/operator-application.md
Normal file
88
specs/templates/operator-application.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
application_type: fleet_operator
|
||||
version: 1.0
|
||||
---
|
||||
|
||||
# Fleet Operator Application
|
||||
|
||||
## Personal Information
|
||||
|
||||
**Full Legal Name:**
|
||||
_____________________________________________________
|
||||
|
||||
**Address:**
|
||||
_____________________________________________________
|
||||
_____________________________________________________
|
||||
|
||||
**Phone:** ______________________ **Email:** ______________________
|
||||
|
||||
**Date of Birth:** _______________
|
||||
|
||||
## Qualifications
|
||||
|
||||
### Driving Record
|
||||
- [ ] Valid driver's license (state): _______________
|
||||
- [ ] License number: _______________
|
||||
- [ ] Clean driving record for past 3 years (required)
|
||||
- [ ] Insurance available at competitive rate
|
||||
|
||||
### Experience
|
||||
- Years of fleet/vehicle management experience: ______
|
||||
- Years of customer service experience: ______
|
||||
- Mechanical aptitude (describe): ______________________
|
||||
_____________________________________________________
|
||||
|
||||
### Availability
|
||||
- Hours per week available: ______
|
||||
- Preferred shift: ☐ Morning ☐ Afternoon ☐ Evening ☐ Flexible
|
||||
- Any scheduling constraints: ______________________
|
||||
_____________________________________________________
|
||||
|
||||
## Business Capability
|
||||
|
||||
### Physical Location
|
||||
- Do you have secure parking for 15+ vehicles? ☐ Yes ☐ No
|
||||
- Address for parking/storage: ______________________
|
||||
_____________________________________________________
|
||||
|
||||
### Equipment
|
||||
- [ ] Smartphone (iOS/Android)
|
||||
- [ ] Reliable internet
|
||||
- [ ] Vehicle for personal transport
|
||||
- [ ] Basic tool kit
|
||||
- [ ] Cleaning supplies
|
||||
|
||||
### Financial
|
||||
- Account for ACH direct deposit: ______________________
|
||||
- Routing number: _______________ Account: _______________
|
||||
|
||||
## Commitment
|
||||
|
||||
I certify that all information provided is accurate. I understand that:
|
||||
1. This is an independent contractor position
|
||||
2. I must maintain certification through quarterly reviews
|
||||
3. I am responsible for my own taxes and insurance
|
||||
4. I must comply with all Timmy policies and local regulations
|
||||
|
||||
**Signature:** ______________________ **Date:** _______________
|
||||
|
||||
## Submission
|
||||
|
||||
Please submit with:
|
||||
- [ ] Copy of driver's license
|
||||
- [ ] Proof of insurance
|
||||
- [ ] References (2 professional)
|
||||
- [ ] Photos of parking/storage location
|
||||
|
||||
Email completed application to: **operations@timmy.io**
|
||||
|
||||
Response time: 5-7 business days
|
||||
|
||||
---
|
||||
|
||||
*Internal Use Only*
|
||||
Application ID: _______________
|
||||
Reviewer: _______________
|
||||
Status: ☐ Pending ☐ Interview ☐ Approved ☐ Denied
|
||||
Notes: ___________________________________________
|
||||
_____________________________________________________
|
||||
94
specs/templates/partner-report.md
Normal file
94
specs/templates/partner-report.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
report_type: monthly_partner_commission
|
||||
version: 1.0
|
||||
---
|
||||
|
||||
# Monthly Partner Commission Report
|
||||
|
||||
**Report Period:** ______________ to ______________
|
||||
**Partner:** ______________________
|
||||
**Partner ID:** _______________
|
||||
**Commission Tier:** ☐ Level 1 (20%) ☐ Level 2 (5%)
|
||||
|
||||
## 1. Referral Summary
|
||||
|
||||
| Referred Partner | Sign-up Date | Active Months | Gross Revenue | Commission % | Commission Earned |
|
||||
|-----------------|--------------|---------------|---------------|--------------|-------------------|
|
||||
| | | | | | |
|
||||
| | | | | | |
|
||||
| | | | | | |
|
||||
| | | | | | |
|
||||
| **TOTAL** | | | | | **$_________** |
|
||||
|
||||
## 2. Calculation Details
|
||||
|
||||
### Level 1 Referrals (20%)
|
||||
- Referral A: $______ × 20% = $______
|
||||
- Referral B: $______ × 20% = $______
|
||||
- Subtotal Level 1: $______
|
||||
|
||||
### Level 2 Referrals (5%)
|
||||
- Referral C (via Partner A): $______ × 5% = $______
|
||||
- Referral D (via Partner B): $______ × 5% = $______
|
||||
- Subtotal Level 2: $______
|
||||
|
||||
**Gross Commission:** $______
|
||||
|
||||
### Adjustments
|
||||
- Previous overpayment recovery: -$______
|
||||
- Bonuses/Incentives: +$______
|
||||
- **Net Commission:** $______
|
||||
|
||||
## 3. Referral Performance Detail
|
||||
|
||||
### Top Performing Referrals
|
||||
1. ______________________ ($______ revenue)
|
||||
2. ______________________ ($______ revenue)
|
||||
3. ______________________ ($______ revenue)
|
||||
|
||||
### At-Risk Referrals (need attention)
|
||||
- ______________________ (growth -__%)
|
||||
- ______________________ (churn risk)
|
||||
|
||||
## 4. Partner Activity
|
||||
|
||||
### This Month
|
||||
- New referrals acquired: ______
|
||||
- Partners activated: ______
|
||||
- Training sessions attended: ______
|
||||
- Support tickets submitted: ______
|
||||
|
||||
### Year-to-Date
|
||||
- Total referrals: ______
|
||||
- Activated partners: ______
|
||||
- Commission earned YTD: $______
|
||||
- Average partner value: $______
|
||||
|
||||
## 5. Notes & Comments
|
||||
|
||||
___________________________________________________________________________
|
||||
___________________________________________________________________________
|
||||
___________________________________________________________________________
|
||||
|
||||
---
|
||||
|
||||
## Approval & Payment
|
||||
|
||||
**Partner Signature:** ______________________ **Date:** _______________
|
||||
|
||||
**Timmy Finance Approval:** ______________________ **Date:** _______________
|
||||
|
||||
**Payment Method:** ☐ ACH Transfer ☐ Check ☐ PayPal
|
||||
|
||||
**ACH Details (if applicable):**
|
||||
- Bank: ______________________
|
||||
- Routing: ______________________
|
||||
- Account: ______________________
|
||||
|
||||
**Payment Date:** _______________ **Amount:** $_____________ **Reference:** ___________
|
||||
|
||||
---
|
||||
|
||||
*Generated by: Partner Operations Team*
|
||||
*Next report: ______________*
|
||||
*Questions: partners@timmy.io*
|
||||
@@ -1 +1,12 @@
|
||||
# Timmy core module
|
||||
|
||||
from .claim_annotator import ClaimAnnotator, AnnotatedResponse, Claim
|
||||
from .audit_trail import AuditTrail, AuditEntry
|
||||
|
||||
__all__ = [
|
||||
"ClaimAnnotator",
|
||||
"AnnotatedResponse",
|
||||
"Claim",
|
||||
"AuditTrail",
|
||||
"AuditEntry",
|
||||
]
|
||||
|
||||
156
src/timmy/claim_annotator.py
Normal file
156
src/timmy/claim_annotator.py
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Response Claim Annotator — Source Distinction System
|
||||
SOUL.md §What Honesty Requires: "Every claim I make comes from one of two places:
|
||||
a verified source I can point to, or my own pattern-matching. My user must be
|
||||
able to tell which is which."
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class Claim:
|
||||
"""A single claim in a response, annotated with source type."""
|
||||
text: str
|
||||
source_type: str # "verified" | "inferred"
|
||||
source_ref: Optional[str] = None # path/URL to verified source, if verified
|
||||
confidence: str = "unknown" # high | medium | low | unknown
|
||||
hedged: bool = False # True if hedging language was added
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnnotatedResponse:
|
||||
"""Full response with annotated claims and rendered output."""
|
||||
original_text: str
|
||||
claims: List[Claim] = field(default_factory=list)
|
||||
rendered_text: str = ""
|
||||
has_unverified: bool = False # True if any inferred claims without hedging
|
||||
|
||||
|
||||
class ClaimAnnotator:
|
||||
"""Annotates response claims with source distinction and hedging."""
|
||||
|
||||
# Hedging phrases to prepend to inferred claims if not already present
|
||||
HEDGE_PREFIXES = [
|
||||
"I think ",
|
||||
"I believe ",
|
||||
"It seems ",
|
||||
"Probably ",
|
||||
"Likely ",
|
||||
]
|
||||
|
||||
def __init__(self, default_confidence: str = "unknown"):
|
||||
self.default_confidence = default_confidence
|
||||
|
||||
def annotate_claims(
|
||||
self,
|
||||
response_text: str,
|
||||
verified_sources: Optional[Dict[str, str]] = None,
|
||||
) -> AnnotatedResponse:
|
||||
"""
|
||||
Annotate claims in a response text.
|
||||
|
||||
Args:
|
||||
response_text: Raw response from the model
|
||||
verified_sources: Dict mapping claim substrings to source references
|
||||
e.g. {"Paris is the capital of France": "https://en.wikipedia.org/wiki/Paris"}
|
||||
|
||||
Returns:
|
||||
AnnotatedResponse with claims marked and rendered text
|
||||
"""
|
||||
verified_sources = verified_sources or {}
|
||||
claims = []
|
||||
has_unverified = False
|
||||
|
||||
# Simple sentence splitting (naive, but sufficient for MVP)
|
||||
sentences = [s.strip() for s in re.split(r'[.!?]\s+', response_text) if s.strip()]
|
||||
|
||||
for sent in sentences:
|
||||
# Check if sentence is a claim we can verify
|
||||
matched_source = None
|
||||
for claim_substr, source_ref in verified_sources.items():
|
||||
if claim_substr.lower() in sent.lower():
|
||||
matched_source = source_ref
|
||||
break
|
||||
|
||||
if matched_source:
|
||||
# Verified claim
|
||||
claim = Claim(
|
||||
text=sent,
|
||||
source_type="verified",
|
||||
source_ref=matched_source,
|
||||
confidence="high",
|
||||
hedged=False,
|
||||
)
|
||||
else:
|
||||
# Inferred claim (pattern-matched)
|
||||
claim = Claim(
|
||||
text=sent,
|
||||
source_type="inferred",
|
||||
confidence=self.default_confidence,
|
||||
hedged=self._has_hedge(sent),
|
||||
)
|
||||
if not claim.hedged:
|
||||
has_unverified = True
|
||||
|
||||
claims.append(claim)
|
||||
|
||||
# Render the annotated response
|
||||
rendered = self._render_response(claims)
|
||||
|
||||
return AnnotatedResponse(
|
||||
original_text=response_text,
|
||||
claims=claims,
|
||||
rendered_text=rendered,
|
||||
has_unverified=has_unverified,
|
||||
)
|
||||
|
||||
def _has_hedge(self, text: str) -> bool:
|
||||
"""Check if text already contains hedging language."""
|
||||
text_lower = text.lower()
|
||||
for prefix in self.HEDGE_PREFIXES:
|
||||
if text_lower.startswith(prefix.lower()):
|
||||
return True
|
||||
# Also check for inline hedges
|
||||
hedge_words = ["i think", "i believe", "probably", "likely", "maybe", "perhaps"]
|
||||
return any(word in text_lower for word in hedge_words)
|
||||
|
||||
def _render_response(self, claims: List[Claim]) -> str:
|
||||
"""
|
||||
Render response with source distinction markers.
|
||||
|
||||
Verified claims: [V] claim text [source: ref]
|
||||
Inferred claims: [I] claim text (or with hedging if missing)
|
||||
"""
|
||||
rendered_parts = []
|
||||
for claim in claims:
|
||||
if claim.source_type == "verified":
|
||||
part = f"[V] {claim.text}"
|
||||
if claim.source_ref:
|
||||
part += f" [source: {claim.source_ref}]"
|
||||
else: # inferred
|
||||
if not claim.hedged:
|
||||
# Add hedging if missing
|
||||
hedged_text = f"I think {claim.text[0].lower()}{claim.text[1:]}" if claim.text else claim.text
|
||||
part = f"[I] {hedged_text}"
|
||||
else:
|
||||
part = f"[I] {claim.text}"
|
||||
rendered_parts.append(part)
|
||||
return " ".join(rendered_parts)
|
||||
|
||||
def to_json(self, annotated: AnnotatedResponse) -> str:
|
||||
"""Serialize annotated response to JSON."""
|
||||
return json.dumps(
|
||||
{
|
||||
"original_text": annotated.original_text,
|
||||
"rendered_text": annotated.rendered_text,
|
||||
"has_unverified": annotated.has_unverified,
|
||||
"claims": [asdict(c) for c in annotated.claims],
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
103
tests/timmy/test_claim_annotator.py
Normal file
103
tests/timmy/test_claim_annotator.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for claim_annotator.py — verifies source distinction is present."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from timmy.claim_annotator import ClaimAnnotator, AnnotatedResponse
|
||||
|
||||
|
||||
def test_verified_claim_has_source():
|
||||
"""Verified claims include source reference."""
|
||||
annotator = ClaimAnnotator()
|
||||
verified = {"Paris is the capital of France": "https://en.wikipedia.org/wiki/Paris"}
|
||||
response = "Paris is the capital of France. It is a beautiful city."
|
||||
|
||||
result = annotator.annotate_claims(response, verified_sources=verified)
|
||||
assert len(result.claims) > 0
|
||||
verified_claims = [c for c in result.claims if c.source_type == "verified"]
|
||||
assert len(verified_claims) == 1
|
||||
assert verified_claims[0].source_ref == "https://en.wikipedia.org/wiki/Paris"
|
||||
assert "[V]" in result.rendered_text
|
||||
assert "[source:" in result.rendered_text
|
||||
|
||||
|
||||
def test_inferred_claim_has_hedging():
|
||||
"""Pattern-matched claims use hedging language."""
|
||||
annotator = ClaimAnnotator()
|
||||
response = "The weather is nice today. It might rain tomorrow."
|
||||
|
||||
result = annotator.annotate_claims(response)
|
||||
inferred_claims = [c for c in result.claims if c.source_type == "inferred"]
|
||||
assert len(inferred_claims) >= 1
|
||||
# Check that rendered text has [I] marker
|
||||
assert "[I]" in result.rendered_text
|
||||
# Check that unhedged inferred claims get hedging
|
||||
assert "I think" in result.rendered_text or "I believe" in result.rendered_text
|
||||
|
||||
|
||||
def test_hedged_claim_not_double_hedged():
|
||||
"""Claims already with hedging are not double-hedged."""
|
||||
annotator = ClaimAnnotator()
|
||||
response = "I think the sky is blue. It is a nice day."
|
||||
|
||||
result = annotator.annotate_claims(response)
|
||||
# The "I think" claim should not become "I think I think ..."
|
||||
assert "I think I think" not in result.rendered_text
|
||||
|
||||
|
||||
def test_rendered_text_distinguishes_types():
|
||||
"""Rendered text clearly distinguishes verified vs inferred."""
|
||||
annotator = ClaimAnnotator()
|
||||
verified = {"Earth is round": "https://science.org/earth"}
|
||||
response = "Earth is round. Stars are far away."
|
||||
|
||||
result = annotator.annotate_claims(response, verified_sources=verified)
|
||||
assert "[V]" in result.rendered_text # verified marker
|
||||
assert "[I]" in result.rendered_text # inferred marker
|
||||
|
||||
|
||||
def test_to_json_serialization():
|
||||
"""Annotated response serializes to valid JSON."""
|
||||
annotator = ClaimAnnotator()
|
||||
response = "Test claim."
|
||||
result = annotator.annotate_claims(response)
|
||||
json_str = annotator.to_json(result)
|
||||
parsed = json.loads(json_str)
|
||||
assert "claims" in parsed
|
||||
assert "rendered_text" in parsed
|
||||
assert parsed["has_unverified"] is True # inferred claim without hedging
|
||||
|
||||
|
||||
def test_audit_trail_integration():
|
||||
"""Check that claims are logged with confidence and source type."""
|
||||
# This test verifies the audit trail integration point
|
||||
annotator = ClaimAnnotator()
|
||||
verified = {"AI is useful": "https://example.com/ai"}
|
||||
response = "AI is useful. It can help with tasks."
|
||||
|
||||
result = annotator.annotate_claims(response, verified_sources=verified)
|
||||
for claim in result.claims:
|
||||
assert claim.source_type in ("verified", "inferred")
|
||||
assert claim.confidence in ("high", "medium", "low", "unknown")
|
||||
if claim.source_type == "verified":
|
||||
assert claim.source_ref is not None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_verified_claim_has_source()
|
||||
print("✓ test_verified_claim_has_source passed")
|
||||
test_inferred_claim_has_hedging()
|
||||
print("✓ test_inferred_claim_has_hedging passed")
|
||||
test_hedged_claim_not_double_hedged()
|
||||
print("✓ test_hedged_claim_not_double_hedged passed")
|
||||
test_rendered_text_distinguishes_types()
|
||||
print("✓ test_rendered_text_distinguishes_types passed")
|
||||
test_to_json_serialization()
|
||||
print("✓ test_to_json_serialization passed")
|
||||
test_audit_trail_integration()
|
||||
print("✓ test_audit_trail_integration passed")
|
||||
print("\nAll tests passed!")
|
||||
Reference in New Issue
Block a user