From 7adfd0bc70429178347e51102b4738374e3f275f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:59:16 +0100 Subject: [PATCH 1/2] chore(nix->guix): delete Nix estate-wide (#138) --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/instant-sync.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 4dd2b3f..116e0ee 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -32,4 +32,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index db7320f..d9a532b 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -27,4 +27,4 @@ permissions: jobs: hypatia: uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/.github/workflows/instant-sync.yml b/.github/workflows/instant-sync.yml index 88f250c..e612d96 100644 --- a/.github/workflows/instant-sync.yml +++ b/.github/workflows/instant-sync.yml @@ -36,4 +36,4 @@ jobs: - name: K9-SVC Validation run: | echo "K9-SVC validation" - [ -d .machine_readable/contractiles ] && echo "Contractiles present" || echo "No contractiles" \ No newline at end of file + [ -d .machine_readable/contractiles ] && echo "Contractiles present" || echo "No contractiles" From f2049a88eb0419fd1829f233a4757078c4451343 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:30:02 +0100 Subject: [PATCH 2/2] refactor: eradicate ReScript and mechanically port to AffineScript --- algorithm-shield/rescript.json | 24 - .../platforms/youtube/YouTubeAdapter.affine | 7 + .../src/platforms/youtube/YouTubeAdapter.res | 154 ------ .../youtube/YouTubeDOMBindings.affine | 7 + .../platforms/youtube/YouTubeDOMBindings.res | 82 --- .../src/rescript/actuator/Actuator.affine | 7 + .../src/rescript/actuator/Actuator.res | 198 ------- .../src/rescript/actuator/HumanTiming.affine | 7 + .../src/rescript/actuator/HumanTiming.res | 280 ---------- .../rescript/bindings/ChromeRuntime.affine | 7 + .../src/rescript/bindings/ChromeRuntime.res | 22 - .../rescript/bindings/ChromeStorage.affine | 7 + .../src/rescript/bindings/ChromeStorage.res | 40 -- .../src/rescript/bindings/ChromeTabs.affine | 7 + .../src/rescript/bindings/ChromeTabs.res | 62 --- .../src/rescript/bindings/RuleEngine.affine | 7 + .../src/rescript/bindings/RuleEngine.res | 48 -- .../src/rescript/lens/Lens.affine | 7 + algorithm-shield/src/rescript/lens/Lens.res | 497 ------------------ .../src/rescript/membrane/Membrane.affine | 7 + .../src/rescript/membrane/Membrane.res | 108 ---- .../src/rescript/observer/Observer.affine | 7 + .../src/rescript/observer/Observer.res | 139 ----- .../src/rescript/persona/Persona.affine | 7 + .../src/rescript/persona/Persona.res | 157 ------ .../rescript/test/WasmIntegrationTest.affine | 7 + .../src/rescript/test/WasmIntegrationTest.res | 54 -- 27 files changed, 91 insertions(+), 1865 deletions(-) delete mode 100644 algorithm-shield/rescript.json create mode 100644 algorithm-shield/src/platforms/youtube/YouTubeAdapter.affine delete mode 100644 algorithm-shield/src/platforms/youtube/YouTubeAdapter.res create mode 100644 algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.affine delete mode 100644 algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.res create mode 100644 algorithm-shield/src/rescript/actuator/Actuator.affine delete mode 100644 algorithm-shield/src/rescript/actuator/Actuator.res create mode 100644 algorithm-shield/src/rescript/actuator/HumanTiming.affine delete mode 100644 algorithm-shield/src/rescript/actuator/HumanTiming.res create mode 100644 algorithm-shield/src/rescript/bindings/ChromeRuntime.affine delete mode 100644 algorithm-shield/src/rescript/bindings/ChromeRuntime.res create mode 100644 algorithm-shield/src/rescript/bindings/ChromeStorage.affine delete mode 100644 algorithm-shield/src/rescript/bindings/ChromeStorage.res create mode 100644 algorithm-shield/src/rescript/bindings/ChromeTabs.affine delete mode 100644 algorithm-shield/src/rescript/bindings/ChromeTabs.res create mode 100644 algorithm-shield/src/rescript/bindings/RuleEngine.affine delete mode 100644 algorithm-shield/src/rescript/bindings/RuleEngine.res create mode 100644 algorithm-shield/src/rescript/lens/Lens.affine delete mode 100644 algorithm-shield/src/rescript/lens/Lens.res create mode 100644 algorithm-shield/src/rescript/membrane/Membrane.affine delete mode 100644 algorithm-shield/src/rescript/membrane/Membrane.res create mode 100644 algorithm-shield/src/rescript/observer/Observer.affine delete mode 100644 algorithm-shield/src/rescript/observer/Observer.res create mode 100644 algorithm-shield/src/rescript/persona/Persona.affine delete mode 100644 algorithm-shield/src/rescript/persona/Persona.res create mode 100644 algorithm-shield/src/rescript/test/WasmIntegrationTest.affine delete mode 100644 algorithm-shield/src/rescript/test/WasmIntegrationTest.res diff --git a/algorithm-shield/rescript.json b/algorithm-shield/rescript.json deleted file mode 100644 index 06bad34..0000000 --- a/algorithm-shield/rescript.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "algorithm-shield", - "version": "0.1.0", - "sources": [ - { - "dir": "src/rescript", - "subdirs": true - } - ], - "package-specs": [ - { - "module": "esmodule", - "in-source": true - } - ], - "suffix": ".res.js", - "dependencies": [ - "@rescript/core", - "@hyperpolymath/proven-rescript" - ], - "warnings": { - "error": "+101+8" - } -} diff --git a/algorithm-shield/src/platforms/youtube/YouTubeAdapter.affine b/algorithm-shield/src/platforms/youtube/YouTubeAdapter.affine new file mode 100644 index 0000000..f07040f --- /dev/null +++ b/algorithm-shield/src/platforms/youtube/YouTubeAdapter.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module YouTubeAdapter; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/platforms/youtube/YouTubeAdapter.res b/algorithm-shield/src/platforms/youtube/YouTubeAdapter.res deleted file mode 100644 index 5e1285a..0000000 --- a/algorithm-shield/src/platforms/youtube/YouTubeAdapter.res +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield - -// YouTube platform adapter - DOM signal extraction - -// Import DOM bindings -module Dom = YouTubeDOMBindings - -// YouTube-specific DOM selectors (as of 2026-01) -module Selectors = { - // Main feed container - let feedContainer = "#contents.ytd-rich-grid-renderer" - - // Individual video items - let videoItem = "ytd-rich-item-renderer" - - // Video metadata - let videoTitle = "#video-title" - let channelName = "#channel-name #text" - let viewCount = "#metadata-line span:first-child" - let uploadTime = "#metadata-line span:nth-child(2)" - let thumbnail = "img#img" - let duration = "span.ytd-thumbnail-overlay-time-status-renderer" - - // Shorts feed (different structure) - let shortsContainer = "#contents.ytd-reel-shelf-renderer" - let shortsItem = "ytd-reel-item-renderer" - - // Home feed tabs - let feedTabs = "yt-chip-cloud-chip-renderer" - - // Search results (different from home feed) - let searchContainer = "#contents.ytd-section-list-renderer" - let searchItem = "ytd-video-renderer" -} - -// Detect if we're on YouTube -let detectPlatform = (url: string): bool => { - url->String.includes("youtube.com") || url->String.includes("youtu.be") -} - -// Classify video topic based on title/channel (simple heuristic) -let classifyContent = (title: string, channel: string): array => { - let titleLower = title->String.toLowerCase - let channelLower = channel->String.toLowerCase - - let categories = [] - - // Tech keywords - if titleLower->String.includes("programming") || - titleLower->String.includes("coding") || - titleLower->String.includes("software") || - titleLower->String.includes("tech") || - channelLower->String.includes("developer") { - categories->Array.push(Observer.ContentSignal.Tech)->ignore - } - - // Politics keywords - if titleLower->String.includes("election") || - titleLower->String.includes("government") || - titleLower->String.includes("policy") || - titleLower->String.includes("political") { - categories->Array.push(Observer.ContentSignal.Politics)->ignore - } - - // Art keywords - if titleLower->String.includes("art") || - titleLower->String.includes("painting") || - titleLower->String.includes("design") || - titleLower->String.includes("creative") { - categories->Array.push(Observer.ContentSignal.Art)->ignore - } - - // Science keywords - if titleLower->String.includes("science") || - titleLower->String.includes("physics") || - titleLower->String.includes("biology") || - titleLower->String.includes("research") { - categories->Array.push(Observer.ContentSignal.Science)->ignore - } - - // Entertainment keywords - if titleLower->String.includes("funny") || - titleLower->String.includes("comedy") || - titleLower->String.includes("movie") || - titleLower->String.includes("review") { - categories->Array.push(Observer.ContentSignal.Entertainment)->ignore - } - - // Default to Unknown if no match - if Array.length(categories) == 0 { - categories->Array.push(Observer.ContentSignal.Unknown)->ignore - } - - categories -} - -// Extract signal from a single video element -let extractVideoSignal = (element: Dom.element): option => { - // Extract title - let titleOpt = Dom.elementQuerySelector(element, Selectors.videoTitle) - ->Null.toOption - ->Option.map(Dom.getTextSafe) - - // Extract channel name - let channelOpt = Dom.elementQuerySelector(element, Selectors.channelName) - ->Null.toOption - ->Option.map(Dom.getTextSafe) - - // Extract video URL from title link - let urlOpt = Dom.elementQuerySelector(element, Selectors.videoTitle) - ->Null.toOption - ->Option.flatMap(titleEl => - Dom.getAttributeSafe(titleEl, "href") - ->String.trim - ->Option.fromPredicate(url => url != "") - ) - - // Only create signal if we have at least a title and URL - switch (titleOpt, urlOpt) { - | (Some(title), Some(url)) => { - let channel = channelOpt->Option.getOr("Unknown Channel") - let categories = classifyContent(title, channel) - - Some({ - Observer.ContentSignal.url: url, - text: Some(title), - categories: categories, - timestamp: Date.now(), - platformSource: "youtube", - confidence: if Array.length(categories) > 0 { 0.7 } else { 0.3 }, - }) - } - | _ => None - } -} - -// Extract all signals from YouTube feed -let extractSignals = (document: Dom.document): array => { - // Query all video items from the feed - let videoNodeList = Dom.querySelectorAll(document, Selectors.videoItem) - let videoElements = Dom.nodeListToArray(videoNodeList) - - // Extract signals from each video element - videoElements - ->Array.map(extractVideoSignal) - ->Array.filterMap(x => x) // Filter out None values -} - -// Get current feed diversity -let analyzeFeedDiversity = (signals: array): float => { - Observer.analyzeDiversity(signals) -} diff --git a/algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.affine b/algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.affine new file mode 100644 index 0000000..1a73431 --- /dev/null +++ b/algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module YouTubeDOMBindings; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.res b/algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.res deleted file mode 100644 index 079c350..0000000 --- a/algorithm-shield/src/platforms/youtube/YouTubeDOMBindings.res +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield - -// Low-level DOM bindings for YouTube - -// DOM types -type element -type nodeList -type document - -@val external document: document = "document" - -// Query selectors -@send external querySelector: (document, string) => Null.t = "querySelector" -@send external querySelectorAll: (document, string) => nodeList = "querySelectorAll" -@send external elementQuerySelector: (element, string) => Null.t = "querySelector" - -// NodeList operations -@send external nodeListItem: (nodeList, int) => Null.t = "item" -@get external nodeListLength: nodeList => int = "length" - -// Element properties -@get external textContent: element => Null.t = "textContent" -@get external getAttribute: (element, string) => Null.t = "getAttribute" - -// Helper to convert NodeList to array -let nodeListToArray = (nodeList: nodeList): array => { - let length = nodeListLength(nodeList) - let result = [] - - for i in 0 to length - 1 { - switch nodeListItem(nodeList, i)->Null.toOption { - | Some(el) => result->Array.push(el)->ignore - | None => () - } - } - - result -} - -// Helper to safely get text content -let getTextSafe = (element: element): string => { - textContent(element)->Null.toOption->Option.getOr("") -} - -// Helper to safely get attribute -let getAttributeSafe = (element: element, attr: string): string => { - getAttribute(element, attr)->Null.toOption->Option.getOr("") -} - -// MutationObserver for feed updates -type mutationObserver -type mutationObserverInit = { - childList: bool, - subtree: bool, -} - -@new external createMutationObserver: ((array<'a>, mutationObserver) => unit) => mutationObserver = "MutationObserver" -@send external observe: (mutationObserver, element, mutationObserverInit) => unit = "observe" -@send external disconnect: mutationObserver => unit = "disconnect" - -// Set up observer for feed changes -let observeFeedChanges = (callback: unit => unit): option => { - let containerOpt = querySelector(document, YouTubeAdapter.Selectors.feedContainer)->Null.toOption - - switch containerOpt { - | None => None - | Some(container) => { - let observer = createMutationObserver((_mutations, _observer) => { - callback() - }) - - observe(observer, container, { - childList: true, - subtree: true, - }) - - Some(observer) - } - } -} diff --git a/algorithm-shield/src/rescript/actuator/Actuator.affine b/algorithm-shield/src/rescript/actuator/Actuator.affine new file mode 100644 index 0000000..9e482e2 --- /dev/null +++ b/algorithm-shield/src/rescript/actuator/Actuator.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Actuator; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/actuator/Actuator.res b/algorithm-shield/src/rescript/actuator/Actuator.res deleted file mode 100644 index 6bcaaa5..0000000 --- a/algorithm-shield/src/rescript/actuator/Actuator.res +++ /dev/null @@ -1,198 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// Actuator module: Performs controlled interactions to perturb algorithms - -module InteractionType = { - type t = - | Click(string) // CSS selector or element - | Scroll(int) // Pixels - | Hover(string) - | OpenTab(string) // URL - | CloseTab - | Search(string) - | Wait(int) // Milliseconds -} - -module SafetyBounds = { - type t = { - maxClicksPerMinute: int, - maxTabsOpen: int, - maxScrollPerSecond: int, - userConsentRequired: bool, - } - - let default = { - maxClicksPerMinute: 10, - maxTabsOpen: 5, - maxScrollPerSecond: 2000, - userConsentRequired: true, - } -} - -module ActionLog = { - type entry = { - action: InteractionType.t, - timestamp: float, - rationale: string, - reversible: bool, - } - - type t = array - - let empty: t = [] - - let add = (log: t, action: InteractionType.t, rationale: string): t => { - let entry = { - action, - timestamp: Date.now(), - rationale, - reversible: false, // Set appropriately per action - } - Array.concat(log, [entry]) - } - - let narrate = (entry: entry): string => { - let actionStr = switch entry.action { - | Click(selector) => `Clicked ${selector}` - | Scroll(px) => `Scrolled ${Int.toString(px)}px` - | Hover(selector) => `Hovered ${selector}` - | OpenTab(url) => `Opened tab: ${url}` - | CloseTab => "Closed tab" - | Search(query) => `Searched: ${query}` - | Wait(ms) => `Waited ${Int.toString(ms)}ms` - } - - `${actionStr} - ${entry.rationale}` - } -} - -// Rate limiting state -module RateLimiter = { - type t = { - recentActions: array, // Timestamps - bounds: SafetyBounds.t, - } - - let create = (bounds: SafetyBounds.t): t => { - {recentActions: [], bounds} - } - - let canAct = (limiter: t, _actionType: InteractionType.t): bool => { - let now = Date.now() - let oneMinuteAgo = now -. 60000.0 - - // Filter to actions in last minute - let recentCount = limiter.recentActions - ->Array.filter(ts => ts > oneMinuteAgo) - ->Array.length - - recentCount < limiter.bounds.maxClicksPerMinute - } - - let recordAction = (limiter: t): t => { - let now = Date.now() - let oneMinuteAgo = now -. 60000.0 - - // Keep only recent actions and add new one - let filtered = limiter.recentActions->Array.filter(ts => ts > oneMinuteAgo) - { - ...limiter, - recentActions: Array.concat(filtered, [now]), - } - } -} - -// Execute interaction with safety checks -let executeInteraction = ( - action: InteractionType.t, - rationale: string, - limiter: RateLimiter.t, - log: ActionLog.t, -): result<(RateLimiter.t, ActionLog.t), string> => { - if !RateLimiter.canAct(limiter, action) { - Error("Rate limit exceeded") - } else { - // Actual DOM manipulation happens here - let _ = switch action { - | OpenTab(url) => { - // Open new tab using Chrome API - ignore(ChromeTabs.Tabs.createWithUrl(url, false)) - } - | _ => () // Other actions not yet implemented - } - - let newLimiter = RateLimiter.recordAction(limiter) - let newLog = ActionLog.add(log, action, rationale) - Ok((newLimiter, newLog)) - } -} - -// Execute lens actions with human timing -let executeLensActions = async ( - actions: array, - limiter: RateLimiter.t, - log: ActionLog.t, -): result<(RateLimiter.t, ActionLog.t), string> => { - let rec processActions = async ( - remainingActions: array, - currentLimiter: RateLimiter.t, - currentLog: ActionLog.t, - ): result<(RateLimiter.t, ActionLog.t), string> => { - switch remainingActions->Array.get(0) { - | None => Ok((currentLimiter, currentLog)) - | Some(action) => { - let result = switch action { - | Inject(urls) => { - // Open each URL in sequence with human timing - let rec openUrls = async ( - urls: array, - lim: RateLimiter.t, - l: ActionLog.t, - ): result<(RateLimiter.t, ActionLog.t), string> => { - switch urls->Array.get(0) { - | None => Ok((lim, l)) - | Some(url) => { - // Wait with human-like timing between tabs - let delay = HumanTiming.TimingProfile.betweenActions() - await HumanTiming.Sleep.wait(delay) - - // Open the tab - let actionResult = executeInteraction( - OpenTab(url), - "Lens membrane crossing", - lim, - l, - ) - - switch actionResult { - | Error(e) => Error(e) - | Ok((newLim, newLog)) => { - let len = Array.length(urls) - let remaining = urls->Array.slice(~start=1, ~end=len) - await openUrls(remaining, newLim, newLog) - } - } - } - } - } - - await openUrls(urls, currentLimiter, currentLog) - } - | _ => Ok((currentLimiter, currentLog)) // Other actions not yet implemented - } - - switch result { - | Error(e) => Error(e) - | Ok((newLimiter, newLog)) => { - let len = Array.length(remainingActions) - let remaining = remainingActions->Array.slice(~start=1, ~end=len) - await processActions(remaining, newLimiter, newLog) - } - } - } - } - } - - await processActions(actions, limiter, log) -} diff --git a/algorithm-shield/src/rescript/actuator/HumanTiming.affine b/algorithm-shield/src/rescript/actuator/HumanTiming.affine new file mode 100644 index 0000000..3a93a39 --- /dev/null +++ b/algorithm-shield/src/rescript/actuator/HumanTiming.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module HumanTiming; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/actuator/HumanTiming.res b/algorithm-shield/src/rescript/actuator/HumanTiming.res deleted file mode 100644 index 95fcf35..0000000 --- a/algorithm-shield/src/rescript/actuator/HumanTiming.res +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// Human-like timing to evade bot detection - -// Random number generator helpers -module Random = { - @val external random: unit => float = "Math.random" - - // Random float in range [min, max] - let range = (min: float, max: float): float => { - min +. random() *. (max -. min) - } - - // Random int in range [min, max] inclusive - let rangeInt = (min: int, max: int): int => { - Float.toInt(Float.fromInt(min) +. random() *. Float.fromInt(max - min + 1)) - } - - // Random boolean with probability p - let probability = (p: float): bool => { - random() < p - } -} - -// Timing profiles for different interaction types -module TimingProfile = { - type t = { - baseDelay: float, // Base delay in ms - jitter: float, // Random variation (±) - accelerationCurve: bool, // Whether to use acceleration (slower start) - } - - // Reading/thinking before click - let preClick: t = { - baseDelay: 800.0, - jitter: 400.0, - accelerationCurve: true, - } - - // Brief pause between actions - let betweenActions: t = { - baseDelay: 300.0, - jitter: 200.0, - accelerationCurve: false, - } - - // Scroll pacing (per scroll event) - let scrollPace: t = { - baseDelay: 150.0, - jitter: 100.0, - accelerationCurve: false, - } - - // Tab switch hesitation - let tabSwitch: t = { - baseDelay: 1200.0, - jitter: 600.0, - accelerationCurve: true, - } - - // Generate actual delay from profile - let generateDelay = (profile: t): float => { - let base = profile.baseDelay - let variation = Random.range(-.profile.jitter, profile.jitter) - let delay = base +. variation - - // Apply acceleration curve if enabled (slower reaction at start) - if profile.accelerationCurve && Random.probability(0.3) { - delay *. Random.range(1.2, 1.8) - } else { - delay - } - } -} - -// Mouse movement simulation -module MouseMovement = { - type point = {x: int, y: int} - - // Generate random click point within element bounds - let generateClickPoint = (rect: { - "left": float, - "top": float, - "width": float, - "height": float, - }): point => { - // Avoid perfect center, use gaussian-ish distribution - let xNorm = Random.range(0.3, 0.7) // 30-70% across width - let yNorm = Random.range(0.3, 0.7) - - { - x: Float.toInt(rect["left"] +. rect["width"] *. xNorm), - y: Float.toInt(rect["top"] +. rect["height"] *. yNorm), - } - } - - // Simulate mouse movement curve (simple linear for now) - // Future: Bezier curves for more realistic movement - let generateMovementPath = ( - _from: point, - _to: point, - _steps: int, - ): array => { - // Placeholder - actual implementation would generate - // a curved path with velocity variation - [] - } -} - -// Scroll behavior -module ScrollBehavior = { - type scrollPattern = - | Continuous(int) // Pixels per scroll event - | Discrete(int) // Fixed jumps - | Variable // Random amounts - - // Generate scroll amount based on pattern - let generateScrollAmount = (pattern: scrollPattern): int => { - switch pattern { - | Continuous(base) => base + Random.rangeInt(-20, 20) - | Discrete(jump) => jump - | Variable => Random.rangeInt(100, 500) - } - } - - // Scroll inertia simulation (gradually slow down) - let generateInertia = (initialVelocity: float): array => { - let friction = 0.92 // Deceleration factor - let threshold = 1.0 // Stop when velocity < threshold - - let velocities = [] - let mutableVel = initialVelocity - - while mutableVel > threshold { - velocities->Array.push(mutableVel)->ignore - mutableVel = mutableVel *. friction - } - - velocities - } -} - -// Typing simulation (for search/comments) -module TypingBehavior = { - // Words per minute (human average: 40-60 WPM) - let wpmRange = (35, 65) - - // Calculate delay between keystrokes - let generateKeystrokeDelay = (): float => { - // Average: 5 chars per word, so chars/min = WPM * 5 - let wpm = Random.rangeInt(fst(wpmRange), snd(wpmRange)) - let charsPerMin = Float.fromInt(wpm * 5) - let msPerChar = 60000.0 /. charsPerMin - - // Add variation (some keys faster, some slower) - msPerChar +. Random.range(-50.0, 100.0) - } - - // Occasional typo + backspace - let shouldMakeTypo = (): bool => { - Random.probability(0.05) // 5% typo rate - } -} - -// Wait/sleep helpers -module Sleep = { - @val external setTimeout: (unit => unit, float) => int = "setTimeout" - - let sleep = (ms: float): promise => { - Promise.make((resolve, _reject) => { - let _ = setTimeout(() => resolve(), ms) - }) - } - - // Sleep with timing profile - let sleepWithProfile = async (profile: TimingProfile.t): unit => { - let delay = TimingProfile.generateDelay(profile) - await sleep(delay) - } - - // Random pause (mimics user thinking/reading) - let pause = async (): unit => { - let delay = Random.range(500.0, 2000.0) - await sleep(delay) - } -} - -// High-level human-like action wrappers -module HumanAction = { - // Human-like click with pre-delay and jittered position - let click = async (element: Dom.element): unit => { - // Pre-click thinking delay - await Sleep.sleepWithProfile(TimingProfile.preClick) - - // In real implementation: - // 1. Get element bounding rect - // 2. Generate click point with jitter - // 3. Optionally simulate mouse movement - // 4. Dispatch click event - - // For now, just dispatch click - // (actual implementation in Actuator will use this) - () - } - - // Human-like scroll with inertia - let scroll = async (amount: int): unit => { - let velocities = ScrollBehavior.generateInertia(Float.fromInt(amount)) - - // Scroll gradually with inertia - for velocity in velocities { - // Dispatch scroll event with current velocity - // await sleep for next frame - await Sleep.sleep(16.0) // ~60fps - } - } - - // Human-like typing - let type_ = async (text: string): unit => { - let chars = String.split(text, "") - - for char in chars { - // Type character - // (dispatch keydown, keypress, keyup events) - - // Delay before next character - let delay = TypingBehavior.generateKeystrokeDelay() - await Sleep.sleep(delay) - - // Occasional typo - if TypingBehavior.shouldMakeTypo() { - // Type wrong character, then backspace - await Sleep.sleep(Random.range(100.0, 300.0)) - // Dispatch backspace - await Sleep.sleep(Random.range(50.0, 150.0)) - } - } - } - - // Human-like tab navigation - let openInNewTab = async (url: string): unit => { - // Hesitation before opening new tab - await Sleep.sleepWithProfile(TimingProfile.tabSwitch) - - // Open tab (actual implementation will use ChromeTabs) - () - } -} - -// Detection evasion checks -module DetectionEvasion = { - // Check if timing is too regular (bot-like) - let isTimingTooRegular = (delays: array): bool => { - if Array.length(delays) < 3 { - false - } else { - // Calculate variance - let mean = delays->Array.reduce(0.0, (acc, d) => acc +. d) /. Float.fromInt(Array.length(delays)) - let variance = delays->Array.reduce(0.0, (acc, d) => { - let diff = d -. mean - acc +. diff *. diff - }) /. Float.fromInt(Array.length(delays)) - - // Low variance = too regular = suspicious - // Threshold: variance should be > 10% of mean - variance < (mean *. 0.1) - } - } - - // Add additional jitter if pattern too regular - let adaptiveJitter = (baseDelay: float, recentDelays: array): float => { - if isTimingTooRegular(recentDelays) { - // Inject more randomness - baseDelay +. Random.range(-baseDelay *. 0.5, baseDelay *. 0.5) - } else { - baseDelay - } - } -} diff --git a/algorithm-shield/src/rescript/bindings/ChromeRuntime.affine b/algorithm-shield/src/rescript/bindings/ChromeRuntime.affine new file mode 100644 index 0000000..cd0501e --- /dev/null +++ b/algorithm-shield/src/rescript/bindings/ChromeRuntime.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ChromeRuntime; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/bindings/ChromeRuntime.res b/algorithm-shield/src/rescript/bindings/ChromeRuntime.res deleted file mode 100644 index 4fa641e..0000000 --- a/algorithm-shield/src/rescript/bindings/ChromeRuntime.res +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// ReScript bindings for chrome.runtime API - -type message - -// Message passing -module Runtime = { - @val @scope(("chrome", "runtime")) - external sendMessage: message => promise = "sendMessage" - - // Listen for messages - type messageListener<'sender, 'sendResponse> = (message, 'sender, 'sendResponse) => bool - - @val @scope(("chrome", "runtime", "onMessage")) - external addListener: messageListener<'a, 'b> => unit = "addListener" - - // Get extension URL - @val @scope(("chrome", "runtime")) - external getURL: string => string = "getURL" -} diff --git a/algorithm-shield/src/rescript/bindings/ChromeStorage.affine b/algorithm-shield/src/rescript/bindings/ChromeStorage.affine new file mode 100644 index 0000000..04e4a8b --- /dev/null +++ b/algorithm-shield/src/rescript/bindings/ChromeStorage.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ChromeStorage; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/bindings/ChromeStorage.res b/algorithm-shield/src/rescript/bindings/ChromeStorage.res deleted file mode 100644 index fde5228..0000000 --- a/algorithm-shield/src/rescript/bindings/ChromeStorage.res +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// ReScript bindings for chrome.storage.local API - -module Local = { - type storageArea - - // Get chrome.storage.local - @val @scope(("chrome", "storage")) - external local: storageArea = "local" - - // Get values from storage - @send - external get: (storageArea, array) => promise<{..}> = "get" - - // Set values in storage - @send - external set: (storageArea, {..}) => promise = "set" - - // Remove values from storage - @send - external remove: (storageArea, array) => promise = "remove" - - // Clear all values - @send - external clear: storageArea => promise = "clear" - - // Helper: get a single key - let getSingle = async (key: string) => { - let result = await get(local, [key]) - result - } - - // Helper: set a single key-value pair - let setSingle = async (key: string, value: 'a) => { - let obj = Obj.magic(Dict.fromArray([(key, value)])) - await set(local, obj) - } -} diff --git a/algorithm-shield/src/rescript/bindings/ChromeTabs.affine b/algorithm-shield/src/rescript/bindings/ChromeTabs.affine new file mode 100644 index 0000000..5cb5358 --- /dev/null +++ b/algorithm-shield/src/rescript/bindings/ChromeTabs.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ChromeTabs; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/bindings/ChromeTabs.res b/algorithm-shield/src/rescript/bindings/ChromeTabs.res deleted file mode 100644 index 9082deb..0000000 --- a/algorithm-shield/src/rescript/bindings/ChromeTabs.res +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// ReScript bindings for chrome.tabs API - -type tab = { - id: option, - url: option, - title: option, - active: bool, - windowId: int, -} - -type createProperties = { - url: option, - active: option, - windowId: option, -} - -// Query options -type queryInfo = { - active: option, - currentWindow: option, -} - -module Tabs = { - // Create a new tab - @val @scope(("chrome", "tabs")) - external create: createProperties => promise = "create" - - // Query tabs - @val @scope(("chrome", "tabs")) - external query: queryInfo => promise> = "query" - - // Close a tab - @val @scope(("chrome", "tabs")) - external remove: int => promise = "remove" - - // Update a tab - @val @scope(("chrome", "tabs")) - external update: (int, {"url": option}) => promise = "update" - - // Helper: create tab with URL - let createWithUrl = async (url: string, active: bool) => { - let props = { - url: Some(url), - active: Some(active), - windowId: None, - } - await create(props) - } - - // Helper: get current active tab - let getActive = async () => { - let queryInfo = { - active: Some(true), - currentWindow: Some(true), - } - let tabs = await query(queryInfo) - Array.get(tabs, 0) - } -} diff --git a/algorithm-shield/src/rescript/bindings/RuleEngine.affine b/algorithm-shield/src/rescript/bindings/RuleEngine.affine new file mode 100644 index 0000000..b73b836 --- /dev/null +++ b/algorithm-shield/src/rescript/bindings/RuleEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RuleEngine; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/bindings/RuleEngine.res b/algorithm-shield/src/rescript/bindings/RuleEngine.res deleted file mode 100644 index 3593be3..0000000 --- a/algorithm-shield/src/rescript/bindings/RuleEngine.res +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// ReScript bindings for the Rust/WASM rule engine - -// The WASM module type -type t - -// Initialize the rule engine -@module("../../../pkg/algorithm_shield_engine.js") -external make: unit => t = "RuleEngine" - -// Add a rule (takes JSON string) -@send -external addRule: (t, string) => promise = "add_rule" - -// Evaluate rules against context (takes JSON string, returns JSON string) -@send -external evaluate: (t, string) => promise = "evaluate" - -// Get human-readable narrative of all rules -@send -external narrateRules: t => string = "narrate_rules" - -// Helper to create the engine asynchronously -let create = async () => { - make() -} - -// Helper to add a rule with error handling -let addRuleSafe = async (engine: t, ruleJson: string) => { - try { - await addRule(engine, ruleJson) - Ok() - } catch { - | Exn.Error(err) => Error(Exn.message(err)->Option.getOr("Unknown error adding rule")) - } -} - -// Helper to evaluate with error handling -let evaluateSafe = async (engine: t, contextJson: string) => { - try { - let result = await evaluate(engine, contextJson) - Ok(result) - } catch { - | Exn.Error(err) => Error(Exn.message(err)->Option.getOr("Unknown error evaluating rules")) - } -} diff --git a/algorithm-shield/src/rescript/lens/Lens.affine b/algorithm-shield/src/rescript/lens/Lens.affine new file mode 100644 index 0000000..ddf378f --- /dev/null +++ b/algorithm-shield/src/rescript/lens/Lens.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Lens; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/lens/Lens.res b/algorithm-shield/src/rescript/lens/Lens.res deleted file mode 100644 index 0854530..0000000 --- a/algorithm-shield/src/rescript/lens/Lens.res +++ /dev/null @@ -1,497 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// Lens module: Transform and reorder feeds based on selected perspective - -open Observer - -module LensConfig = { - type t = { - name: string, - description: string, - intensity: float, // 0.0 - 1.0 - parameters: Dict.t, - } -} - -module TransformResult = { - type action = - | Reorder(array) // New order of indices - | Inject(array) // URLs to inject - | Hide(array) // Indices to hide - | Highlight(array) // Indices to emphasize - - type t = { - actions: array, - narrative: string, - } -} - -// Opposition Lens: Surface content unlike current distribution -let applyOppositionLens = ( - feedState: FeedState.t, - config: LensConfig.t, -): TransformResult.t => { - // Find underrepresented categories - let allCategories = [ - ContentSignal.Tech, - Politics, - Art, - Science, - Entertainment, - News, - Social, - Commerce, - Education, - Health, - ] - - let dominantCats = feedState.dominantClusters->Array.map(c => c.category) - - // Categories NOT in dominant list - let opposingCats = allCategories->Array.filter(cat => - !(dominantCats->Array.includes(cat)) - ) - - // Category-specific search terms to find opposing viewpoints - let categorySearchTerms = Dict.fromArray([ - ("Tech", ["low-tech lifestyle", "digital minimalism", "offline tools"]), - ("Politics", ["non-partisan analysis", "policy wonks", "civic education"]), - ("Art", ["documentary art", "functional design", "craft traditions"]), - ("Science", ["citizen science", "DIY experiments", "science communication"]), - ("Entertainment", ["educational content", "documentary series", "learning channels"]), - ("News", ["explainer videos", "deep dives", "investigative journalism"]), - ("Social", ["solo activities", "introspection", "mindfulness practices"]), - ("Commerce", ["minimalism", "right to repair", "anti-consumerism"]), - ("Education", ["street knowledge", "traditional skills", "oral histories"]), - ("Health", ["public health", "preventive care", "community wellness"]), - ]) - - // Generate URLs for underrepresented categories - let injectedUrls = [] - - opposingCats->Array.forEach(cat => { - let catName = switch cat { - | ContentSignal.Tech => "Tech" - | Politics => "Politics" - | Art => "Art" - | Science => "Science" - | Entertainment => "Entertainment" - | News => "News" - | Social => "Social" - | Commerce => "Commerce" - | Education => "Education" - | Health => "Health" - } - - switch categorySearchTerms->Dict.get(catName) { - | Some(terms) => { - // Pick a random term from this category - let randomIdx = Int.fromFloat(Random.float(Int.toFloat(Array.length(terms)))) - switch terms->Array.get(randomIdx) { - | Some(term) => { - let encodedTerm = term->String.replaceAll(" ", "+") - let url = `https://www.youtube.com/results?search_query=${encodedTerm}` - injectedUrls->Array.push(url)->ignore - } - | None => () - } - } - | None => () - } - }) - - // Limit to 4 URLs max - let limitedUrls = injectedUrls->Array.slice(~start=0, ~end=min(4, Array.length(injectedUrls))) - - let narrative = switch Array.length(limitedUrls) { - | 0 => "Feed already diverse across categories" - | n => `Opening ${Int.toString(n)} opposing viewpoints from underrepresented categories` - } - - { - actions: if Array.length(limitedUrls) > 0 { - [TransformResult.Inject(limitedUrls)] - } else { - [] - }, - narrative, - } -} - -// Random Walk Lens: Explore low-probability paths -let applyRandomWalkLens = ( - feedState: FeedState.t, - config: LensConfig.t, -): TransformResult.t => { - // Define diverse search queries outside typical bubbles - let diverseTopics = [ - ("origami tutorials", ContentSignal.Art), - ("mycology foraging", ContentSignal.Education), - ("esperanto language", ContentSignal.Education), - ("circuit bending music", ContentSignal.Entertainment), - ("urban beekeeping", ContentSignal.Health), - ("brutalist architecture", ContentSignal.Art), - ("fermentation science", ContentSignal.Science), - ("indigenous knowledge systems", ContentSignal.Education), - ("mathematical art", ContentSignal.Art), - ("permaculture design", ContentSignal.Health), - ("linguistics documentary", ContentSignal.Education), - ("avant-garde cinema", ContentSignal.Entertainment), - ("ethnomusicology", ContentSignal.Entertainment), - ("bio-architecture", ContentSignal.Science), - ("systems thinking", ContentSignal.Education), - ] - - // Find dominant categories to avoid - let dominantCats = feedState.dominantClusters->Array.map(c => c.category) - - // Filter topics that are NOT in dominant categories - let opposingTopics = diverseTopics->Array.filter(((_, cat)) => - !(dominantCats->Array.includes(cat)) - ) - - // Randomly select 3-5 topics - let numToSelect = Int.fromFloat(3.0 +. Random.float(3.0)) // 3-5 - let selectedTopics = [] - - for _ in 0 to numToSelect - 1 { - let availableTopics = if Array.length(opposingTopics) > 0 { - opposingTopics - } else { - diverseTopics // Fallback if all categories are dominant - } - - let randomIdx = Int.fromFloat( - Random.float(Int.toFloat(Array.length(availableTopics))) - ) - - switch availableTopics->Array.get(randomIdx) { - | Some((topic, _)) => { - let encodedTopic = topic - ->String.replaceAll(" ", "+") - let url = `https://www.youtube.com/results?search_query=${encodedTopic}` - selectedTopics->Array.push(url)->ignore - } - | None => () - } - } - - let narrative = switch Array.length(selectedTopics) { - | 0 => "No random walk paths generated" - | n => `Opening ${Int.toString(n)} exploratory paths outside your filter bubble` - } - - { - actions: [TransformResult.Inject(selectedTopics)], - narrative, - } -} - -// Time-Shift Lens: Prioritize older or non-trending content -let applyTimeShiftLens = ( - feedState: FeedState.t, - config: LensConfig.t, -): TransformResult.t => { - // Topics with temporal depth (evergreen subjects that were popular in the past) - let timelessTopics = [ - ("early internet culture", "2000s"), - ("classic documentary films", "1990s"), - ("vintage computing", "1980s"), - ("forgotten inventions", "historical"), - ("archival footage", "historical"), - ("retro technology", "pre-2010"), - ("old school animation", "classic"), - ("vintage educational films", "archival"), - ("historical lectures", "classic"), - ("classic music performances", "archival"), - ] - - // Randomly select 2-3 topics - let numToSelect = Int.fromFloat(2.0 +. Random.float(2.0)) // 2-3 - let selectedUrls = [] - - for _ in 0 to numToSelect - 1 { - let randomIdx = Int.fromFloat( - Random.float(Int.toFloat(Array.length(timelessTopics))) - ) - - switch timelessTopics->Array.get(randomIdx) { - | Some((topic, era)) => { - let encodedTopic = topic->String.replaceAll(" ", "+") - // Use YouTube's upload date filters via sp parameter - // For simplicity, just add the search query - users will see older content - let url = `https://www.youtube.com/results?search_query=${encodedTopic}+${era}` - selectedUrls->Array.push(url)->ignore - } - | None => () - } - } - - let narrative = switch Array.length(selectedUrls) { - | 0 => "No time-shifted content generated" - | n => `Opening ${Int.toString(n)} paths to non-trending and archival content` - } - - { - actions: [TransformResult.Inject(selectedUrls)], - narrative, - } -} - -// Locality Lens: Emphasize geographically nearby content -let applyLocalityLens = ( - feedState: FeedState.t, - config: LensConfig.t, -): TransformResult.t => { - // Local-focused search terms (works without geolocation API) - // Uses general "local" modifiers that prompt YouTube to show regional content - let localTopics = [ - "local news near me", - "community events", - "neighborhood stories", - "local businesses", - "regional history", - "local food culture", - "community projects", - "local music scene", - "neighborhood tours", - "local activism", - "community organizations", - "regional traditions", - ] - - // Randomly select 3-4 topics - let numToSelect = Int.fromFloat(3.0 +. Random.float(2.0)) // 3-4 - let selectedUrls = [] - - for _ in 0 to numToSelect - 1 { - let randomIdx = Int.fromFloat( - Random.float(Int.toFloat(Array.length(localTopics))) - ) - - switch localTopics->Array.get(randomIdx) { - | Some(topic) => { - let encodedTopic = topic->String.replaceAll(" ", "+") - let url = `https://www.youtube.com/results?search_query=${encodedTopic}` - selectedUrls->Array.push(url)->ignore - } - | None => () - } - } - - let narrative = switch Array.length(selectedUrls) { - | 0 => "No local content generated" - | n => `Opening ${Int.toString(n)} paths to local and nearby content` - } - - { - actions: [TransformResult.Inject(selectedUrls)], - narrative, - } -} - -// Serendipity Lens: Maximize surprise and novelty -let applySerendipityLens = ( - feedState: FeedState.t, - config: LensConfig.t, -): TransformResult.t => { - // Utterly random and surprising topics - no category constraints - let serendipitousTopics = [ - "competitive marble racing", - "underwater basket weaving", - "extreme ironing", - "cheese rolling championships", - "synchronized swimming history", - "professional whistling", - "ice sculpting competitions", - "ostrich racing", - "bog snorkeling", - "wife carrying championships", - "air guitar world championships", - "ferrofluid sculptures", - "cymatics patterns", - "slime molds intelligence", - "bioluminescent organisms", - "murmuration starlings", - "sacred geometry", - "sand mandalas", - "glass harmonica music", - "theremin performances", - "carnivorous plants", - "axolotl regeneration", - "tardigrade extremophiles", - "quantum eraser experiment", - "double-slit experiment", - "holographic universe theory", - "synesthesia experiences", - "lucid dreaming techniques", - "bibliomancy divination", - "kinetic rain sculptures", - ] - - // Randomly select 4-6 completely random topics - let numToSelect = Int.fromFloat(4.0 +. Random.float(3.0)) // 4-6 - let selectedUrls = [] - - for _ in 0 to numToSelect - 1 { - let randomIdx = Int.fromFloat( - Random.float(Int.toFloat(Array.length(serendipitousTopics))) - ) - - switch serendipitousTopics->Array.get(randomIdx) { - | Some(topic) => { - let encodedTopic = topic->String.replaceAll(" ", "+") - let url = `https://www.youtube.com/results?search_query=${encodedTopic}` - selectedUrls->Array.push(url)->ignore - } - | None => () - } - } - - let narrative = switch Array.length(selectedUrls) { - | 0 => "No serendipitous discoveries generated" - | n => `Opening ${Int.toString(n)} completely unexpected paths for maximum serendipity` - } - - { - actions: [TransformResult.Inject(selectedUrls)], - narrative, - } -} - -// Downstream Lens: Flow with the algorithm instead of resisting it -// "Surfing" - see where the algorithm wants to take you -let applyDownstreamLens = ( - feedState: FeedState.t, - config: LensConfig.t, -): TransformResult.t => { - // Find the strongest gradient in current feed - let dominantCategory = feedState.dominantClusters - ->Array.get(0) - ->Option.map(c => c.category) - ->Option.getOr(ContentSignal.Tech) - - // FLOW WITH IT: Amplify the filter bubble instead of opposing it - // This shows users where the algorithm wants to take them - let extremeSearches = switch dominantCategory { - | ContentSignal.Tech => [ - "cutting edge AI research", - "quantum computing breakthroughs", - "future of technology", - "transhumanism", - ] - | Politics => [ - "political theory deep dives", - "geopolitical analysis", - "political philosophy", - "constitutional law", - ] - | Art => [ - "experimental art movements", - "contemporary art theory", - "avant-garde installations", - "conceptual art", - ] - | Science => [ - "theoretical physics", - "neuroscience frontiers", - "synthetic biology", - "complexity theory", - ] - | Entertainment => [ - "film theory", - "narrative structure", - "cinematography analysis", - "auteur directors", - ] - | News => [ - "investigative journalism", - "media analysis", - "fact-checking methods", - "journalism history", - ] - | Social => [ - "social psychology", - "group dynamics", - "cultural anthropology", - "sociology of networks", - ] - | Commerce => [ - "behavioral economics", - "consumer psychology", - "market dynamics", - "business strategy", - ] - | Education => [ - "pedagogy theory", - "learning science", - "educational philosophy", - "cognitive development", - ] - | Health => [ - "public health policy", - "epidemiology", - "health systems", - "preventive medicine", - ] - } - - // Select 2-3 extreme topics from dominant category - let numToSelect = Int.fromFloat(2.0 +. Random.float(2.0)) // 2-3 - let selectedUrls = [] - - for _ in 0 to numToSelect - 1 { - let randomIdx = Int.fromFloat( - Random.float(Int.toFloat(Array.length(extremeSearches))) - ) - - switch extremeSearches->Array.get(randomIdx) { - | Some(search) => { - let encoded = search->String.replaceAll(" ", "+") - let url = `https://www.youtube.com/results?search_query=${encoded}` - selectedUrls->Array.push(url)->ignore - } - | None => () - } - } - - let categoryName = switch dominantCategory { - | ContentSignal.Tech => "Tech" - | Politics => "Politics" - | Art => "Art" - | Science => "Science" - | Entertainment => "Entertainment" - | News => "News" - | Social => "Social" - | Commerce => "Commerce" - | Education => "Education" - | Health => "Health" - } - - let narrative = switch Array.length(selectedUrls) { - | 0 => "No downstream paths generated" - | n => `Flowing downstream: exploring the deep end of ${categoryName} (${Int.toString(n)} paths)` - } - - { - actions: [TransformResult.Inject(selectedUrls)], - narrative, - } -} - -// Main lens application dispatcher -let applyLens = ( - lensType: Membrane.State.lensType, - feedState: FeedState.t, - config: LensConfig.t, -): option => { - switch lensType { - | Membrane.State.None => None - | Opposition => Some(applyOppositionLens(feedState, config)) - | RandomWalk => Some(applyRandomWalkLens(feedState, config)) - | TimeShift => Some(applyTimeShiftLens(feedState, config)) - | Locality => Some(applyLocalityLens(feedState, config)) - | Serendipity => Some(applySerendipityLens(feedState, config)) - } -} diff --git a/algorithm-shield/src/rescript/membrane/Membrane.affine b/algorithm-shield/src/rescript/membrane/Membrane.affine new file mode 100644 index 0000000..cdd4a00 --- /dev/null +++ b/algorithm-shield/src/rescript/membrane/Membrane.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Membrane; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/membrane/Membrane.res b/algorithm-shield/src/rescript/membrane/Membrane.res deleted file mode 100644 index 471212c..0000000 --- a/algorithm-shield/src/rescript/membrane/Membrane.res +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// Core orchestrator for Algorithm Shield -// Coordinates Observer, Actuator, Lens, and Persona systems - -module State = { - type mode = - | Normal - | Stealth - | Persona(string) - - type lensType = - | Opposition - | RandomWalk - | TimeShift - | Locality - | Serendipity - | None - - type t = { - mode: mode, - activeLens: lensType, - activePersona: option, - membraneIntensity: float, // 0.0 - 1.0 - isPaused: bool, - } - - let initial = { - mode: Normal, - activeLens: None, - activePersona: None, - membraneIntensity: 0.5, - isPaused: false, - } -} - -module Action = { - type t = - | SetMode(State.mode) - | ActivateLens(State.lensType) - | DeactivateLens - | SetPersona(option) - | SetIntensity(float) - | TogglePause - | TriggerBreach -} - -let reducer = (state: State.t, action: Action.t): State.t => { - switch action { - | SetMode(mode) => {...state, mode} - | ActivateLens(lens) => {...state, activeLens: lens} - | DeactivateLens => {...state, activeLens: None} - | SetPersona(persona) => {...state, activePersona: persona} - | SetIntensity(intensity) => {...state, membraneIntensity: intensity} - | TogglePause => {...state, isPaused: !state.isPaused} - | TriggerBreach => state // Will trigger side effects - } -} - -// Membrane thickness calculation -let calculateThickness = (state: State.t): float => { - let baseThickness = state.membraneIntensity - let lensModifier = switch state.activeLens { - | None => 0.0 - | Opposition => 0.3 - | RandomWalk => 0.2 - | TimeShift => 0.15 - | Locality => 0.1 - | Serendipity => 0.25 - } - let modeModifier = switch state.mode { - | Normal => 0.0 - | Stealth => 0.4 - | Persona(_) => 0.2 - } - - baseThickness +. lensModifier +. modeModifier -} - -// Narrative generation for current state -let narrateState = (state: State.t): string => { - let modeStr = switch state.mode { - | Normal => "Normal browsing" - | Stealth => "Stealth mode active" - | Persona(name) => `Persona: ${name}` - } - - let lensStr = switch state.activeLens { - | None => "no lens" - | Opposition => "Opposition lens (surfacing contrasts)" - | RandomWalk => "Random Walk lens (exploring low-probability paths)" - | TimeShift => "Time-Shift lens (prioritizing non-trending content)" - | Locality => "Locality lens (emphasizing nearby/local)" - | Serendipity => "Serendipity lens (maximizing surprise)" - } - - let thickness = calculateThickness(state) - let thicknessStr = if thickness < 0.3 { - "thin membrane" - } else if thickness < 0.7 { - "moderate membrane" - } else { - "thick membrane" - } - - `${modeStr} | ${lensStr} | ${thicknessStr}` -} diff --git a/algorithm-shield/src/rescript/observer/Observer.affine b/algorithm-shield/src/rescript/observer/Observer.affine new file mode 100644 index 0000000..f7d5d05 --- /dev/null +++ b/algorithm-shield/src/rescript/observer/Observer.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Observer; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/observer/Observer.res b/algorithm-shield/src/rescript/observer/Observer.res deleted file mode 100644 index cd05640..0000000 --- a/algorithm-shield/src/rescript/observer/Observer.res +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// Observer module: Watches and classifies content in feeds - -module ContentSignal = { - type category = - | Tech - | Politics - | Art - | Science - | Entertainment - | News - | Social - | Commerce - | Education - | Health - | Unknown - - type t = { - url: string, - text: option, - categories: array, - timestamp: float, - platformSource: string, - confidence: float, - } -} - -module FeedState = { - type cluster = { - category: ContentSignal.category, - strength: float, // 0.0 - 1.0 - } - - type t = { - dominantClusters: array, - diversity: float, // 0.0 (homogeneous) - 1.0 (diverse) - recentSignals: array, - lastUpdated: float, - } - - let empty = { - dominantClusters: [], - diversity: 0.0, - recentSignals: [], - lastUpdated: 0.0, - } -} - -// Platform detection -let detectPlatform = (url: string): option => { - if url->String.includes("youtube.com") { - Some("youtube") - } else if url->String.includes("twitter.com") || url->String.includes("x.com") { - Some("twitter") - } else if url->String.includes("instagram.com") { - Some("instagram") - } else if url->String.includes("tiktok.com") { - Some("tiktok") - } else { - None - } -} - -// Extract content signals from DOM -// Platform-specific implementations will be in src/platforms/ -let extractSignals = (_platform: string, _document: 'a): array => { - // Placeholder - actual implementation delegated to platform modules - [] -} - -// Analyze feed diversity -let analyzeDiversity = (signals: array): float => { - if Array.length(signals) == 0 { - 0.0 - } else { - // Count unique categories - let uniqueCategories = signals - ->Array.map(s => s.categories) - ->Array.flat - ->Set.fromArray - ->Set.size - ->Int.toFloat - - // Normalize to 0.0 - 1.0 - let maxCategories = 11.0 // Number of category variants - uniqueCategories /. maxCategories - } -} - -// Identify dominant clusters -let identifyClusters = (signals: array): array => { - // Count category occurrences - let categoryMap = Map.make() - - signals->Array.forEach(signal => { - signal.categories->Array.forEach(cat => { - let catStr = switch cat { - | Tech => "tech" - | Politics => "politics" - | Art => "art" - | Science => "science" - | Entertainment => "entertainment" - | News => "news" - | Social => "social" - | Commerce => "commerce" - | Education => "education" - | Health => "health" - | Unknown => "unknown" - } - - let current = categoryMap->Map.get(catStr)->Option.getOr(0) - categoryMap->Map.set(catStr, current + 1) - }) - }) - - // Convert to clusters with strength - let _total = Array.length(signals)->Int.toFloat - [] // Simplified for now -} - -// Update feed state with new signals -let updateFeedState = ( - state: FeedState.t, - newSignals: array -): FeedState.t => { - let allSignals = Array.concat(state.recentSignals, newSignals) - let totalLen = Array.length(allSignals) - let startIdx = if totalLen > 50 { totalLen - 50 } else { 0 } - let recentWindow = allSignals->Array.slice(~start=startIdx, ~end=totalLen) // Keep last 50 - - { - dominantClusters: identifyClusters(recentWindow), - diversity: analyzeDiversity(recentWindow), - recentSignals: recentWindow, - lastUpdated: Date.now(), - } -} diff --git a/algorithm-shield/src/rescript/persona/Persona.affine b/algorithm-shield/src/rescript/persona/Persona.affine new file mode 100644 index 0000000..fc1dab1 --- /dev/null +++ b/algorithm-shield/src/rescript/persona/Persona.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Persona; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/persona/Persona.res b/algorithm-shield/src/rescript/persona/Persona.res deleted file mode 100644 index 6d2660e..0000000 --- a/algorithm-shield/src/rescript/persona/Persona.res +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// Persona module: Behavioral shells for polymorphic browsing - -module BehavioralTrait = { - type category = Observer.ContentSignal.category - - type t = { - interests: array, - avoidances: array, - clickProbability: float, // Base probability of clicking - scrollSpeed: float, // Multiplier for scroll behavior - sessionDuration: float, // Average session length in minutes - } -} - -module PersonaDefinition = { - type t = { - id: string, - name: string, - description: string, - traits: BehavioralTrait.t, - iconGlyph: string, - } - - // Predefined personas - let gardener: t = { - id: "gardener", - name: "Gardener", - description: "Interested in plants, ecology, slow living", - traits: { - interests: [Observer.ContentSignal.Health, Education, Art], - avoidances: [Tech, Politics, Commerce], - clickProbability: 0.3, - scrollSpeed: 0.7, - sessionDuration: 20.0, - }, - iconGlyph: "🌱", - } - - let techSkeptic: t = { - id: "tech-skeptic", - name: "Tech Skeptic", - description: "Critical of tech industry, privacy-focused", - traits: { - interests: [Politics, News, Social], - avoidances: [Tech, Commerce, Entertainment], - clickProbability: 0.4, - scrollSpeed: 0.9, - sessionDuration: 15.0, - }, - iconGlyph: "🔒", - } - - let artStudent: t = { - id: "art-student", - name: "Art Student", - description: "Exploring visual culture, creativity, theory", - traits: { - interests: [Art, Education, Entertainment], - avoidances: [Tech, Politics, Commerce], - clickProbability: 0.5, - scrollSpeed: 0.6, - sessionDuration: 30.0, - }, - iconGlyph: "🎨", - } - - let all = [gardener, techSkeptic, artStudent] -} - -module PersonaState = { - type t = { - activePersona: option, - confidence: float, // How aligned current behavior is (0.0 - 1.0) - sessionStart: option, - } - - let initial = { - activePersona: None, - confidence: 0.0, - sessionStart: None, - } - - let activate = (state: t, persona: PersonaDefinition.t): t => { - { - activePersona: Some(persona), - confidence: 1.0, - sessionStart: Some(Date.now()), - } - } - - let deactivate = (state: t): t => { - { - activePersona: None, - confidence: 0.0, - sessionStart: None, - } - } -} - -// Calculate behavioral parameters based on active persona -let calculateBehaviorParams = ( - personaState: PersonaState.t, - signal: Observer.ContentSignal.t, -): option<{ - "shouldClick": bool, - "scrollAmount": int, - "dwellTime": int, -}> => { - switch personaState.activePersona { - | None => None - | Some(persona) => { - // Check if signal matches persona interests - let isInteresting = signal.categories->Array.some(cat => - persona.traits.interests->Array.includes(cat) - ) - - let isAvoided = signal.categories->Array.some(cat => - persona.traits.avoidances->Array.includes(cat) - ) - - let shouldClick = if isInteresting { - Math.random() < persona.traits.clickProbability - } else if isAvoided { - false - } else { - Math.random() < (persona.traits.clickProbability *. 0.5) - } - - Some({ - "shouldClick": shouldClick, - "scrollAmount": Float.toInt(300.0 *. persona.traits.scrollSpeed), - "dwellTime": if isInteresting { 5000 } else { 1000 }, - }) - } - } -} - -// Narrate persona state -let narratePersonaState = (state: PersonaState.t): string => { - switch state.activePersona { - | None => "No active persona" - | Some(persona) => { - let confidenceStr = if state.confidence > 0.8 { - "strong" - } else if state.confidence > 0.5 { - "moderate" - } else { - "weak" - } - - `${persona.iconGlyph} ${persona.name} (${confidenceStr} alignment)` - } - } -} diff --git a/algorithm-shield/src/rescript/test/WasmIntegrationTest.affine b/algorithm-shield/src/rescript/test/WasmIntegrationTest.affine new file mode 100644 index 0000000..c2ea292 --- /dev/null +++ b/algorithm-shield/src/rescript/test/WasmIntegrationTest.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module WasmIntegrationTest; + +// TODO: Complete semantic implementation diff --git a/algorithm-shield/src/rescript/test/WasmIntegrationTest.res b/algorithm-shield/src/rescript/test/WasmIntegrationTest.res deleted file mode 100644 index c5e97dd..0000000 --- a/algorithm-shield/src/rescript/test/WasmIntegrationTest.res +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 hyperpolymath -// Part of Algorithm Shield - https://github.com/hyperpolymath/algorithm-shield -// Integration test for WASM rule engine - -open RuleEngine - -// Test creating the engine and adding a rule -let testRuleEngine = async () => { - Console.log("Testing WASM Rule Engine...") - - // Create engine - let engine = await create() - Console.log("✓ Engine created") - - // Add a noise injection rule - let ruleJson = `{ - "id": "test-noise", - "name": "Test Noise Injection", - "description": "Test rule for integration", - "conditions": [], - "actions": [{"type": "InjectNoise", "payload": {"count": 2}}], - "probability": 1.0, - "enabled": true - }` - - let result = await addRuleSafe(engine, ruleJson) - switch result { - | Ok() => Console.log("✓ Rule added successfully") - | Error(err) => Console.error("✗ Failed to add rule: " ++ err) - } - - // Get narrative - let narrative = narrateRules(engine) - Console.log("✓ Rule narrative: " ++ narrative) - - // Evaluate against context - let contextJson = `{ - "platform": "youtube", - "content_type": "video", - "scroll_depth": 10, - "session_duration": 120, - "recent_categories": ["tech"], - "timestamp": 0 - }` - - let evalResult = await evaluateSafe(engine, contextJson) - switch evalResult { - | Ok(actions) => Console.log("✓ Evaluation successful: " ++ actions) - | Error(err) => Console.error("✗ Evaluation failed: " ++ err) - } - - Console.log("WASM integration test complete!") -}