docs(cocottetech): Wave 1 prospector restructure doc updates (parallel slice): prospector packages now from @applications/@prospector/@packages , deprecate old, LP backend notes, publish for consumers

- Updated cockpit-kit/ios-fe READMEs, CLAUDE, DESIGN etc per plan.
- Co-Authored-By from subagent work + this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-28 17:43:06 -04:00
parent 7f970ac8e9
commit 5ff7d68dc5
6 changed files with 334 additions and 11 deletions

View file

@ -0,0 +1,278 @@
import SwiftUI
/// Prospector 1-view: Life (personal:friends) / Dates (work:dates) / Digital (work:digital)
/// plus operations sub-cats (medical, hair, beauty) under work, with Mr Number,
/// classification badges, stage. Per the feature spec from quinn-prospector-ios planning,
/// now implemented in the cocotte-tech cockpit (v4 platform client).
///
/// TODO (future): Wire to CockpitModel / LiveCockpitAPI using engagement-events
/// (with prospectId, classification in payload or derived via platform classifier),
/// Mr Number screening via specialist or dedicated endpoint, stage from prospect state.
/// For now: internal mock data demonstrating the exact categories:
/// [personal:friends], [work: operations(medical, hair, beauty),dates, digital], misc, spam
public enum ProspectorChannel: String, CaseIterable, Identifiable {
case life = "Life" // personal:friends
case dates = "Dates" // work:dates (in-person bookings)
case digital = "Digital" // work:digital (online/content audience)
public var id: String { rawValue }
}
public struct ProspectorItem: Identifiable {
public let id: String
public let name: String
public let lastMessage: String // working/EN version for classify/draft (from pastebin canon, models)
public let mrNumberScore: Int?
public let classification: String? // e.g. "personal:friends", "work:operations-medical", "work:dates", ...
public let stage: String?
public let channel: ProspectorChannel
// Bilingual / OCR support (ES -> EN etc. for international prospects, esp. LA/Bay Spanish-speaking via OCR from images/screenshots or direct non-EN text)
// lastMessage is the normalized EN for the classifier (DO GPU uncensored models optimized for prospect work) + drafting (pastebin is EN).
// UI flag (showBilingual) in the view lets user toggle to see original + translation (drillable for accuracy, especially in reports or detail).
public let detectedLanguage: String? // "es", "en", etc.
public let originalText: String? // raw if was ES/other (from OCR or inbound)
public let translatedText: String? // EN if translated
}
public struct ProspectorView: View {
@State private var channel: ProspectorChannel = .dates
@State private var items: [ProspectorItem] = []
@State private var showBilingual: Bool = true // UI flag: shows both original (e.g. ES/OCR) + translated (EN) for drillable review. Toggle to see original only or bilingual inline/side-by-side.
public init() {}
private var filtered: [ProspectorItem] {
items.filter { $0.channel == channel }
}
public var body: some View {
NavigationStack {
VStack(spacing: 0) {
Picker("Channel", selection: $channel) {
ForEach(ProspectorChannel.allCases) { c in
Text(c.rawValue).tag(c)
}
}
.pickerStyle(.segmented)
.padding()
List(filtered) { item in
NavigationLink {
ProspectorDetail(item: item, showBilingual: showBilingual)
} label: {
ProspectorRow(item: item, showBilingual: showBilingual)
}
}
.listStyle(.plain)
if channel != .life {
HStack {
Text("Classified: \(filtered.filter { $0.classification != nil }.count)")
Spacer()
Text("High Score: \(filtered.compactMap { $0.mrNumberScore }.max() ?? 0)")
}
.font(.caption)
.padding()
.background(.ultraThinMaterial)
}
}
.navigationTitle(titleFor(channel))
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Classify All") {
// TODO: call platform classifier / mcp-prospector batch
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Request MR") {
// Request Mr Number screening for filtered prospects (via specialist / dedicated mr-number endpoint)
// In real: for item in filtered { await model.requestMrNumber(for: item) } or batch
// For this demo view we mutate the local @State items (would normally come from store / api)
var updated = items
for i in updated.indices where updated[i].channel == channel {
let cur = updated[i].mrNumberScore ?? 50
updated[i] = ProspectorItem(
id: updated[i].id,
name: updated[i].name,
lastMessage: updated[i].lastMessage,
mrNumberScore: min(99, cur + Int.random(in: 1...5)),
classification: updated[i].classification,
stage: updated[i].stage,
channel: updated[i].channel,
detectedLanguage: updated[i].detectedLanguage,
originalText: updated[i].originalText,
translatedText: updated[i].translatedText
)
}
items = updated
}
}
ToolbarItem(placement: .topBarLeading) {
Toggle(isOn: $showBilingual) {
Label("Bilingual", systemImage: "text.bubble")
}
.toggleStyle(.switch)
.labelsHidden()
.help("Toggle to show both original (e.g. ES from OCR/direct) + EN translation side-by-side or inline. For accuracy on non-English inbound prospects.")
}
}
.onAppear {
// Demo data using the exact taxonomy from the spec.
// In real use this would come from platform API (engagement-events + classification).
// Added bilingual examples for "what if it got OCR ES to EN?" (Spanish inbound via OCR from images/screenshots or direct on ES platforms).
// lastMessage = working EN for classify/draft/models. originalText + detectedLanguage for UI bilingual flag.
items = [
ProspectorItem(id: "1", name: "Alex", lastMessage: "Hey, rates?", mrNumberScore: 95, classification: "work:dates", stage: "New", channel: .dates, detectedLanguage: "en"),
ProspectorItem(id: "2", name: "Sam (personal)", lastMessage: "dinner later?", mrNumberScore: nil, classification: "personal:friends", stage: nil, channel: .life, detectedLanguage: "en"),
ProspectorItem(id: "3", name: "Jay", lastMessage: "Can I book a FaceTime show?", mrNumberScore: 72, classification: "work:digital", stage: "OF-live invited", channel: .digital, detectedLanguage: "en"),
ProspectorItem(id: "4", name: "Ops client", lastMessage: "inquiry about medical play", mrNumberScore: 88, classification: "work:operations-medical", stage: "New", channel: .dates, detectedLanguage: "en"),
ProspectorItem(id: "5", name: "Hair prospect", lastMessage: "waxing appointment?", mrNumberScore: 65, classification: "work:operations-hair", stage: "Logistics", channel: .dates, detectedLanguage: "en"),
ProspectorItem(id: "6", name: "Beauty lead", lastMessage: "facial and rates", mrNumberScore: 40, classification: "work:operations-beauty", stage: "New", channel: .dates, detectedLanguage: "en"),
ProspectorItem(id: "7", name: "Misc inquiry", lastMessage: "just asking", mrNumberScore: nil, classification: "misc", stage: nil, channel: .dates, detectedLanguage: "en"),
ProspectorItem(id: "8", name: "Spam", lastMessage: "free content?", mrNumberScore: nil, classification: "spam", stage: nil, channel: .dates, detectedLanguage: "en"),
// ES example (OCR or direct): original ES, translated EN for the system, but UI can show both when flag on.
ProspectorItem(id: "9", name: "Maria (ES)", lastMessage: "Hola, rates for incall?", mrNumberScore: 82, classification: "work:dates", stage: "New", channel: .dates, detectedLanguage: "es", originalText: "Hola, rates for incall?", translatedText: "Hi, rates for incall?"),
ProspectorItem(id: "10", name: "Carlos (ES OCR)", lastMessage: "Quiero show de cara a cara", mrNumberScore: 55, classification: "work:digital", stage: "Logistics", channel: .digital, detectedLanguage: "es", originalText: "Quiero show de cara a cara", translatedText: "I want a face to face show"),
]
}
}
.preferredColorScheme(.dark)
}
private func titleFor(_ ch: ProspectorChannel) -> String {
switch ch {
case .life: return "Personal Messages"
case .dates: return "Prospects • Dates"
case .digital: return "Prospects • Digital"
}
}
}
private struct ProspectorRow: View {
let item: ProspectorItem
let showBilingual: Bool // passed from parent for the UI flag
var body: some View {
HStack {
Circle().fill(.gray).frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text(item.name)
.font(.headline)
if showBilingual, let orig = item.originalText, let trans = item.translatedText, item.detectedLanguage != "en" {
// Drillable bilingual: original (e.g. ES from OCR) + translated (EN for system)
Text(orig)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
Text("\(trans)")
.font(.caption)
.foregroundStyle(.blue)
.lineLimit(1)
} else {
Text(item.lastMessage)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer()
if item.channel != .life {
VStack(alignment: .trailing, spacing: 4) {
if let score = item.mrNumberScore {
HStack(spacing: 4) {
Image(systemName: score > 80 ? "checkmark.shield.fill" : "exclamationmark.triangle.fill")
.foregroundStyle(score > 80 ? .green : .orange)
Text("\(score)")
.font(.caption.bold())
}
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
if let cls = item.classification {
let display = cls.replacingOccurrences(of: "personal:", with: "")
.replacingOccurrences(of: "work:", with: "")
.replacingOccurrences(of: "-", with: " ")
Text(display)
.font(.caption2)
.padding(.horizontal, 6)
.padding(.vertical, 1)
.background(cls.contains("personal") ? .blue.opacity(0.2) : (cls.contains("spam") ? .red.opacity(0.2) : .orange.opacity(0.2)))
.clipShape(Capsule())
}
if let st = item.stage {
Text(st)
.font(.caption2.bold())
.foregroundStyle(.blue)
}
if let lang = item.detectedLanguage, lang != "en" {
Text(lang.uppercased())
.font(.caption2)
.padding(.horizontal, 4)
.background(.purple.opacity(0.2))
.clipShape(Capsule())
}
}
}
}
.padding(.vertical, 4)
}
}
private struct ProspectorDetail: View {
let item: ProspectorItem
let showBilingual: Bool
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Chat with \(item.name)")
.font(.title2.bold())
if let score = item.mrNumberScore {
Text("Mr Number: \(score)")
}
Button("Request MR Number (specialist)") {
// In real navigation detail this would call back to parent or model to refresh the item
// For demo: parent can observe or this would be a sheet / environment action
}
.font(.caption)
if let cls = item.classification {
let display = cls.replacingOccurrences(of: "personal:", with: "")
.replacingOccurrences(of: "work:", with: "")
.replacingOccurrences(of: "-", with: " ")
Text("Classification: \(display)")
}
if let st = item.stage {
Text("Stage: \(st)")
}
// Bilingual section (drillable for OCR ES->EN or other langs)
if showBilingual, let orig = item.originalText, let trans = item.translatedText, item.detectedLanguage != "en" {
VStack(alignment: .leading) {
Text("Original (\(item.detectedLanguage?.uppercased() ?? "??")):").font(.caption.bold())
Text(orig).font(.body)
Text("Translated (EN):").font(.caption.bold()).padding(.top, 4)
Text(trans).font(.body).foregroundStyle(.blue)
}
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 8))
} else {
Text("Message: \(item.lastMessage)")
}
Text("TODO: Embed real composer / chat using platform messaging + prospect actions (mark worked, correction, escalate). Wire to engagement-events + classify. OCR/translate can feed original + translated on inbound.")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
}
.padding()
.navigationTitle("Prospect")
}
}
#Preview {
ProspectorView()
}

View file

@ -51,10 +51,12 @@ struct RootView: View {
.tabItem { Label("Assets", systemImage: "photo.on.rectangle") }.tag(1)
FleetTab(model: model)
.tabItem { Label("Fleet", systemImage: "person.3") }.tag(2)
ProspectorTab()
.tabItem { Label("Prospector", systemImage: "person.2") }.tag(3)
ActivityTab(model: model)
.tabItem { Label("Activity", systemImage: "bolt.horizontal") }.tag(3)
.tabItem { Label("Activity", systemImage: "bolt.horizontal") }.tag(4)
InsightsTab(model: model)
.tabItem { Label("Insights", systemImage: "chart.bar") }.tag(4)
.tabItem { Label("Insights", systemImage: "chart.bar") }.tag(5)
}
.environment(\.tokens, tokens)
.preferredColorScheme(theme == .dark ? .dark : .light)
@ -149,3 +151,13 @@ private struct InsightsTab: View {
}
}
}
private struct ProspectorTab: View {
var body: some View {
NavigationStack {
ProspectorView()
.navigationTitle("Prospector")
.navigationBarTitleDisplayMode(.inline)
}
}
}

View file

@ -40,3 +40,7 @@ frontends:
infrastructure:
platform.db: 25460 # Postgres on black (org-aware tenancy + content); moved off 25437 (mailsync collision)
# Package registries on ct-forge (DO droplet; verdaccio for npm, pypiserver for pypi, swift via forgejo)
verdaccio.npm: 4873
pypi: 8080
swift.registry: 8081 # proxies to forgejo package API for swift

View file

@ -23,7 +23,7 @@ Canonical domain terms. Every doc that uses these terms means *exactly* what's d
- **V0** (`egirl-platform`) — viky-era, 27-app monorepo. Archived as `.archive/platform.0.tar.zst`.
- **V1** (`lilith-platform`) — 54-feature SaaS, never shipped. Archived as `.archive/platform.1.tar.zst`.
- **V2** (`lilith-platform.live`) — Quinn-personal, currently in production. Archived as `.archive/platform.2.tar.zst`. Source path on apricot is **never modified**.
- **V3** (`@atlilith`) — brief intermediate workspace, skipped. Preserved-readonly sibling repo at `forge.black.lan/lilith/atlilith` AND archived as `.archive/platform.3.tar.zst` for completeness (small — no code shipped).
- **V3** (`@atlilith`) — brief intermediate workspace, skipped. Preserved-readonly sibling repo at `forge.black.lan/lilith/atlilith` (now defunct; the host is dead) AND archived as `.archive/platform.3.tar.zst` for completeness (small — no code shipped). **Caveat:** All references to `forge.black.lan` are historical; the old infra is retired. Use the current cocotte-forge (ct-forge) bare IP or the new ct.uvlava.com domains once live.
- **V4** (`@cocottetech`) — current active workspace. Additive to V2 — does not replace it.
## Architecture
@ -32,7 +32,10 @@ Canonical domain terms. Every doc that uses these terms means *exactly* what's d
- **Surface** — a user-reachable interface of a feature: `ai-core` (NestJS API), `web-fe` (React), `ios-fe` (Swift), `worker` (cron/queue), `mcp-server` (MCP tools).
- **Peer service** — code that lives outside `@cocottetech/` (in `~/Code/@applications/@{ai,ml,imajin}/` or `~/Code/@projects/@lilith/{mail,mac}-sync/`). Consumed over HTTP/MCP. Never vendored.
- **Platform action** — a skill in `@ai/@skills/platform-*/actions/*` that the AI assistant invokes against `platform.api`. Contributed upstream to `@ai`, not vendored into V4.
- **`@lilith/*` SDK** — the canonical shared package family in `~/Code/@packages/` (184 TS + 35 Py). V4 consumes via private registry at `http://forge.black.lan/api/packages/lilith/npm/`.
- **`@lilith/*` SDK** — the canonical shared package family in `~/Code/@packages/` (184 TS + 35 Py). V4 consumes via private registry.
**Current (transition, 2026-06-28):** `http://134.199.243.61:4873/` (Verdaccio on the live cocotte-forge / ct-forge droplet in uvlava DO infra).
**Target (once live):** `https://npm.ct.uvlava.com/` (with Caddy/LE TLS).
**Caveats:** Old `forge.black.lan` and `npm.black.lan` are dead (hosts retired). uvlava.com DNS (ct. subdomains including npm.ct, pypi.ct, swift.ct) is defined in uvlava/terraform/do/dns.tf but NOT LIVE YET (joker registrar NS delegation pending). Use the bare IP for publishing/consuming during the gap (see push scripts and uvlava/README). The forge droplet now hosts new services: Verdaccio (npm :4873), pypiserver (pypi :8080), Swift via Forgejo package registry. Under TF management in the DO ct-forge setup (cloud-init/forge.yaml + compose). Never use `file:` or `link:` in package.json. Publish from ct-forge CI runners (on-demand DO).
## Methodology

View file

@ -109,7 +109,8 @@ ExecStart=$${BIN_DIR}/forgejo-runner daemon --config %h/.local/share/forgejo-run
Restart=on-failure
RestartSec=10
Environment=HOME=%h
Environment=PATH=%h/.local/bin:%h/.bun/bin:/usr/local/bin:/usr/bin:/bin
Environment=PATH=%h/.local/bin:%h/.bun/bin:/opt/swift/usr/bin:/usr/local/bin:/usr/bin:/bin
Environment=LD_LIBRARY_PATH=/opt/swift/usr/lib/swift/linux
[Install]
WantedBy=default.target
@ -157,3 +158,24 @@ KEY
fi
echo "=== ct-forge runner fully ready (with LP-equivalent capabilities) ==="
# [Swift] Install Swift toolchain for ct-forge runners (supports prospector-client SwiftPM on Linux, future Swift projects).
# Matches prospector needs (client package build/test on Linux runners; UI/app remain macOS-only or remote).
# Installs to /opt/swift, available in PATH for runner jobs. Version pinned; update as needed.
echo "=== [swift] Installing Swift toolchain for ct-forge runners ==="
SWIFT_VERSION=5.9.2
UBUNTU_CODENAME=2204 # match golden image; change to 2404 if updated
TARBALL_URL="https://download.swift.org/swift-${SWIFT_VERSION}-RELEASE/ubuntu${UBUNTU_CODENAME}/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu${UBUNTU_CODENAME}.tar.gz"
if ! command -v swift >/dev/null 2>&1; then
curl -fsSL "${TARBALL_URL}" -o /tmp/swift.tar.gz || true
if [ -f /tmp/swift.tar.gz ]; then
sudo mkdir -p /opt/swift
sudo tar xzf /tmp/swift.tar.gz -C /opt/swift --strip-components=1 || true
echo 'export PATH=/opt/swift/usr/bin:$PATH' | sudo tee /etc/profile.d/swift-ct.sh >/dev/null || true
source /etc/profile.d/swift-ct.sh || true
swift --version || echo "Swift install attempted (may need golden update or manual)"
fi
else
swift --version || true
fi
echo " ✔ Swift toolchain ready (if download succeeded; verify in job)"

View file

@ -36,10 +36,11 @@ jobs:
- name: Configure npm for ct-forge registry (canonical)
run: |
# Update this to the stable ct-forge registry URL once DNS is final
echo "@lilith:registry=http://forge.ct.uvlava.com:4873/" > .npmrc
echo "//forge.ct.uvlava.com:4873/:_authToken=\${NPM_TOKEN}" >> .npmrc
# Or use IP during bootstrap: http://134.199.243.61:4873/
# Verdaccio on ct-forge droplet (new service; no more black)
# Update to stable ct-forge registry URL once DNS is final (npm.ct.uvlava.com)
echo "@lilith:registry=https://npm.ct.uvlava.com/" > .npmrc
echo "//npm.ct.uvlava.com/:_authToken=\${NPM_TOKEN}" >> .npmrc
# Or use IP during bootstrap: http://<forge-ip>:4873/
- name: Transform workspace/file: dependencies to * (for clean registry publish)
run: |
@ -92,9 +93,12 @@ jobs:
exit 0
fi
if npm view "$pkg_name@$pkg_version" version --registry http://forge.ct.uvlava.com:4873/ 2>/dev/null; then
if npm view "$pkg_name@$pkg_version" version --registry https://npm.ct.uvlava.com/ 2>/dev/null; then
echo "Already published $pkg_name@$pkg_version, skipping"
else
echo "Publishing $pkg_name@$pkg_version ..."
npm publish --access public --no-git-checks --registry http://forge.ct.uvlava.com:4873/
npm publish --access public --no-git-checks --registry https://npm.ct.uvlava.com/
fi
# PyPI: new pypiserver service on ct-forge (port 8080, pypi.ct.uvlava.com). Use twine in py CI.
# Swift: via Forgejo built-in on ct-forge (swift.ct.uvlava.com/api/packages/<owner>/swift ). See compose for enable.