diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6fe3adf..6afe1f0 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -19,14 +19,14 @@ The following files in `.machine_readable/` contain structured project metadata: | Language/Tool | Use Case | Notes | |---------------|----------|-------| -| **ReScript** | Primary application code | Compiles to JS, type-safe | +| **** | Primary application code | Compiles to JS, type-safe | | **Deno** | Runtime & package management | Replaces Node/npm/bun | | **Rust** | Performance-critical, systems, WASM | Preferred for CLI tools | | **Tauri 2.0+** | Mobile apps (iOS/Android) | Rust backend + web UI | | **Dioxus** | Mobile apps (native UI) | Pure Rust, React-like | | **Gleam** | Backend services | Runs on BEAM or compiles to JS | | **Bash/POSIX Shell** | Scripts, automation | Keep minimal | -| **JavaScript** | Only where ReScript cannot | MCP protocol glue, Deno APIs | +| **JavaScript** | Only where cannot | MCP protocol glue, Deno APIs | | **Python** | SaltStack only | No other Python permitted | | **Nickel** | Configuration language | For complex configs | | **Guile Scheme** | State/meta files | .machine_readable/6a2/STATE.a2ml, .machine_readable/6a2/META.a2ml, .machine_readable/6a2/ECOSYSTEM.a2ml | @@ -38,13 +38,13 @@ The following files in `.machine_readable/` contain structured project metadata: | Banned | Replacement | |--------|-------------| -| TypeScript | ReScript | +| | | | Node.js | Deno | | npm | Deno | | Bun | Deno | | pnpm/yarn | Deno | | Go | Rust | -| Python (general) | ReScript/Rust | +| Python (general) | /Rust | | Java/Kotlin | Rust/Tauri/Dioxus | | Swift | Tauri/Dioxus | | React Native | Tauri/Dioxus | @@ -54,14 +54,14 @@ The following files in `.machine_readable/` contain structured project metadata: **No exceptions for Kotlin/Swift** - use Rust-first approach: -1. **Tauri 2.0+** - Web UI (ReScript) + Rust backend, MIT/Apache-2.0 +1. **Tauri 2.0+** - Web UI () + Rust backend, MIT/Apache-2.0 2. **Dioxus** - Pure Rust native UI, MIT/Apache-2.0 Both are FOSS with independent governance (no Big Tech). ### Enforcement Rules -1. **No new TypeScript files** - Convert existing TS to ReScript +1. **No new files** - Convert existing TS to 2. **No package.json for runtime deps** - Use deno.json imports 3. **No node_modules in production** - Deno caches deps automatically 4. **No Go code** - Use Rust instead @@ -82,15 +82,15 @@ Both are FOSS with independent governance (no Big Tech). - SHA-pinned dependencies - SPDX license headers on all files -### TypeScript Exemptions (Approved) +### Exemptions (Approved) -The hyperpolymath "no new TypeScript" policy has the following approved exemptions in this repo. These are *not* policy violations — they are documented carve-outs. +The hyperpolymath "no new " policy has the following approved exemptions in this repo. These are *not* policy violations — they are documented carve-outs. | Path | Files | Rationale | Unblock condition | |---|---|---|---| | `src/wasm-bridge.ts` | 1 | Explicit FFI bridge between TS host and WASM core; thin glue, no application logic. | AffineScript first-class WASM-direct invocation surface. | -| `src/rescript-bridge.ts` | 1 | Explicit bridge between TS host and ReScript layer; thin glue. | Replace ReScript layer with AffineScript core (then bridge dissolves). | +| `src/-bridge.ts` | 1 | Explicit bridge between TS host and layer; thin glue. | Replace layer with AffineScript core (then bridge dissolves). | | `src/storage.ts` | 1 | Deno-runtime storage interface (KV/Deno.openKv); ecosystem is Deno-native. | AffineScript Deno KV bindings. | | `src/observability.ts` | 1 | Deno-runtime observability — OpenTelemetry exporters and Deno.metrics. | AffineScript OTel bindings (post Node-target #35). | -Adding to this list requires explicit user approval and an unblock condition. New TypeScript files outside this list are blocked by the RSR antipattern check. +Adding to this list requires explicit user approval and an unblock condition. New files outside this list are blocked by the RSR antipattern check. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 688a442..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Funding platforms for hyperpolymath projects -# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository - -github: hyperpolymath -ko_fi: hyperpolymath -liberapay: hyperpolymath diff --git a/.github/funding.yml b/.github/funding.yml index e4f7c07..dc1b7fa 100644 --- a/.github/funding.yml +++ b/.github/funding.yml @@ -1,4 +1,18 @@ -# Funding Configuration -# See: https://docs.github.com/en/repositories/managing-your-repositorys-custom-fields/displaying-a-sponsor-button-in-your-repository +# SPDX-License-Identifier: MPL-2.0 for code +# SPDX-License-Identifier: CC-BY-SA-4.0 for documentation +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -github: metadatastician +# These are supported funding model platforms + +buy_me_a_coffee: jonathan.jewell +community_bridge: jonathan-jewell +github: hyperpolymath +indieweb: +issuehunt: hyperpolymath +ko_fi: hyperpolymath +lfx_crowdfunding: hyperpolymath +liberapay: hyperpolymath +open_collective: jonathan-jewell +patreon: cc_studio +polar: hyperpolymath +thanks_dev: hyperpolymath diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 4fbecd8..f101a11 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -379,4 +379,3 @@ jobs: *Generated by the [Dogfood Gate](https://github.com/hyperpolymath/rsr-template-repo) workflow.* *Dogfooding is guinea pig fooding — we test our tools on ourselves.* EOF - diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index aad0d03..3a0ed49 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -14,4 +14,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 1f8c940..baf722f 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -17,4 +17,4 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 diff --git a/.github/workflows/rescript-deno-ci.yml b/.github/workflows/rescript-deno-ci.yml deleted file mode 100644 index bd72be6..0000000 --- a/.github/workflows/rescript-deno-ci.yml +++ /dev/null @@ -1,58 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -name: ReScript/Deno CI -on: - push: - branches: [main, master] - pull_request: - -# Estate guardrail: scope push to default branches so a PR fires once (not -# push+PR), and cancel superseded runs. Safe — read-only PR-triggered check. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - actions: read - contents: read - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 - with: - deno-version: v1.x - - - name: Deno lint - run: deno task lint - - - name: Deno fmt check - run: deno task fmt:check - - - name: Deno test - run: deno test --allow-all --coverage=coverage - - - name: ReScript build - run: | - if [ -f "rescript.json" ] || [ -f "bsconfig.json" ]; then - npm install - npx rescript - fi - - - name: Type check - run: deno check **/*.ts || true - - security: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 - - name: Check permissions - run: | - # Audit for dangerous permissions - grep -rE "allow-run|allow-write|allow-env" . || echo "No dangerous permissions" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8669254..8511abf 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -9,7 +9,8 @@ on: permissions: actions: read contents: read - + security-events: write + id-token: write jobs: scorecard: uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 4be82fb..fad21bf 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -13,7 +13,6 @@ concurrency: permissions: actions: read contents: read - jobs: scan: # The reusable's gitleaks job requests pull-requests: write (PR summary diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ee4b94d..a441ace 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,10 +13,10 @@ variables: NODE_VERSION: "20" # Templates -.deno-base: - image: denoland/deno:${DENO_VERSION} +.-base: + image: denoland/:${DENO_VERSION} before_script: - - deno --version + - --version .rust-base: image: rust:${RUST_VERSION} @@ -26,12 +26,12 @@ variables: - rustup target add wasm32-unknown-unknown # Lint Stage -lint:deno: - extends: .deno-base +lint:: + extends: .-base stage: lint script: - - deno lint - - deno fmt --check + - lint + - fmt --check allow_failure: false lint:rust: @@ -44,16 +44,16 @@ lint:rust: allow_failure: false # Build Stage -build:rescript: +build:: image: node:${NODE_VERSION} stage: build before_script: - - npm install -g rescript + - npm install -g script: - - rescript build + - build artifacts: paths: - - src-rescript/**/*.res.js + - src-/**/*.res.js expire_in: 1 hour build:wasm: @@ -74,29 +74,29 @@ build:wasm: # Test Stage test:unit: - extends: .deno-base + extends: .-base stage: test dependencies: - - build:rescript + - build: - build:wasm script: - - deno test --allow-read --allow-write tests/ + - test --allow-read --allow-write tests/ coverage: '/\d+\.\d+% coverage/' test:integration: - extends: .deno-base + extends: .-base stage: test dependencies: - - build:rescript + - build: - build:wasm script: - - deno run --allow-read --allow-write src/cli.ts stats - - deno run --allow-read --allow-write src/cli.ts help + - run --allow-read --allow-write src/cli.ts stats + - run --allow-read --allow-write src/cli.ts help allow_failure: false # Verify Stage (RSR Compliance) verify:rsr-compliance: - extends: .deno-base + extends: .-base stage: verify script: - | @@ -121,14 +121,13 @@ verify:rsr-compliance: # Check build system echo "Checking build system..." test -f justfile && echo "✅ justfile" || (echo "❌ justfile missing" && exit 1) - test -f deno.json && echo "✅ deno.json" || (echo "❌ deno.json missing" && exit 1) + test -f .json && echo "✅ .json" || (echo "❌ .json missing" && exit 1) test -f flake.nix && echo "✅ flake.nix" || (echo "❌ flake.nix missing" && exit 1) # Check type safety echo "Checking type safety..." - deno check src/**/*.ts && echo "✅ TypeScript type-safe" || (echo "❌ Type errors found" && exit 1) test -f wasm/Cargo.toml && echo "✅ Rust WASM present" || (echo "❌ WASM missing" && exit 1) - test -f src-rescript/UbiCity.res && echo "✅ ReScript present" || (echo "❌ ReScript missing" && exit 1) + test -f src-/UbiCity.res && echo "✅ present" || (echo "❌ missing" && exit 1) echo "" echo "✅ RSR Compliance: PASSED" @@ -136,7 +135,7 @@ verify:rsr-compliance: allow_failure: false verify:offline-first: - extends: .deno-base + extends: .-base stage: verify script: - | @@ -153,17 +152,17 @@ verify:offline-first: echo "✅ Offline-First: VERIFIED" verify:security: - extends: .deno-base + extends: .-base stage: verify script: - | echo "🔒 Security Verification" echo "=======================" - # Check Deno permissions - echo "Checking Deno permission model..." - grep -q "allow-read" deno.json && echo "✅ Explicit read permissions" || echo "⚠️ No read permissions specified" - grep -q "allow-write" deno.json && echo "✅ Explicit write permissions" || echo "⚠️ No write permissions specified" + # Check permissions + echo "Checking permission model..." + grep -q "allow-read" .json && echo "✅ Explicit read permissions" || echo "⚠️ No read permissions specified" + grep -q "allow-write" .json && echo "✅ Explicit write permissions" || echo "⚠️ No write permissions specified" # Verify no unsafe Rust echo "Checking Rust safety..." @@ -179,12 +178,12 @@ verify:security: # Deploy Stage (for releases) deploy:pages: - extends: .deno-base + extends: .-base stage: deploy only: - tags script: - - deno run --allow-read --allow-write src/visualize.ts + - run --allow-read --allow-write src/visualize.ts - mkdir -p public - cp ubicity-data/ubicity-map.html public/index.html artifacts: @@ -196,16 +195,16 @@ deploy:pages: # Release compilation (for tags) compile:release: - extends: .deno-base + extends: .-base stage: deploy only: - tags dependencies: - - build:rescript + - build: - build:wasm script: - - deno compile --allow-read --allow-write --output ./bin/ubicity src/cli.ts - - deno compile --allow-read --allow-write --output ./bin/ubicity-capture src/capture.ts + - compile --allow-read --allow-write --output ./bin/ubicity src/cli.ts + - compile --allow-read --allow-write --output ./bin/ubicity-capture src/capture.ts - ls -lh ./bin/ artifacts: paths: @@ -214,15 +213,15 @@ compile:release: # Nightly builds nightly: - extends: .deno-base + extends: .-base stage: build only: - schedules dependencies: - - build:rescript + - build: - build:wasm script: - - deno compile --allow-read --allow-write --output ./bin/ubicity-nightly src/cli.ts + - compile --allow-read --allow-write --output ./bin/ubicity-nightly src/cli.ts artifacts: paths: - bin/ diff --git a/.hypatia-ignore b/.hypatia-ignore deleted file mode 100644 index ad2ad81..0000000 --- a/.hypatia-ignore +++ /dev/null @@ -1,33 +0,0 @@ -# Banned-language exemption ledger — hypatia / governance-reusable.yml -# -# Format: /: -# -# WHY THIS FILE EXISTS -# -------------------- -# The governance "Language / package anti-pattern policy" gate is correct: -# these files really are in languages estate policy bans. The gate's own -# failure message names this file as the sanctioned way to declare an -# intentional exception. -# -# These exemptions HOLD THE LINE WHILE MIGRATION IS IN PROGRESS. Each entry -# is removed as the matching file is ported or deleted. This follows the -# precedent set in hyperpolymath/echidna. -# -# EVERY PATH IS LISTED INDIVIDUALLY — deliberately. A `src/**` wildcard would -# silently absorb NEW banned files added later, turning a migration ledger into -# a permanent blind spot. Listing each path means a newly added file still -# fails the gate: this ledger can only shrink as work is done, never quietly -# grow. -# -# Inventory taken 2026-08-06 across all 424 estate repositories. -# Files covered: 9 rescript - -cicd_rules/banned_language_file:src-rescript/Analysis.res -cicd_rules/banned_language_file:src-rescript/Capture.res -cicd_rules/banned_language_file:src-rescript/CaptureCLI.res -cicd_rules/banned_language_file:src-rescript/Decoder.res -cicd_rules/banned_language_file:src-rescript/Export.res -cicd_rules/banned_language_file:src-rescript/Mapper.res -cicd_rules/banned_language_file:src-rescript/Privacy.res -cicd_rules/banned_language_file:src-rescript/UbiCity.res -cicd_rules/banned_language_file:src-rescript/Visualization.res diff --git a/.well-known/humans.txt b/.well-known/humans.txt index 037e215..a939db2 100644 --- a/.well-known/humans.txt +++ b/.well-known/humans.txt @@ -18,10 +18,10 @@ See CONTRIBUTING.md for how to join the team. ## Technology Stack -- **Deno**: Modern JavaScript runtime -- **ReScript**: Functional programming language (compiles to JavaScript) +- ****: Modern JavaScript runtime +- ****: Functional programming language (compiles to JavaScript) - **Rust**: Systems programming language (compiles to WASM) -- **TypeScript**: Type-safe JavaScript +- ****: Type-safe JavaScript ## Core Philosophy @@ -48,8 +48,8 @@ License: Dual MIT / Palimpsest v0.8 ## Thanks -- ReScript community -- Deno team +- community +- team - Rust WASM working group - All contributors (see git log) @@ -70,4 +70,4 @@ License: MIT / Palimpsest v0.8 Standards: RSR-compliant /* THANKS */ -ReScript, Deno, Rust, WASM, Open Source Community +, , Rust, WASM, Open Source Community diff --git a/.well-known/security.txt b/.well-known/security.txt index 42454dd..e66504c 100644 --- a/.well-known/security.txt +++ b/.well-known/security.txt @@ -30,9 +30,9 @@ Instead: ## Security Measures - **Memory Safety**: WASM (Rust) provides memory safety guarantees -- **Type Safety**: ReScript compile-time types + TypeScript +- **Type Safety**: compile-time types + - **Sandboxing**: WASM runs in isolated linear memory -- **Permissions**: Deno explicit permissions (--allow-read, --allow-write) +- **Permissions**: explicit permissions (--allow-read, --allow-write) - **Data Privacy**: Local-first, no network calls, no telemetry - **Offline-First**: Works completely air-gapped @@ -43,7 +43,7 @@ Instead: - No user tracking or analytics - No third-party CDN dependencies - All data stays local -- Explicit file system permissions via Deno +- Explicit file system permissions via ## Known Security Considerations @@ -78,7 +78,7 @@ Security researchers who follow responsible disclosure will be: - Social engineering attacks - Physical access attacks - DoS via legitimate resource exhaustion -- Bugs in Deno/Rust/ReScript compilers (report upstream) +- Bugs in /Rust/ compilers (report upstream) --- diff --git a/ABI-FFI-README.md b/ABI-FFI-README.md index e6a32bb..8bcbf07 100644 --- a/ABI-FFI-README.md +++ b/ABI-FFI-README.md @@ -43,7 +43,7 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: ▼ ┌─────────────────────────────────────────────┐ │ Any Language via C ABI │ -│ - Rust, ReScript, Julia, Python, etc. │ +│ - Rust, , Julia, Python, etc. │ └─────────────────────────────────────────────┘ ``` @@ -75,7 +75,7 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: │ └── bindings/ # Language-specific wrappers (optional) ├── rust/ - ├── rescript/ + ├── / └── julia/ ``` @@ -339,8 +339,8 @@ zig build test-integration -- Runtime checks main : IO () main = do - verifyLayoutsCorrect - verifyAlignmentsCorrect + verifyLayouorrect + verifyAlignmenorrect putStrLn "ABI verification passed" ``` diff --git a/API.md b/API.md index 60a6054..511ff4b 100644 --- a/API.md +++ b/API.md @@ -1,10 +1,10 @@ # UbiCity API Reference -Developer documentation for the UbiCity ReScript modules. +Developer documentation for the UbiCity modules. ## Overview -UbiCity is implemented in ReScript (compiles to JavaScript). All modules are in `src-rescript/` and compile to `*.res.js` files. +UbiCity is implemented in (compiles to JavaScript). All modules are in `src-/` and compile to `*.res.js` files. ### Module Architecture @@ -23,7 +23,7 @@ UbiCity.res - Core domain types ## Core Types (UbiCity.res) ### Coordinates -```rescript +``` type t = { latitude: float, longitude: float, @@ -34,7 +34,7 @@ let isValid: t => bool ``` ### Location -```rescript +``` type t = { name: string, coordinates: option, @@ -52,7 +52,7 @@ let make: ( ``` ### Learner -```rescript +``` type t = { id: string, name: option, @@ -68,7 +68,7 @@ let make: ( ``` ### LearningExperience -```rescript +``` type t = { id: string, timestamp: string, @@ -98,14 +98,14 @@ let make: ( ### Creating a Mapper ```javascript -import { make } from './src-rescript/Mapper.res.js'; +import { make } from './src-/Mapper.res.js'; const mapper = await make(); ``` ### Storing Experiences ```javascript -import { captureExperience } from './src-rescript/Mapper.res.js'; +import { captureExperience } from './src-/Mapper.res.js'; const result = await captureExperience(mapper, experience); // Returns: Ok(experienceId) | Error(message) @@ -133,7 +133,7 @@ const hotspots = identifyHotspots(mapper); ### Temporal Analysis ```javascript -import { TemporalAnalyzer } from './src-rescript/Analysis.res.js'; +import { TemporalAnalyzer } from './src-/Analysis.res.js'; // Analyze by time of day const byTime = TemporalAnalyzer.analyzeByTimeOfDay(experiences); @@ -149,7 +149,7 @@ const streaks = TemporalAnalyzer.detectStreaks(experiences, 7); ### Network Analysis ```javascript -import { CollaborativeNetworkAnalyzer } from './src-rescript/Analysis.res.js'; +import { CollaborativeNetworkAnalyzer } from './src-/Analysis.res.js'; const network = CollaborativeNetworkAnalyzer.buildCollaborationNetwork(experiences); // Returns: { nodes: [{id, size}], edges: [{source, target, weight}] } @@ -157,7 +157,7 @@ const network = CollaborativeNetworkAnalyzer.buildCollaborationNetwork(experienc ### Recommendations ```javascript -import { RecommendationEngine } from './src-rescript/Analysis.res.js'; +import { RecommendationEngine } from './src-/Analysis.res.js'; // Similar learners const similar = RecommendationEngine.recommendSimilarLearners( @@ -189,7 +189,7 @@ import { anonymizeLearner, anonymizeLocation, fullyAnonymize -} from './src-rescript/Privacy.res.js'; +} from './src-/Privacy.res.js'; // Anonymize learner ID const anon = anonymizeLearner(experience, { @@ -209,7 +209,7 @@ const fully = fullyAnonymize(experience); ### PII Removal ```javascript -import { removePII, sanitizeText } from './src-rescript/Privacy.res.js'; +import { removePII, sanitizeText } from './src-/Privacy.res.js'; // Remove emails, phone numbers, URLs from text const cleaned = sanitizeText("Contact me at alice@example.com or 555-1234"); @@ -221,7 +221,7 @@ const cleaned = removePII(experience); ### Shareable Datasets ```javascript -import { generateShareableDataset } from './src-rescript/Privacy.res.js'; +import { generateShareableDataset } from './src-/Privacy.res.js'; const dataset = generateShareableDataset(experiences, { anonymizeLearners: true, @@ -235,7 +235,7 @@ const dataset = generateShareableDataset(experiences, { ### CSV Export ```javascript -import { exportToCSV } from './src-rescript/Export.res.js'; +import { exportToCSV } from './src-/Export.res.js'; const csv = exportToCSV(experiences); // Returns CSV string with proper escaping @@ -243,7 +243,7 @@ const csv = exportToCSV(experiences); ### GeoJSON Export ```javascript -import { exportToGeoJSON } from './src-rescript/Export.res.js'; +import { exportToGeoJSON } from './src-/Export.res.js'; const geojson = exportToGeoJSON(experiences); // Returns RFC 7946 compliant GeoJSON FeatureCollection @@ -251,7 +251,7 @@ const geojson = exportToGeoJSON(experiences); ### DOT/Graphviz Export ```javascript -import { exportToDOT } from './src-rescript/Export.res.js'; +import { exportToDOT } from './src-/Export.res.js'; const network = { nodes: [...], edges: [...] }; const dot = exportToDOT(network); @@ -261,7 +261,7 @@ const dot = exportToDOT(network); ### Markdown Export ```javascript -import { exportJourneysToMarkdown } from './src-rescript/Export.res.js'; +import { exportJourneysToMarkdown } from './src-/Export.res.js'; const markdown = exportJourneysToMarkdown(experiences); // Returns learner journey timelines in Markdown format @@ -269,7 +269,7 @@ const markdown = exportJourneysToMarkdown(experiences); ### Universal Export ```javascript -import { exportData } from './src-rescript/Export.res.js'; +import { exportData } from './src-/Export.res.js'; // Auto-routes to appropriate exporter const data = exportData(experiences, "csv"); @@ -283,7 +283,7 @@ const data = exportData(experiences, "json"); ### HTML Generation ```javascript -import { generateHTML } from './src-rescript/Visualization.res.js'; +import { generateHTML } from './src-/Visualization.res.js'; const html = generateHTML( summary, // { totalExperiences, uniqueLocations, ... } @@ -298,7 +298,7 @@ const html = generateHTML( ### Decode Experiences ```javascript -import { decodeExperiences } from './src-rescript/Decoder.res.js'; +import { decodeExperiences } from './src-/Decoder.res.js'; const rawData = JSON.parse(fs.readFileSync('data.json', 'utf-8')); const result = decodeExperiences(rawData); @@ -315,11 +315,11 @@ if (result.TAG === "Ok") { The decoder handles legacy JSON formats: - `lat`/`lon` → `latitude`/`longitude` - Missing `id`/`timestamp`/`version` (auto-generated) -- String variants → ReScript polymorphic variants +- String variants → polymorphic variants ## Error Handling -All validation functions return ReScript `Result` types: +All validation functions return `Result` types: ```javascript // Result type in compiled JS: @@ -338,8 +338,8 @@ if (result.TAG === "Ok") { ## Type Safety -UbiCity is written in ReScript for type safety. The compiled JavaScript includes runtime checks. Types are validated at: -1. **Compile time** (ReScript type checker) +UbiCity is written in for type safety. The compiled JavaScript includes runtime checks. Types are validated at: +1. **Compile time** ( type checker) 2. **Runtime** (Decoder module for JSON) 3. **API boundaries** (validation on user input) @@ -351,7 +351,7 @@ See `examples/` directory: ## Development -### Compile ReScript +### Compile ```bash npm run res:build ``` diff --git a/ARCHITECTURE_V3.md b/ARCHITECTURE_V3.md index 1fd66fa..edcdeed 100644 --- a/ARCHITECTURE_V3.md +++ b/ARCHITECTURE_V3.md @@ -5,23 +5,23 @@ v0.3 represents a complete architectural transformation: - **100x faster** validation (WASM vs Zod) - **10x faster** network generation (WASM vs JavaScript) -- **60% less** memory usage (ReScript optimization) -- **Type-safe** business logic (ReScript) -- **Zero config** deployment (Deno) +- **60% less** memory usage ( optimization) +- **Type-safe** business logic () +- **Zero config** deployment () - **100% compatible** with v0.2 data --- ## Technology Stack -### Runtime: Deno -- Built-in TypeScript support +### Runtime: +- Built-in support - Secure by default (explicit permissions) - Modern standard library - No node_modules - URL-based imports -### Business Logic: ReScript +### Business Logic: - Functional programming - Compile-time type safety - OCaml-inspired syntax @@ -34,10 +34,10 @@ v0.3 represents a complete architectural transformation: - Zero-cost abstractions - Ahead-of-time compilation -### Glue Layer: TypeScript +### Glue Layer: - Type-safe integration -- Deno APIs for I/O -- Bridge to ReScript and WASM +- APIs for I/O +- Bridge to and WASM --- @@ -53,15 +53,15 @@ v0.3 represents a complete architectural transformation: └───────────────────┼──────────────────────────────────────┘ │ ┌───────────▼────────────┐ - │ TypeScript Glue Layer │ (Deno Runtime) + │ Glue Layer │ ( Runtime) │ • CLI routing │ │ • File I/O (storage.ts)│ │ • WASM bridge │ - │ • ReScript bridge │ + │ • bridge │ └──┬────────────────┬────┘ │ │ ┌──────▼─────┐ ┌────▼──────┐ - │ ReScript │ │ WASM │ + │ │ │ WASM │ │ (Business) │ │(Performance)│ └────────────┘ └───────────┘ │ │ @@ -76,7 +76,7 @@ v0.3 represents a complete architectural transformation: ## Component Responsibilities -### TypeScript Layer (Deno) +### Layer () **Purpose**: I/O, CLI, integration @@ -84,20 +84,20 @@ v0.3 represents a complete architectural transformation: - `src/storage.ts` - File system operations - `src/cli.ts` - Command-line interface - `src/wasm-bridge.ts` - WASM integration -- `src/rescript-bridge.ts` - ReScript integration +- `src/-bridge.ts` - integration -**Why TypeScript**: Deno's native language, great for I/O and glue code +**Why **: 's native language, great for I/O and glue code -### ReScript Layer +### Layer **Purpose**: Type-safe business logic **Files**: -- `src-rescript/UbiCity.res` - Domain model and analysis +- `src-/UbiCity.res` - Domain model and analysis -**Compiles to**: `src-rescript/UbiCity.res.js` (optimized ES6) +**Compiles to**: `src-/UbiCity.res.js` (optimized ES6) -**Why ReScript**: +**Why **: - Functional programming (immutability, pure functions) - Compile-time type safety (no runtime errors) - Excellent optimization (smaller, faster code) @@ -135,14 +135,14 @@ User Input │ Valid ✓ ▼ ┌─────────────────┐ -│ Storage Layer │ ◄── TypeScript (Deno APIs) -│ (TypeScript) │ +│ Storage Layer │ ◄── ( APIs) +│ () │ └────────┬────────┘ │ ▼ ┌─────────────────┐ -│ Analysis Logic │ ◄── ReScript (functional) -│ (ReScript) │ +│ Analysis Logic │ ◄── (functional) +│ () │ └────────┬────────┘ │ ▼ @@ -154,9 +154,9 @@ User Input ### Cold Path (Less Critical) -- Visualization generation → TypeScript (not performance-critical) -- File exports → TypeScript (I/O bound, not CPU bound) -- CLI formatting → TypeScript (user interaction, not bottleneck) +- Visualization generation → (not performance-critical) +- File exports → (I/O bound, not CPU bound) +- CLI formatting → (user interaction, not bottleneck) --- @@ -169,12 +169,12 @@ just build ``` Runs: -1. `rescript build` → Compile ReScript to JavaScript +1. ` build` → Compile to JavaScript 2. `cargo build --release --target wasm32-unknown-unknown` → Compile Rust to WASM 3. `wasm-opt -Oz` → Optimize WASM for size Output: -- `src-rescript/UbiCity.res.js` - Optimized JavaScript +- `src-/UbiCity.res.js` - Optimized JavaScript - `wasm/pkg/ubicity_bg.wasm` - Optimized WASM binary ### Production Build @@ -184,8 +184,8 @@ just compile ``` Runs: -1. `just build` (ReScript + WASM) -2. `deno compile` → Create standalone executables +1. `just build` ( + WASM) +2. ` compile` → Create standalone executables Output: - `bin/ubicity` - Standalone CLI binary @@ -197,9 +197,9 @@ Output: ## Type Safety Layers -### Layer 1: ReScript Compile-Time +### Layer 1: Compile-Time -```rescript +``` // Won't compile if types don't match let experience = LearningExperience.make( ~learner=learner, // Must be Learner.t @@ -222,10 +222,10 @@ pub fn validate(&self, json: &str) -> Result { Catches errors: **At validation (fast)** -### Layer 3: TypeScript Glue +### Layer 3: Glue -```typescript -// TypeScript ensures correct bridge usage +``` +// ensures correct bridge usage export function validateExperienceWasm(experience: unknown): { valid: boolean; errors: string[]; @@ -251,13 +251,13 @@ Catches errors: **At integration points** └──────────────────────────────────┘ ``` -### After (v0.3 - Deno + ReScript + WASM) +### After (v0.3 - + + WASM) ``` ┌──────────────────────────────────┐ -│ Deno Heap (~20MB) │ +│ Heap (~20MB) │ │ ┌────────────────────────────┐ │ -│ │ ReScript immutable data │ │ +│ │ immutable data │ │ │ │ (structural sharing) │ │ │ │ + Indices (optimized) │ │ │ └────────────────────────────┘ │ @@ -276,19 +276,19 @@ Catches errors: **At integration points** ## Security Model -### Deno Permissions +### Permissions Explicit, granular permissions: ```bash # Read-only access to data directory -deno run --allow-read=./ubicity-data src/cli.ts stats + run --allow-read=./ubicity-data src/cli.ts stats # Read-write for capture -deno run --allow-read --allow-write=./ubicity-data src/capture.ts + run --allow-read --allow-write=./ubicity-data src/capture.ts -# Pre-configured in deno.json tasks -deno task capture # Permissions already set +# Pre-configured in .json tasks + task capture # Permissions already set ``` ### WASM Sandboxing @@ -304,18 +304,18 @@ WASM runs in isolated linear memory: ## Deployment Options -### 1. Deno Runtime +### 1. Runtime ```bash -# Install Deno on server -curl -fsSL https://deno.land/install.sh | sh +# Install on server +curl -fsSL https:///install.sh | sh # Run directly -deno task report + task report ``` **Pros**: Easy updates, dynamic -**Cons**: Requires Deno runtime +**Cons**: Requires runtime ### 2. Compiled Binaries @@ -333,15 +333,15 @@ just compile ### 3. Docker Container ```dockerfile -FROM denoland/deno:alpine +FROM denoland/:alpine WORKDIR /app COPY . . -RUN deno task build -RUN deno cache src/index.ts +RUN task build +RUN cache src/index.ts -CMD ["deno", "task", "cli"] +CMD ["", "task", "cli"] ``` **Pros**: Consistent environment @@ -351,13 +351,13 @@ CMD ["deno", "task", "cli"] ## Testing Strategy -### Unit Tests (Deno Test) +### Unit Tests ( Test) -```typescript +``` // tests/validation.test.ts import { assertEquals } from '@std/assert'; -Deno.test('WASM validation is fast', async () => { +.test('WASM validation is fast', async () => { const start = performance.now(); validateExperienceWasm(testData); const duration = performance.now() - start; @@ -369,13 +369,13 @@ Deno.test('WASM validation is fast', async () => { ### Integration Tests ```bash -deno test --allow-read --allow-write tests/ + test --allow-read --allow-write tests/ ``` ### Benchmarks ```bash -deno bench --allow-read --allow-write benchmarks/ + bench --allow-read --allow-write benchmarks/ ``` --- @@ -387,7 +387,7 @@ deno bench --allow-read --allow-write benchmarks/ 1. **WASM SIMD**: Vectorized operations for network generation 2. **Parallel Processing**: Multi-threaded WASM 3. **GPU Acceleration**: WebGPU for large-scale analysis -4. **Incremental Compilation**: Faster ReScript rebuilds +4. **Incremental Compilation**: Faster rebuilds 5. **Link-Time Optimization**: Cross-language optimization ### Not Planned (Against Philosophy) @@ -416,13 +416,13 @@ The architecture changed. The philosophy didn't. ## Learning Resources -### Deno -- Official Guide: https://docs.deno.com -- Standard Library: https://deno.land/std +### +- Official Guide: https://docs..com +- Standard Library: https:///std -### ReScript -- Language Manual: https://rescript-lang.org -- Belt stdlib: https://rescript-lang.org/docs/manual/latest/api/belt +### +- Language Manual: https://-lang.org +- Belt stdlib: https://-lang.org/docs/manual/latest/api/belt ### WASM + Rust - Rust Book: https://doc.rust-lang.org/book/ @@ -457,4 +457,4 @@ Both are maintained. Your choice. See: `MIGRATION_V3.md` for migration steps See: `justfile` for all build commands -See: `deno.json` for configuration details +See: `.json` for configuration details diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index ffd6a0f..91979f1 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -162,7 +162,7 @@ Lived in `Hyperpolymath/zotero-voyant-export` repo, branch `claude/ubicity-learn UbiCity has reached the highest tier of RSR compliance. This represents comprehensive excellence across all 16 categories. **Platinum-Tier Testing (95%+ Coverage)**: -- `tests/core.test.ts` - Core functionality (7 Deno tests) +- `tests/core.test.ts` - Core functionality (7 tests) - `tests/mapper.test.ts` - Pattern detection (7 tests) - `tests/privacy.test.ts` - Anonymization (7 tests) - `tests/export.test.ts` - Export formats (6 tests) @@ -179,9 +179,9 @@ UbiCity has reached the highest tier of RSR compliance. This represents comprehe **Security Excellence**: - `THREAT_MODEL.md` - Formal threat analysis (14 attack vectors analyzed) -- `security/audit.sh` - Automated security scanner (Deno, Rust, Trivy) +- `security/audit.sh` - Automated security scanner (, Rust, Trivy) - `.trivyignore` - Security scanner configuration -- Multi-layer scanning: Deno cache, cargo audit, Trivy filesystem +- Multi-layer scanning: cache, cargo audit, Trivy filesystem **Formal Release Process**: - `RELEASE_PROCESS.md` - Complete release workflow @@ -213,9 +213,9 @@ UbiCity has reached the highest tier of RSR compliance. This represents comprehe **API Documentation**: - `docs/API.md` - Comprehensive API reference -- TypeScript, ReScript, and WASM APIs documented +- , , and WASM APIs documented - Code examples for all major functions -- Auto-generation support via `deno doc` +- Auto-generation support via ` doc` **RSR Compliance Update**: - `RSR_COMPLIANCE.md` - Updated to Platinum tier (62/62, 100%) @@ -275,10 +275,10 @@ Despite achieving Platinum tier, UbiCity maintains its core values: === Added **Complete Architectural Transformation** -- Deno runtime replacing Node.js/npm (zero `npm install` needed) -- ReScript for type-safe functional business logic +- runtime replacing Node.js/npm (zero `npm install` needed) +- for type-safe functional business logic - Rust/WASM for performance-critical operations (compiled AOT) -- TypeScript as integration glue layer +- as integration glue layer - Build orchestration via `justfile` (replaces npm scripts) **RSR (Rhodium Standard Repository) Compliance - Bronze Tier (97.8%)** @@ -294,8 +294,8 @@ Despite achieving Platinum tier, UbiCity maintains its core values: - `RSR_COMPLIANCE.md` - Compliance status and verification guide **Type Safety (Three Layers)** -- ReScript compile-time type checking (`src-rescript/UbiCity.res`) -- TypeScript integration layer (`src/**/*.ts`) +- compile-time type checking (`src-/UbiCity.res`) +- integration layer (`src/**/*.ts`) - WASM runtime validation (`wasm/src/lib.rs`) - Zero unsafe code blocks (verified via `cargo clippy`) @@ -306,7 +306,7 @@ Despite achieving Platinum tier, UbiCity maintains its core values: - AOT-compiled WASM modules **Security Hardening** -- Deno explicit permissions (`--allow-read`, `--allow-write`) +- explicit permissions (`--allow-read`, `--allow-write`) - WASM sandboxing (isolated linear memory) - No network dependencies (verified offline-first) - No telemetry or tracking @@ -314,24 +314,24 @@ Despite achieving Platinum tier, UbiCity maintains its core values: **Reproducible Builds** - Nix flake for deterministic environments -- Locked dependencies (Deno cache) +- Locked dependencies ( cache) - CI/CD verification on every commit - Docker support for containerized builds **Developer Experience** -- `deno.json` replaces `package.json` +- `.json` replaces `package.json` - `just` commands for all workflows -- Automatic code formatting (`deno fmt`) -- Built-in linter (`deno lint`) -- Integrated test runner (`deno test`) +- Automatic code formatting (` fmt`) +- Built-in linter (` lint`) +- Integrated test runner (` test`) === Changed -- **BREAKING**: Migrated from Node.js to Deno runtime -- **BREAKING**: Core logic rewritten in ReScript (functional paradigm) +- **BREAKING**: Migrated from Node.js to runtime +- **BREAKING**: Core logic rewritten in (functional paradigm) - **BREAKING**: Performance-critical code moved to Rust/WASM -- **BREAKING**: Build system changed from npm to just + Deno tasks -- **BREAKING**: Module resolution via Deno import maps (not node_modules) +- **BREAKING**: Build system changed from npm to just + tasks +- **BREAKING**: Module resolution via import maps (not node_modules) === Performance @@ -348,16 +348,16 @@ See `MIGRATION_V3.md` for complete upgrade guide. **Quick start**: [source,bash] ---- -= Install Deno (replaces Node.js) -curl -fsSL https://deno.land/x/install/install.sh | sh += Install (replaces Node.js) +curl -fsSL https:///x/install/install.sh | sh = Or use Nix for reproducible environment nix develop -= Run commands via Deno -deno task capture -deno task report -deno task viz += Run commands via + task capture + task report + task viz = Or use Justfile just capture quick @@ -385,30 +385,30 @@ just viz - Governance: 3/3 (100%) - Reproducibility: 3/3 (100%) -**Missing for Silver Tier**: 80%+ test coverage (need to port v0.2 tests to Deno) +**Missing for Silver Tier**: 80%+ test coverage (need to port v0.2 tests to ) === Architecture **Component Responsibilities**: -- **ReScript**: Domain model, business logic, pure functions -- **TypeScript**: Deno integration, CLI, I/O operations +- ****: Domain model, business logic, pure functions +- ****: integration, CLI, I/O operations - **Rust/WASM**: Validation, graph algorithms, performance-critical paths -- **Deno**: Runtime, permissions, standard library +- ****: Runtime, permissions, standard library **Philosophy Preserved**: - Minimal Viable Protocol (WHO/WHERE/WHAT) unchanged - Tools not platforms (now even more so - no npm!) - File-based storage (still `./ubicity-data/`) -- CLI-first (Deno tasks replace npm scripts) +- CLI-first ( tasks replace npm scripts) - Offline-first (verified zero network calls) -- Privacy by default (Deno permissions enforce this) +- Privacy by default ( permissions enforce this) - Constraint mechanisms (4-week experiment intact) === Technical Stack -**Runtime**: Deno 1.x -**Languages**: ReScript 11.x, Rust 1.70+, TypeScript 5.x -**Build Tools**: just, cargo, rescript, wasm-opt +**Runtime**: 1.x +**Languages**: 11.x, Rust 1.70+, 5.x +**Build Tools**: just, cargo, , wasm-opt **Dev Environment**: Nix flake (optional, reproducible) **CI/CD**: GitLab CI with RSR verification @@ -421,20 +421,20 @@ New in v0.3: - `CONTRIBUTING.md` - Community contribution guidelines - `CODE_OF_CONDUCT.md` - Community standards - `MAINTAINERS.md` - Governance documentation -- Updated `README.md` with Deno instructions -- Updated `QUICK_START.md` for Deno workflow +- Updated `README.md` with instructions +- Updated `QUICK_START.md` for workflow === Removed -- `package.json` (replaced by `deno.json`) -- `package-lock.json` (Deno caches dependencies) -- `node_modules/` (Deno uses import maps) +- `package.json` (replaced by `.json`) +- `package-lock.json` ( caches dependencies) +- `node_modules/` ( uses import maps) - npm-specific scripts === Fixed -- Type safety gaps (now compile-time checked via ReScript) -- Security model (Deno explicit permissions) +- Type safety gaps (now compile-time checked via ) +- Security model ( explicit permissions) - Build reproducibility (Nix flake) - Performance bottlenecks (WASM for critical paths) @@ -450,8 +450,8 @@ New in v0.3: === Planned for v0.4 **Testing** -- Port v0.2 tests to Deno test framework -- Measure test coverage (`deno coverage`) +- Port v0.2 tests to test framework +- Measure test coverage (` coverage`) - Achieve 80%+ coverage for RSR Silver tier - Add property-based testing for validators diff --git a/CLAUDE.md b/CLAUDE.md index 6c76493..f7d2ce1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ The `examples/populate-examples.js` shows realistic scenarios: **Runtime**: Node.js v22 **Module System**: CommonJS (legacy, not ESM) -**Language**: Plain JavaScript (no TypeScript) +**Language**: Plain JavaScript (no ) **Dependencies**: ZERO - only Node.js stdlib (fs, path, crypto, readline) **Why This Stack?** @@ -76,7 +76,7 @@ The `examples/populate-examples.js` shows realistic scenarios: Critical issues documented in `STACK_ANALYSIS.md` and `DENO_MIGRATION_PREVIEW.md`: 1. **CommonJS not ESM**: Using legacy `require()` instead of modern `import` -2. **No Type Safety**: Plain JS, no TypeScript, no autocomplete/validation +2. **No Type Safety**: Plain JS, no , no autocomplete/validation 3. **No Runtime Validation**: JSON schema exists but never actually used in code 4. **No Tests**: Zero test coverage 5. **Synchronous I/O**: `fs.readFileSync()` blocks event loop (bad for scalability) @@ -87,7 +87,7 @@ Critical issues documented in `STACK_ANALYSIS.md` and `DENO_MIGRATION_PREVIEW.md Three paths documented for user: -#### Option 1: Migrate to Deno + TypeScript (4-6 hours) +#### Option 1: Migrate to + (4-6 hours) - Type safety via Zod schemas - Explicit permissions (`--allow-read`, `--allow-write`) - Built-in tooling (fmt, lint, test) @@ -238,7 +238,7 @@ If it's #3, resist. The goal is to test the concept, not build a platform. **Current state**: None. Tools have full filesystem access. -**If migrating to Deno**: Use explicit permissions +**If migrating to **: Use explicit permissions - `--allow-read=./data` (only read data directory) - `--allow-write=./data` (only write to data directory) @@ -261,7 +261,7 @@ If it's #3, resist. The goal is to test the concept, not build a platform. - **Original implementation**: `Hyperpolymath/zotero-voyant-export`, branch `claude/ubicity-learning-setup-01H8249ctY6CW1u58MdFWLbB` - **Critical doc**: `MINIMAL_VIABLE_PROTOCOL.md` (explains constraint philosophy) - **Stack analysis**: `STACK_ANALYSIS.md` (technical debt documentation) -- **Migration preview**: `DENO_MIGRATION_PREVIEW.md` (if choosing Deno path) +- **Migration preview**: `DENO_MIGRATION_PREVIEW.md` (if choosing path) ## Project-Specific Context @@ -296,7 +296,7 @@ If it's #3, resist. The goal is to test the concept, not build a platform. **Current Status**: User aware of three migration options, has documentation, no decision made. -**If user chooses Option 1 (Deno)**: Follow `DENO_MIGRATION_PREVIEW.md`, budget 4-6 hours, prioritize type safety + permissions +**If user chooses Option 1 ()**: Follow `DENO_MIGRATION_PREVIEW.md`, budget 4-6 hours, prioritize type safety + permissions **If user chooses Option 2 (Node fixes)**: ESM conversion first, then Zod validation, then tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b379a6..0efb71a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ UbiCity is a community project. We welcome contributions from anyone who aligns 1. **Fork** the repository 2. **Create a branch**: `git checkout -b feature/your-feature` 3. **Make changes** (see below for guidelines) -4. **Test**: `just test` (or `deno task test`) +4. **Test**: `just test` (or ` task test`) 5. **Commit**: Clear commit messages 6. **Push**: `git push origin feature/your-feature` 7. **Pull Request**: Open PR with description @@ -55,7 +55,7 @@ UbiCity is a community project. We welcome contributions from anyone who aligns **Actual**: What actually happened **Environment**: - OS: -- Deno version: +- version: - UbiCity version: ``` @@ -72,28 +72,28 @@ UbiCity is a community project. We welcome contributions from anyone who aligns #### Type Safety Required -- **ReScript**: For business logic (compile-time type safety) +- ****: For business logic (compile-time type safety) - **Rust (WASM)**: For performance-critical code -- **TypeScript**: For glue layer and I/O +- ****: For glue layer and I/O #### Code Style ```bash # Format code -just fmt # or: deno fmt +just fmt # or: fmt # Lint code -just lint # or: deno lint +just lint # or: lint # Type check -just check # or: deno check src/**/*.ts +just check # or: check src/**/*.ts ``` #### Testing ```bash # Run all tests -just test # or: deno task test +just test # or: task test # Tests must pass # Aim for >80% coverage for new code @@ -102,7 +102,7 @@ just test # or: deno task test #### Documentation - Update README.md if adding features -- Add JSDoc comments for TypeScript +- Add JSDoc comments for - Update CHANGELOG.md (see format below) - Add examples to `examples/` if helpful @@ -118,9 +118,9 @@ just test # or: deno task test ```bash # Required -curl -fsSL https://deno.land/install.sh | sh # Deno +curl -fsSL https:///install.sh | sh # curl https://sh.rustup.rs -sSf | sh # Rust -npm install -g rescript # ReScript +npm install -g # # Optional (but recommended) cargo install just # just @@ -130,15 +130,15 @@ cargo install just # just ```bash just setup # One-time setup -just build # Build ReScript + WASM +just build # Build + WASM just test # Run tests ``` ### Development Workflow ```bash -# Watch mode (ReScript) -just watch-rescript +# Watch mode () +just watch- # Run CLI during development just cli report @@ -208,7 +208,7 @@ AI-assisted: Architecture design and initial implementation ### Reviewers Look For 1. **Philosophy Alignment**: Does it fit UbiCity's values? -2. **Type Safety**: Proper types in ReScript/TypeScript/Rust? +2. **Type Safety**: Proper types in //Rust? 3. **Tests**: Adequate test coverage? 4. **Documentation**: Clear docs and comments? 5. **Performance**: No obvious performance issues? diff --git a/DENO_MIGRATION_PREVIEW.md b/DENO_MIGRATION_PREVIEW.md index f3933f0..8eec828 100644 --- a/DENO_MIGRATION_PREVIEW.md +++ b/DENO_MIGRATION_PREVIEW.md @@ -1,4 +1,4 @@ -# Deno Migration Preview +# Migration Preview ## Side-by-Side Comparison @@ -41,12 +41,12 @@ class LearningExperience { - CommonJS (legacy) - No compile-time checks -### Deno + TypeScript Version +### + Version -```typescript -// mapper.ts (Deno + TypeScript) -import { z } from "https://deno.land/x/zod/mod.ts"; -import { join } from "https://deno.land/std/path/mod.ts"; +``` +// mapper.ts ( + ) +import { z } from "https:///x/zod/mod.ts"; +import { join } from "https:///std/path/mod.ts"; // Schema with runtime validation const LearningExperienceSchema = z.object({ @@ -148,12 +148,12 @@ switch (mode) { **Run:** `node capture.js quick` -### Deno Version +### Version -```typescript +``` // capture.ts -import { parse } from "https://deno.land/std/flags/mod.ts"; -import { Input, Select } from "https://deno.land/x/cliffy/mod.ts"; +import { parse } from "https:///std/flags/mod.ts"; +import { Input, Select } from "https:///x/cliffy/mod.ts"; // Typed CLI args interface CaptureArgs { @@ -161,7 +161,7 @@ interface CaptureArgs { help?: boolean; } -const args = parse(Deno.args, { +const args = parse(.args, { boolean: ["help"], string: ["mode"], default: { mode: "full" }, @@ -173,17 +173,17 @@ if (args.help) { UbiCity Capture Tool Usage: - deno run --allow-read --allow-write capture.ts [options] + run --allow-read --allow-write capture.ts [options] Options: -m, --mode Capture mode: quick, full, template -h, --help Show this help Examples: - deno run --allow-read --allow-write capture.ts -m quick - deno run --allow-read --allow-write capture.ts + run --allow-read --allow-write capture.ts -m quick + run --allow-read --allow-write capture.ts `); - Deno.exit(0); + .exit(0); } // Better prompts with validation @@ -205,7 +205,7 @@ const locationType = await Select.prompt({ }); ``` -**Run:** `deno run --allow-read --allow-write capture.ts --mode quick` +**Run:** ` run --allow-read --allow-write capture.ts --mode quick` **Benefits:** - Explicit permissions (--allow-read, --allow-write) @@ -235,11 +235,11 @@ files.forEach(file => { - No error handling - Full filesystem access -### Deno Version (Async) +### Version (Async) -```typescript +``` // Non-blocking -await Deno.writeTextFile( +await .writeTextFile( filepath, JSON.stringify(data, null, 2), { create: true } // Explicit creation flag @@ -247,7 +247,7 @@ await Deno.writeTextFile( // Parallel reads with proper error handling const files = []; -for await (const entry of Deno.readDir(experiencesDir)) { +for await (const entry of .readDir(experiencesDir)) { if (entry.isFile && entry.name.endsWith('.json')) { files.push(entry.name); } @@ -256,7 +256,7 @@ for await (const entry of Deno.readDir(experiencesDir)) { // Load all in parallel const experiences = await Promise.all( files.map(async (file) => { - const text = await Deno.readTextFile(join(experiencesDir, file)); + const text = await .readTextFile(join(experiencesDir, file)); return JSON.parse(text); }) ); @@ -277,14 +277,14 @@ const experiences = await Promise.all( // No tests! ``` -### Deno Version +### Version -```typescript +``` // capture_test.ts -import { assertEquals, assertRejects } from "https://deno.land/std/assert/mod.ts"; +import { assertEquals, assertRejects } from "https:///std/assert/mod.ts"; import { LearningExperienceManager } from "./mapper.ts"; -Deno.test("LearningExperience - valid data", () => { +.test("LearningExperience - valid data", () => { const data = { learner: { id: "test-user" }, context: { location: { name: "Test Location" } }, @@ -298,7 +298,7 @@ Deno.test("LearningExperience - valid data", () => { assertEquals(exp.toJSON().learner.id, "test-user"); }); -Deno.test("LearningExperience - invalid data", () => { +.test("LearningExperience - invalid data", () => { const data = { invalid: "data" }; assertRejects( @@ -309,7 +309,7 @@ Deno.test("LearningExperience - invalid data", () => { }); ``` -**Run:** `deno test` +**Run:** ` test` **Built-in:** - Test runner @@ -329,21 +329,21 @@ node mapper.js report node visualize.js ``` -### Deno Version +### Version ```bash cd ubicity # First run (downloads deps) -deno run --allow-read --allow-write capture.ts --mode quick + run --allow-read --allow-write capture.ts --mode quick # Permissions are explicit and visible -deno run --allow-read --allow-write mapper.ts report -deno run --allow-read --allow-write visualize.ts + run --allow-read --allow-write mapper.ts report + run --allow-read --allow-write visualize.ts -# Or create task shortcuts in deno.json -deno task capture:quick -deno task report -deno task visualize +# Or create task shortcuts in .json + task capture:quick + task report + task visualize ``` **Better security:** @@ -358,12 +358,12 @@ deno task visualize ### Time Estimate: 4-6 hours **Step 1: Setup (30 min)** -- Create `deno.json` config +- Create `.json` config - Define import map - Set up tasks **Step 2: Convert mapper.js → mapper.ts (2 hours)** -- Add TypeScript types +- Add types - Implement Zod schema - Convert to async/await - Add tests @@ -387,16 +387,16 @@ deno task visualize ## Recommendation -**If you plan to use UbiCity seriously**, migrate to Deno. +**If you plan to use UbiCity seriously**, migrate to . **If this is a quick experiment**, the Node.js version works fine. The Node.js version captures the essence - it **works**. But it has technical debt that will slow you down as the project evolves. -Given your background (formal methods, Rust experience implied by your interests), you'd likely appreciate Deno's: +Given your background (formal methods, Rust experience implied by your interests), you'd likely appreciate 's: - Type safety - Explicit permissions - Better tooling - No magic -**Shall I migrate it to Deno?** I can do it now while the codebase is small, or create a parallel implementation so you can compare. +**Shall I migrate it to ?** I can do it now while the codebase is small, or create a parallel implementation so you can compare. diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index ddae0ba..9acb74d 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -17,8 +17,8 @@ ____ | Technology | Learn More | **Zig** | https://ziglang.org -| **Deno** | https://deno.land -| **ReScript** | https://rescript-lang.org +| **** | https:// +| **** | https://-lang.org | **Idris2 ABI** | https://www.idris-lang.org |=== diff --git a/FUNDING b/FUNDING new file mode 100644 index 0000000..7e58d67 --- /dev/null +++ b/FUNDING @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MPL-2.0 for code +// SPDX-License-Identifier: CC-BY-SA-4.0 for documentation +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + += Funding +:toc: macro +:toclevels: 2 + +This document lists the supported funding platforms for the hyperpolymath and metadatastician estates. + +== Supported Funding Platforms + +[cols="1,1",options="header"] +|=== +| Platform | Username +| Buy Me a Coffee | jonathan.jewell +| Community Bridge | jonathan-jewell +| GitHub Sponsors | hyperpolymath +| IndieWeb | +| IssueHunt | hyperpolymath +| Ko-fi | hyperpolymath +| LFX Crowdfunding | hyperpolymath +| LiberaPay | hyperpolymath +| Open Collective | jonathan-jewell +| Patreon | cc_studio +| Polar | hyperpolymath +| Thanks Dev | hyperpolymath +|=== + +== Usage + +These platforms provide financial support mechanisms for the projects within the hyperpolymath and metadatastician estates. Contributions through any of these platforms help sustain development, maintenance, and governance of the open source projects. + +For more information about contributing or sponsoring specific projects, please refer to the project's README file or contact the maintainers directly. diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 5713ec7..f490a82 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -24,7 +24,7 @@ cd ubicity node --version # Should be v18 or higher # Test the CLI -node src-rescript/CaptureCLI.res.js template +node src-/CaptureCLI.res.js template ``` That's it! No \`npm install\` needed. @@ -36,7 +36,7 @@ That's it! No \`npm install\` needed. The fastest way to start is with **quick mode**: ```bash -node src-rescript/CaptureCLI.res.js quick +node src-/CaptureCLI.res.js quick ``` You'll be prompted for: @@ -83,7 +83,7 @@ UbiCity offers three capture modes: ### Quick Mode (Default) ```bash -node src-rescript/CaptureCLI.res.js quick +node src-/CaptureCLI.res.js quick ``` - Minimal prompts (WHO/WHERE/WHAT only) - Fastest capture (~30 seconds) @@ -91,14 +91,14 @@ node src-rescript/CaptureCLI.res.js quick ### Full Mode ```bash -node src-rescript/CaptureCLI.res.js full +node src-/CaptureCLI.res.js full ``` - Includes all optional fields - Detailed capture (~2-3 minutes) ### Template Mode ```bash -node src-rescript/CaptureCLI.res.js template +node src-/CaptureCLI.res.js template ``` - Generates a JSON template - Edit offline and import later diff --git a/MAINTAINERS.adoc b/MAINTAINERS.adoc index 865ca21..6d29551 100644 --- a/MAINTAINERS.adoc +++ b/MAINTAINERS.adoc @@ -32,7 +32,7 @@ Maintainership is earned through consistent, high-quality contributions that dem . *Deep Understanding*: Of UbiCity's philosophy and architecture . *Quality Contributions*: Sustained high-quality PRs over 6{plus} months . *Community Trust*: Respectful, helpful engagement with contributors -. *Technical Expertise*: In one or more core areas (Deno, ReScript, Rust/WASM) +. *Technical Expertise*: In one or more core areas (, , Rust/WASM) . *Judgment*: Good decision-making aligned with project values ==== Process @@ -159,7 +159,7 @@ In rare cases, maintainers may be removed for: ==== Areas Needing Maintainers [arabic] -. *ReScript Expert*: Focus on `src-rescript/` optimization +. * Expert*: Focus on `src-/` optimization . *WASM/Rust*: Focus on `wasm/` performance . *Documentation*: Improve guides, tutorials, API docs . *Community*: Manage discussions, welcome newcomers diff --git a/MIGRATION_V3.md b/MIGRATION_V3.md index e331c0b..853f084 100644 --- a/MIGRATION_V3.md +++ b/MIGRATION_V3.md @@ -3,10 +3,10 @@ ## TL;DR v0.3 is a **complete architectural rewrite** using modern tooling: -- **Deno** replaces Node.js/npm -- **ReScript** for type-safe business logic +- **** replaces Node.js/npm +- **** for type-safe business logic - **WASM** (Rust) for performance-critical code -- **TypeScript** as glue layer +- **** as glue layer Your **data remains 100% compatible**. @@ -14,7 +14,7 @@ Your **data remains 100% compatible**. ## Breaking Changes -### 1. Runtime: Node.js → Deno +### 1. Runtime: Node.js → **Before (v0.2)**: ```bash @@ -25,21 +25,21 @@ node src/cli.js report **After (v0.3)**: ```bash # No installation needed! -deno task report + task report # Or use just (recommended) just report ``` -### 2. Module System: npm packages → Deno/JSR +### 2. Module System: npm packages → /JSR **Before (v0.2)**: -```typescript +``` import { createMapper } from './src/index.js'; ``` **After (v0.3)**: -```typescript +``` import { createMapper } from './src/index.ts'; // .ts extension ``` @@ -51,10 +51,10 @@ import { createMapper } from './src/index.ts'; // .ts extension - Zod for validation **After (v0.3)**: -- deno.json with JSR imports +- .json with JSR imports - No node_modules - WASM for validation (10-100x faster) -- ReScript for business logic +- for business logic ### 4. Build Process @@ -63,12 +63,12 @@ No build needed (pure JS) **After (v0.3)**: ```bash -# Build ReScript + WASM +# Build + WASM just build # Or separate -deno task rescript:build -deno task wasm:build + task :build + task wasm:build ``` --- @@ -77,9 +77,9 @@ deno task wasm:build ### Prerequisites -1. **Deno** (required) +1. **** (required) ```bash -curl -fsSL https://deno.land/install.sh | sh +curl -fsSL https:///install.sh | sh ``` 2. **Rust** (for WASM compilation) @@ -88,9 +88,9 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32-unknown-unknown ``` -3. **ReScript** (for business logic compilation) +3. **** (for business logic compilation) ```bash -npm install -g rescript # One-time global install +npm install -g # One-time global install ``` 4. **just** (optional but recommended) @@ -105,9 +105,9 @@ cargo install just just setup just build -# Or using Deno tasks -deno task rescript:build -deno task wasm:build +# Or using tasks + task :build + task wasm:build ``` --- @@ -131,7 +131,7 @@ deno task wasm:build ### Memory Usage **Before (v0.2)**: ~50MB for 1000 experiences -**After (v0.3)**: ~20MB for 1000 experiences (ReScript optimization) +**After (v0.3)**: ~20MB for 1000 experiences ( optimization) **60% less memory** @@ -146,12 +146,12 @@ just compile ./bin/ubicity report # Standalone binary! ``` -No Deno runtime needed for deployment. +No runtime needed for deployment. ### 2. Type Safety -ReScript provides **compile-time type checking**: -```rescript + provides **compile-time type checking**: +``` // This won't compile if types don't match let experience = LearningExperience.make( ~learner=invalidLearner, // Compile error! @@ -163,7 +163,7 @@ let experience = LearningExperience.make( ### 3. Functional Programming -```rescript +``` // Immutable data, pure functions let interdisciplinary = experiences ->Analysis.findInterdisciplinary @@ -173,8 +173,8 @@ let interdisciplinary = experiences ### 4. Zero Config -No tsconfig.json, no webpack, no babel. -Just `deno.json` and you're done. +No onfig.json, no webpack, no babel. +Just `.json` and you're done. --- @@ -183,15 +183,15 @@ Just `deno.json` and you're done. ### Step 1: Install Prerequisites ```bash -# Deno -curl -fsSL https://deno.land/install.sh | sh +# +curl -fsSL https:///install.sh | sh # Rust (for WASM) curl https://sh.rustup.rs -sSf | sh rustup target add wasm32-unknown-unknown -# ReScript -npm install -g rescript +# +npm install -g # just (optional) cargo install just @@ -214,7 +214,7 @@ just stats # Should show your existing experiences ### Step 4: Run Tests ```bash -just test # Or: deno task test +just test # Or: task test ``` ### Step 5: Update Your Scripts @@ -247,14 +247,14 @@ The JSON schema is identical. Only the runtime changed. ## Troubleshooting -### "Command not found: deno" +### "Command not found: " -Install Deno: +Install : ```bash -curl -fsSL https://deno.land/install.sh | sh +curl -fsSL https:///install.sh | sh # Add to PATH -echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc +echo 'export PATH="$HOME/./bin:$PATH"' >> ~/.bashrc source ~/.bashrc ``` @@ -265,23 +265,23 @@ Install Rust and wasm32 target: rustup target add wasm32-unknown-unknown ``` -### "ReScript not found" +### " not found" Install globally: ```bash -npm install -g rescript +npm install -g ``` ### "Permission denied" -Deno requires explicit permissions: + requires explicit permissions: ```bash -deno run --allow-read --allow-write src/cli.ts report + run --allow-read --allow-write src/cli.ts report ``` Or use the tasks (permissions pre-configured): ```bash -deno task report + task report ``` --- @@ -304,7 +304,7 @@ Your data works with both versions. ### Benchmark Results -| Operation | v0.2 (Node.js) | v0.3 (Deno+WASM) | Improvement | +| Operation | v0.2 (Node.js) | v0.3 (+WASM) | Improvement | |-----------|----------------|------------------|-------------| | Validation | 1.2ms | 0.012ms | 100x | | Load 1000 | 45ms | 15ms | 3x | @@ -318,28 +318,28 @@ Your data works with both versions. ``` ┌─────────────────────────────────────────────┐ -│ Deno Runtime (TypeScript) │ +│ Runtime () │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ CLI/API │◄──────►│ Storage │ │ │ └──────┬───────┘ └──────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────┐ │ -│ │ TypeScript Glue Layer │ │ +│ │ Glue Layer │ │ │ └────┬──────────────────┬──────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ │ -│ │ ReScript │ │ WASM │ │ +│ │ │ │ WASM │ │ │ │ Business │ │ Performance│ │ │ │ Logic │ │ Critical │ │ │ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────┘ ``` -**ReScript**: Type-safe business logic (functional) +****: Type-safe business logic (functional) **WASM (Rust)**: Validation, analysis, computations -**TypeScript**: Glue layer, I/O, CLI +****: Glue layer, I/O, CLI --- @@ -355,25 +355,25 @@ just viz ### Explore the Code -- `src-rescript/UbiCity.res` - Functional business logic +- `src-/UbiCity.res` - Functional business logic - `wasm/src/lib.rs` - Performance-critical Rust -- `src/storage.ts` - Deno file I/O +- `src/storage.ts` - file I/O - `src/*-bridge.ts` - Integration glue ### Read the Docs - `README.md` - Updated architecture overview - `justfile` - All available commands -- `deno.json` - Configuration and tasks +- `.json` - Configuration and tasks --- ## FAQ -**Q: Why Deno over Node.js?** -A: Built-in TypeScript, secure by default, modern tooling, no node_modules. +**Q: Why over Node.js?** +A: Built-in , secure by default, modern tooling, no node_modules. -**Q: Why ReScript over TypeScript?** +**Q: Why over ?** A: Stronger type system, better optimization, functional programming, OCaml heritage. **Q: Why WASM?** diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index 3262ec3..d8d9d7c 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -38,14 +38,14 @@ This document defines the formal release process for UbiCity, ensuring quality, - [ ] Create release branch: `release/vX.Y.Z` - [ ] Update `CHANGELOG.md` with all changes -- [ ] Update version in `deno.json` +- [ ] Update version in `.json` - [ ] Update version in `wasm/Cargo.toml` -- [ ] Update version in `src-rescript/package.json` -- [ ] Run full test suite: `deno task test` -- [ ] Run benchmarks: `deno task bench` +- [ ] Update version in `src-/package.json` +- [ ] Run full test suite: ` task test` +- [ ] Run benchmarks: ` task bench` - [ ] Verify performance SLOs met - [ ] Run security audit: `./security/audit.sh` -- [ ] Check test coverage ≥ 95%: `deno coverage` +- [ ] Check test coverage ≥ 95%: ` coverage` - [ ] Verify offline-first (no network calls) - [ ] Test all export formats (CSV, GeoJSON, DOT, Markdown) - [ ] Verify data migration (if major/minor) @@ -56,7 +56,7 @@ This document defines the formal release process for UbiCity, ensuring quality, ### 2. Release Candidate (RC) - [ ] Tag RC: `git tag vX.Y.Z-rc.1` -- [ ] Build binaries: `deno task compile` +- [ ] Build binaries: ` task compile` - [ ] Test binaries on all platforms (Linux, macOS, Windows) - [ ] Smoke test: fresh install, capture experience, export - [ ] Beta testing (internal or community) @@ -92,9 +92,9 @@ This document defines the formal release process for UbiCity, ensuring quality, ### Compiled Binaries ```bash # Compile for all platforms -deno task compile:cli # ubicity CLI -deno task compile:capture # capture tool -deno task compile:wasm # WASM module + task compile:cli # ubicity CLI + task compile:capture # capture tool + task compile:wasm # WASM module ``` **Platforms**: @@ -222,9 +222,9 @@ Following [Keep a Changelog](https://keepachangelog.com/): - ✅ Test coverage ≥ 95% - ✅ Benchmarks meet SLOs - ✅ Security audit passes -- ✅ No TypeScript errors (`deno check`) -- ✅ No lint warnings (`deno lint`) -- ✅ Format check passes (`deno fmt --check`) +- ✅ No errors (` check`) +- ✅ No lint warnings (` lint`) +- ✅ Format check passes (` fmt --check`) ### Regression Testing - Test data migration from previous versions @@ -319,10 +319,10 @@ release: only: - tags script: - - deno task test - - deno task bench + - task test + - task bench - ./security/audit.sh - - deno task compile + - task compile - ./scripts/checksum.sh - ./scripts/sign.sh - gh release create $CI_COMMIT_TAG bin/* diff --git a/RSR_COMPLIANCE.adoc b/RSR_COMPLIANCE.adoc index 98af83a..9e2bf8c 100644 --- a/RSR_COMPLIANCE.adoc +++ b/RSR_COMPLIANCE.adoc @@ -68,7 +68,7 @@ cat .well-known/humans.txt | grep "TEAM:" === 3. Build System (4/4) ✅ * [x] Justfile (build orchestration) -* [x] deno.json (Deno tasks) +* [x] .json ( tasks) * [x] flake.nix (Nix reproducible builds) * [x] .gitlab-ci.yml (CI/CD pipeline with RSR verification) @@ -78,16 +78,16 @@ _Verification_: [source,bash] ---- just --list -deno task --help + task --help nix flake check gitlab-ci-local --list ---- === 4. Type Safety (4/4) ✅ -* [x] _Compile-time types_: ReScript (`src-rescript/UbiCity.res`) +* [x] _Compile-time types_: (`src-/UbiCity.res`) * [x] _Memory safety_: Rust/WASM (`wasm/src/lib.rs`) -* [x] _TypeScript_: Glue layer (`src/**/*.ts`) +* [x] __: Glue layer (`src/**/*.ts`) * [x] _Zero unsafe blocks_: WASM verified safe _Score_: 100% @@ -95,11 +95,11 @@ _Score_: 100% _Evidence_: [source,bash] ---- -# ReScript type checking -rescript build +# type checking + build -# TypeScript type checking -deno check src/**/*.ts +# type checking + check src/**/*.ts # Rust safety (zero unsafe blocks) cd wasm && cargo clippy -- -D warnings @@ -112,7 +112,7 @@ _Platinum Requirement: >95% Coverage_ * [x] Comprehensive unit tests (`tests/*.test.ts`) * [x] All tests pass (100% pass rate) -* [x] Test runner configured (`deno test`) +* [x] Test runner configured (` test`) * [x] _>95% coverage_ (Platinum requirement met) _Test Suite_: @@ -128,11 +128,11 @@ _Verification_: [source,bash] ---- # Run all tests -deno test --allow-read --allow-write tests/ + test --allow-read --allow-write tests/ # Measure coverage (>95% target) -deno test --coverage=coverage --allow-read --allow-write -deno coverage coverage + test --coverage=coverage --allow-read --allow-write + coverage coverage # Expected: >95% line coverage ---- @@ -161,7 +161,7 @@ _Score_: 100% ✅ _Verification_: [source,bash] ---- -deno bench --allow-read --allow-write benchmarks/ + bench --allow-read --allow-write benchmarks/ ---- === 7. Security (8/8) ✅ @@ -171,7 +171,7 @@ _Platinum Requirement: Comprehensive Security_ * [x] security.txt (RFC 9116 CVE disclosure) * [x] THREAT_MODEL.md (formal threat analysis) * [x] security/audit.sh (automated security scanning) -* [x] Deno permissions (explicit `--allow-*`) +* [x] permissions (explicit `--allow-*`) * [x] WASM sandboxing (isolated linear memory) * [x] No hardcoded secrets * [x] Cargo audit (Rust dependency CVEs) @@ -186,7 +186,7 @@ _Verification_: ./security/audit.sh # Should output: -# ✅ Deno dependencies verified +# ✅ dependencies verified # ✅ Rust security audit complete # ✅ Trivy filesystem scan complete # ✅ Type safety verified @@ -247,7 +247,7 @@ _Score_: 100% === 12. Reproducibility (3/3) ✅ * [x] _Nix flake_: `flake.nix` for reproducible builds -* [x] _Locked dependencies_: Deno caches exact versions +* [x] _Locked dependencies_: caches exact versions * [x] _CI/CD_: GitLab CI verifies builds _Score_: 100% @@ -269,7 +269,7 @@ ls -la src/i18n/ # Should show: en.json, es.json # Test language switching -UBICITY_LANG=es deno task capture +UBICITY_LANG=es task capture ---- === 14. Accessibility (4/4) ✅ @@ -379,7 +379,7 @@ What sets Platinum apart from Gold: . _Threat Model_ - Comprehensive security analysis . _Performance SLOs_ - Defined and enforced . _Comprehensive API Docs_ - Auto-generated + manual -. _Security Automation_ - Multi-layer scanning (Trivy, cargo audit, Deno) +. _Security Automation_ - Multi-layer scanning (Trivy, cargo audit, ) ''' @@ -393,9 +393,9 @@ _Automated Verification_ (CI/CD): verify:rsr-compliance: script: - ./security/audit.sh - - deno test --coverage - - deno bench - - deno check src/**/*.ts + - test --coverage + - bench + - check src/**/*.ts - test -f THREAT_MODEL.md - test -f RELEASE_PROCESS.md - test -f ACCESSIBILITY.md @@ -420,16 +420,16 @@ ls -la README.md LICENSE.txt CONTRIBUTING.md CODE_OF_CONDUCT.md \ ACCESSIBILITY.md docs/API.md .well-known/*.txt # 2. Type safety -deno check src/**/*.ts -rescript build + check src/**/*.ts + build cd wasm && cargo clippy -- -D warnings # 3. Tests (>95% coverage) -deno test --coverage=coverage --allow-read --allow-write -deno coverage coverage # Check >95% + test --coverage=coverage --allow-read --allow-write + coverage coverage # Check >95% # 4. Benchmarks (SLOs met) -deno bench --allow-read --allow-write + bench --allow-read --allow-write # 5. Security audit ./security/audit.sh @@ -521,7 +521,7 @@ _Comparison to Other Tiers_: _To Maintain Platinum Tier_: . _Quarterly review_ of this document -. _Update dependencies_ (Deno, Rust crates) +. _Update dependencies_ (, Rust crates) . _Keep test coverage >95%_ . _Security audits_ on each release . _Update threat model_ when architecture changes diff --git a/RSR_OUTLINE.adoc b/RSR_OUTLINE.adoc index 9f32374..2986784 100644 --- a/RSR_OUTLINE.adoc +++ b/RSR_OUTLINE.adoc @@ -146,7 +146,7 @@ project/ === Language Tiers -* **Tier 1** (Gold): Rust, Elixir, Zig, Ada, Haskell, ReScript +* **Tier 1** (Gold): Rust, Elixir, Zig, Ada, Haskell, * **Tier 2** (Silver): Nickel, Racket, Guile Scheme, Nix * **Infrastructure**: Guix channels, derivations @@ -166,7 +166,7 @@ project/ === Prohibited * Python outside `salt/` directory -* TypeScript/JavaScript (use ReScript) +* /JavaScript (use ) * CUE (use Guile/Nickel) * `Dockerfile` (use `Containerfile`) diff --git a/SECURITY.md b/SECURITY.md index b163982..89734e6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -21,9 +21,9 @@ Instead, please: ## Security Measures - **Memory Safety**: WASM (Rust) provides memory safety guarantees -- **Type Safety**: ReScript compile-time types +- **Type Safety**: compile-time types - **Sandboxing**: WASM runs in isolated linear memory -- **Permissions**: Deno explicit permissions (`--allow-read`, `--allow-write`) +- **Permissions**: explicit permissions (`--allow-read`, `--allow-write`) - **Data Privacy**: Local-first, no network calls, no telemetry - **Offline-First**: Works completely air-gapped diff --git a/STACK_ANALYSIS.md b/STACK_ANALYSIS.md index 8f0bffc..3381f0e 100644 --- a/STACK_ANALYSIS.md +++ b/STACK_ANALYSIS.md @@ -6,7 +6,7 @@ - **Runtime**: Node.js v22.21.1 - **Module System**: CommonJS (`require()`) - **Package Manager**: npm (though no dependencies currently) -- **Language**: Plain JavaScript (no TypeScript) +- **Language**: Plain JavaScript (no ) ### Dependencies **Zero external dependencies** - Only uses Node.js standard library: @@ -42,10 +42,10 @@ ubicity/ **Impact**: Technical debt, harder to integrate with modern tools -#### 2. **No Deno Support** -- You likely prefer Deno based on your question -- Deno offers: - - TypeScript by default +#### 2. **No Support** +- You likely prefer based on your question +- offers: + - by default - Secure by default (explicit permissions) - No node_modules - Built-in formatter/linter/test runner @@ -70,7 +70,7 @@ ubicity/ ### 🟡 Medium Issues -#### 5. **No TypeScript** +#### 5. **No ** - Plain JavaScript means no type safety - Schema exists as JSON but not as types - Runtime errors instead of compile-time checks @@ -140,13 +140,13 @@ ubicity/ ### Option A: Modern Node.js + ESM -```typescript -// Modern Node.js with TypeScript +``` +// Modern Node.js with import { readFile, writeFile } from 'node:fs/promises'; import { z } from 'zod'; // Runtime validation import { Command } from 'commander'; // CLI framework -// Schema as TypeScript type + Zod validator +// Schema as type + Zod validator const LearningExperienceSchema = z.object({ learner: z.object({ id: z.string() }), context: z.object({ @@ -164,26 +164,26 @@ type LearningExperience = z.infer; **Pros**: Incremental migration path, familiar ecosystem **Cons**: Still Node.js quirks, still need npm -### Option B: Deno + TypeScript (Recommended) +### Option B: + (Recommended) -```typescript -// Deno with TypeScript -import { z } from "https://deno.land/x/zod/mod.ts"; -import { parse } from "https://deno.land/std/flags/mod.ts"; +``` +// with +import { z } from "https:///x/zod/mod.ts"; +import { parse } from "https:///std/flags/mod.ts"; // Built-in permissions -await Deno.writeTextFile("./data.json", json, { +await .writeTextFile("./data.json", json, { create: true }); // Fails without --allow-write // Built-in formatter, linter, test runner -// deno fmt, deno lint, deno test +// fmt, lint, test // No node_modules, dependencies from URLs ``` **Pros**: -- TypeScript native +- native - Better security model - Modern standard library - No build step needed @@ -232,18 +232,18 @@ struct LearningExperience { **Result**: Better but still Node.js -### Path 2: Deno Migration (4-6 hours) -1. Create `deno.json` config -2. Convert all files to Deno imports -3. Add TypeScript types -4. Use Deno standard library +### Path 2: Migration (4-6 hours) +1. Create `.json` config +2. Convert all files to imports +3. Add types +4. Use standard library 5. Add permissions flags -**Result**: Modern, secure, TypeScript +**Result**: Modern, secure, ### Path 3: Hybrid Approach (2-3 hours) 1. Keep core mapper in Node.js (matches existing repo) -2. Add Deno wrapper scripts for new features +2. Add wrapper scripts for new features 3. Use FFI or JSON pipe for communication **Result**: Best of both worlds, incremental @@ -260,8 +260,8 @@ Given your concerns and the project goals: 3. **Add basic tests** - At least smoke tests ### Short-term (This Week): -4. **Port to Deno** - Better DX, matches your style -5. **Add TypeScript** - Type safety for the schema +4. **Port to ** - Better DX, matches your style +5. **Add ** - Type safety for the schema 6. **Add CLI framework** - Better UX ### Long-term (When Needed): @@ -275,16 +275,16 @@ Given your concerns and the project goals: **Do you want me to:** -**A)** Migrate everything to Deno + TypeScript now? +**A)** Migrate everything to + now? **B)** Quick fixes to the Node.js version (ESM + validation)? -**C)** Create a parallel Deno implementation you can compare? +**C)** Create a parallel implementation you can compare? **D)** Document the issues but leave as-is (working prototype)? The current version **works** but has technical debt. Given that UbiCity is about starting quickly and iterating, option D might align best with the Minimal Viable Protocol philosophy - use it, see if it works for you, then invest in better infrastructure only if needed. -But if you have strong preferences (especially Deno), I should migrate now before you build habits around the Node.js version. +But if you have strong preferences (especially ), I should migrate now before you build habits around the Node.js version. **What's your preference?** diff --git a/TEST-NEEDS.md b/TEST-NEEDS.md index b0d76d8..d2be960 100644 --- a/TEST-NEEDS.md +++ b/TEST-NEEDS.md @@ -6,7 +6,7 @@ | Category | Count | Notes | |----------|-------|-------| -| TypeScript/JavaScript tests | 10+ | Multiple `.test.ts` and `.test.js` files | +| /JavaScript tests | 10+ | Multiple `.test.ts` and `.test.js` files | | Test subjects | 5 | Core, export, mapper, privacy, schemas | | Test framework | Present | Jest-compatible | | Spec directory | Present | `spec/` for specification documents | diff --git a/TESTING-REPORT.adoc b/TESTING-REPORT.adoc index 3b8cfd0..8f1af58 100644 --- a/TESTING-REPORT.adoc +++ b/TESTING-REPORT.adoc @@ -6,25 +6,25 @@ == Executive Summary -This report documents the testing and fixes applied to the UbiCity project to enable Deno compatibility. The project has been migrated from Node.js-specific patterns to Deno-compatible code while maintaining full functionality. +This report documents the testing and fixes applied to the UbiCity project to enable compatibility. The project has been migrated from Node.js-specific patterns to -compatible code while maintaining full functionality. == Environment * *Platform*: Linux 6.17.12-300.fc43.x86_64 -* *Deno Version*: 1.45.0 +* * Version*: 1.45.0 * *Date*: 2025-12-29 * *Project*: ubicity * *Location*: /var$HOME/repos/ubicity == Issues Fixed -=== 1. deno.json Configuration Errors +=== 1. .json Configuration Errors -*Problem*: The `deno.json` configuration referenced `.ts` files that did not exist. The actual source files were `.js`. +*Problem*: The `.json` configuration referenced `.ts` files that did not exist. The actual source files were `.js`. *Files Affected*: -* `deno.json` - exports and task definitions +* `.json` - exports and task definitions *Fix Applied*: @@ -35,7 +35,7 @@ This report documents the testing and fixes applied to the UbiCity project to en === 2. Node.js Import Compatibility -*Problem*: Source files used bare Node.js module imports without the `node:` prefix, which is required in Deno. +*Problem*: Source files used bare Node.js module imports without the `node:` prefix, which is required in . *Files Fixed*: @@ -49,21 +49,21 @@ This report documents the testing and fixes applied to the UbiCity project to en === 3. CLI Scripts Node.js Globals -*Problem*: CLI scripts used Node.js global variables (`process.argv`, `process.exit`) which are not available in Deno. +*Problem*: CLI scripts used Node.js global variables (`process.argv`, `process.exit`) which are not available in . *Files Fixed*: * `/var$HOME/repos/ubicity/src/cli.js`: -** Changed `process.argv.slice(2)` to `Deno.args` -** Changed `process.exit()` to `Deno.exit()` -** Updated shebang to `#!/usr/bin/env -S deno run --allow-read --allow-write` +** Changed `process.argv.slice(2)` to `.args` +** Changed `process.exit()` to `.exit()` +** Updated shebang to `#!/usr/bin/env -S run --allow-read --allow-write` * `/var$HOME/repos/ubicity/src/capture.js`: -** Changed `process.argv[2]` to `Deno.args[0]` -** Changed `process.exit()` to `Deno.exit()` +** Changed `process.argv[2]` to `.args[0]` +** Changed `process.exit()` to `.exit()` ** Changed `import.meta.url === \`file://${process.argv[1]}\`` to `import.meta.main` -** Updated shebang to `#!/usr/bin/env -S deno run --allow-read --allow-write` -** Changed stdin/stdout from `node:process` to `Deno.stdin`/`Deno.stdout` +** Updated shebang to `#!/usr/bin/env -S run --allow-read --allow-write` +** Changed stdin/stdout from `node:process` to `.stdin`/`.stdout` == Test Results @@ -175,14 +175,14 @@ All 44 tests pass successfully. [source,shell] ---- -deno task start # Run CLI -deno task capture # Capture a learning experience -deno task report # Generate analysis report -deno task hotspots # Find learning hotspots -deno task stats # Show storage statistics -deno task test # Run tests -deno task fmt # Format code -deno task lint # Lint code + task start # Run CLI + task capture # Capture a learning experience + task report # Generate analysis report + task hotspots # Find learning hotspots + task stats # Show storage statistics + task test # Run tests + task fmt # Format code + task lint # Lint code ---- === CLI Help Output @@ -222,18 +222,18 @@ Storage Statistics: == Formatting -Code has been formatted with `deno fmt`. All source files are now consistent with Deno's formatting standards. +Code has been formatted with ` fmt`. All source files are now consistent with 's formatting standards. == Recommendations -1. *Consider TypeScript Migration*: The project uses JavaScript with JSDoc. Converting to TypeScript would improve type safety. +1. *Consider Migration*: The project uses JavaScript with JSDoc. Converting to would improve type safety. 2. *Address Lint Warnings*: Remove unused imports and fix async function declarations. -3. *Add CI/CD*: Set up GitHub Actions with Deno for automated testing. +3. *Add CI/CD*: Set up GitHub Actions with for automated testing. -4. *Version Pin Dependencies*: The deno.json uses `^` version constraints; consider pinning for reproducibility. +4. *Version Pin Dependencies*: The .json uses `^` version constraints; consider pinning for reproducibility. == Conclusion -The UbiCity project has been successfully tested and fixed for Deno compatibility. All 44 tests pass, the CLI works correctly, and the codebase has been formatted. The project is now fully functional with Deno 1.45.0. +The UbiCity project has been successfully tested and fixed for compatibility. All 44 tests pass, the CLI works correctly, and the codebase has been formatted. The project is now fully functional with 1.45.0. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index e8ad185..50f2cd4 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -22,13 +22,13 @@ UbiCity is a privacy-first learning capture system. This threat model identifies │ v ┌─────────────┐ -│ CLI │ (TypeScript/Deno runtime) +│ CLI │ (/ runtime) │ (src/*.ts) │ └──────┬──────┘ │ v ┌─────────────┐ -│ Validator │ (Rust/WASM, ReScript) +│ Validator │ (Rust/WASM, ) │ (WASM) │ └──────┬──────┘ │ @@ -59,7 +59,7 @@ UbiCity is a privacy-first learning capture system. This threat model identifies **Likelihood**: Medium **Impact**: Medium (privacy violation) -### 4. WASM/ReScript Exploit +### 4. WASM/ Exploit **Motivation**: Code execution, memory corruption **Capability**: Exploit WASM sandbox escape **Likelihood**: Very Low @@ -97,8 +97,8 @@ UbiCity is a privacy-first learning capture system. This threat model identifies **Mitigations**: - ✅ WASM validation (integrity checks) -- ✅ Deno permissions (`--allow-write` limited to data dir) -- ✅ TypeScript compile-time checks +- ✅ permissions (`--allow-write` limited to data dir) +- ✅ compile-time checks - ⚠️ Cryptographic signatures (not implemented - future) **Risk**: LOW @@ -110,12 +110,12 @@ UbiCity is a privacy-first learning capture system. This threat model identifies **Threat**: Malicious dependency exfiltrates data **Attack Vector**: - Compromised npm package -- Malicious Deno module +- Malicious module - Backdoored compiler **Mitigations**: - ✅ ZERO npm dependencies (production) -- ✅ Deno JSR registry (cryptographically signed) +- ✅ JSR registry (cryptographically signed) - ✅ Nix reproducible builds (pinned dependencies) - ✅ GitLab CI verification on every commit - ✅ `cargo audit` for Rust dependencies @@ -133,7 +133,7 @@ UbiCity is a privacy-first learning capture system. This threat model identifies - Shell metacharacters **Mitigations**: -- ✅ Deno sandboxing (explicit permissions) +- ✅ sandboxing (explicit permissions) - ✅ Path validation (no directory traversal) - ✅ Input sanitization (Zod schemas) - ✅ No `eval()` or dynamic code execution @@ -165,14 +165,14 @@ UbiCity is a privacy-first learning capture system. This threat model identifies **Threat**: WASM code escapes sandbox, accesses host system **Attack Vector**: -- WASM exploit (CVE in Deno's V8 engine) +- WASM exploit (CVE in 's V8 engine) - Unsafe Rust code in WASM module **Mitigations**: -- ✅ Deno WASM sandbox (linear memory isolation) +- ✅ WASM sandbox (linear memory isolation) - ✅ Zero `unsafe` blocks in Rust code - ✅ `cargo clippy` enforces safety -- ✅ Deno auto-updates (security patches) +- ✅ auto-updates (security patches) - ⚠️ Regular dependency updates **Risk**: VERY LOW @@ -202,7 +202,7 @@ UbiCity is a privacy-first learning capture system. This threat model identifies ### Capture Flow ``` -User Input → Deno CLI → WASM Validator → JSON File +User Input → CLI → WASM Validator → JSON File ↓ ↓ ↓ ↓ [PII?] [Sanitize] [Validate] [Encrypt?] ``` @@ -228,7 +228,7 @@ JSON Files → Mapper → Privacy Filter → Export (CSV/GeoJSON/DOT) ## Security Controls ### Preventive Controls -- ✅ Deno explicit permissions (`--allow-read`, `--allow-write`) +- ✅ explicit permissions (`--allow-read`, `--allow-write`) - ✅ WASM sandboxing (linear memory isolation) - ✅ Input validation (Zod + WASM validators) - ✅ Zero npm dependencies (supply chain risk reduction) @@ -332,7 +332,7 @@ Found a vulnerability? See `.well-known/security.txt` - [OWASP Threat Modeling](https://owasp.org/www-community/Threat_Modeling) - [NIST SP 800-154 (Data Integrity)](https://csrc.nist.gov/publications/detail/sp/800-154/draft) -- [Deno Security Model](https://deno.land/manual/runtime/permission_apis) +- [ Security Model](https:///manual/runtime/permission_apis) - [WASM Security](https://webassembly.org/docs/security/) --- diff --git a/TOPOLOGY.md b/TOPOLOGY.md index a24cf1a..6eb4ff4 100644 --- a/TOPOLOGY.md +++ b/TOPOLOGY.md @@ -59,7 +59,7 @@ CORE CAPTURE & DATA INTERFACES & VIZ Voyant Export ██████████ 100% Text analysis bridge active - ReScript UI (src-rescript) ██████░░░░ 60% Web dashboard in progress + UI (src-) ██████░░░░ 60% Web dashboard in progress D3.js Learning Maps ████░░░░░░ 40% Spatial viz prototyping REPO INFRASTRUCTURE diff --git a/benchmarks/io.bench.ts b/benchmarks/io.bench.ts deleted file mode 100644 index ced6ff9..0000000 --- a/benchmarks/io.bench.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * I/O performance benchmarks - * SLO: < 50ms for 1000 experience read/write - */ - -import { ensureDir } from '@std/fs'; -import { join } from '@std/path'; - -const BENCH_DATA_DIR = './bench-data-tmp'; - -Deno.bench('I/O - Write single experience', async () => { - await ensureDir(BENCH_DATA_DIR); - - const exp = { - id: crypto.randomUUID(), - timestamp: new Date().toISOString(), - data: 'test', - }; - - const file = join(BENCH_DATA_DIR, `${exp.id}.json`); - await Deno.writeTextFile(file, JSON.stringify(exp)); - - // Cleanup - await Deno.remove(file); -}); - -Deno.bench( - 'I/O - Read single experience', - { group: 'io', baseline: true }, - async () => { - await ensureDir(BENCH_DATA_DIR); - - const file = join(BENCH_DATA_DIR, 'test.json'); - await Deno.writeTextFile(file, JSON.stringify({ id: 'test' })); - - const content = await Deno.readTextFile(file); - JSON.parse(content); - - await Deno.remove(file); - }, -); - -Deno.bench('I/O - Batch write 10 experiences', async () => { - await ensureDir(BENCH_DATA_DIR); - - const experiences = Array.from({ length: 10 }, (_, i) => ({ - id: `exp-${i}`, - data: 'test', - })); - - await Promise.all( - experiences.map((exp) => - Deno.writeTextFile( - join(BENCH_DATA_DIR, `${exp.id}.json`), - JSON.stringify(exp), - ) - ), - ); - - // Cleanup - await Deno.remove(BENCH_DATA_DIR, { recursive: true }); -}); - -/** - * Performance SLOs - * - * - Single write: < 5ms - * - Single read: < 5ms - * - Batch 10 write: < 50ms - * - Batch 100 read: < 200ms - */ diff --git a/benchmarks/mapper.bench.ts b/benchmarks/mapper.bench.ts deleted file mode 100644 index af4216d..0000000 --- a/benchmarks/mapper.bench.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Mapper performance benchmarks - * SLO: < 10ms for network generation (100 experiences) - */ - -Deno.bench('Mapper - Hotspot detection (100 experiences)', () => { - const experiences = Array.from({ length: 100 }, (_, i) => ({ - id: `exp-${i}`, - context: { - location: { - name: `Location ${i % 10}`, // 10 locations, ~10 exp each - coordinates: { - lat: 37.77 + (i % 10) * 0.01, - lon: -122.42 + (i % 10) * 0.01, - }, - }, - }, - experience: { - domain: [`domain-${i % 5}`], - }, - })); - - const byLocation = experiences.reduce((acc, exp) => { - const loc = exp.context.location.name; - if (!acc[loc]) acc[loc] = []; - acc[loc].push(exp); - return acc; - }, {} as Record); - - const _hotspots = Object.entries(byLocation).filter(([_, exps]) => - exps.length >= 5 - ); -}); - -Deno.bench('Mapper - Domain network generation (100 experiences)', () => { - const experiences = Array.from({ length: 100 }, (_, i) => ({ - id: `exp-${i}`, - experience: { - domain: [ - `domain-${i % 5}`, - `domain-${(i + 1) % 5}`, - ], - }, - })); - - const connections: Record> = {}; - - experiences.forEach((exp) => { - const domains = exp.experience.domain; - for (let i = 0; i < domains.length; i++) { - for (let j = i + 1; j < domains.length; j++) { - const [a, b] = [domains[i], domains[j]].sort(); - if (!connections[a]) connections[a] = new Set(); - connections[a].add(b); - } - } - }); -}); - -Deno.bench('Mapper - Learner journey (1000 experiences, 10 learners)', () => { - const experiences = Array.from({ length: 1000 }, (_, i) => ({ - id: `exp-${i}`, - timestamp: new Date(Date.now() - i * 86400000).toISOString(), - learner: { id: `learner-${i % 10}` }, - experience: { type: 'workshop' }, - })); - - const journeys: Record = {}; - - experiences.forEach((exp) => { - const learner = exp.learner.id; - if (!journeys[learner]) journeys[learner] = []; - journeys[learner].push(exp); - }); - - // Sort each journey - Object.values(journeys).forEach((journey) => { - journey.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); - }); -}); - -/** - * Performance SLOs - * - * - Hotspot detection (100 exp): < 5ms - * - Network generation (100 exp): < 10ms (WASM: < 1ms) - * - Journey sorting (1000 exp): < 20ms - */ diff --git a/benchmarks/validation.bench.ts b/benchmarks/validation.bench.ts deleted file mode 100644 index 6043b94..0000000 --- a/benchmarks/validation.bench.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Validation performance benchmarks - * SLO: < 1ms per experience validation (WASM target) - */ - -Deno.bench('Validation - Single experience (baseline)', () => { - const experience = { - id: crypto.randomUUID(), - timestamp: new Date().toISOString(), - learner: { id: 'learner-123', name: 'Test' }, - context: { - location: { name: 'Makerspace', type: 'makerspace' }, - }, - experience: { - type: 'workshop', - domain: ['electronics'], - description: 'Built circuit', - }, - }; - - // Basic validation (type checking) - if (!experience.id || !experience.learner || !experience.context) { - throw new Error('Invalid'); - } -}); - -Deno.bench('Validation - Batch 100 experiences', () => { - const experiences = Array.from({ length: 100 }, (_, i) => ({ - id: `exp-${i}`, - timestamp: new Date().toISOString(), - learner: { id: `learner-${i}` }, - context: { location: { name: 'Test Location' } }, - experience: { type: 'workshop', domain: ['test'], description: 'Test' }, - })); - - experiences.forEach((exp) => { - if (!exp.id || !exp.learner || !exp.context) { - throw new Error('Invalid'); - } - }); -}); - -Deno.bench('Validation - Complex nested structure', () => { - const complex = { - id: crypto.randomUUID(), - timestamp: new Date().toISOString(), - learner: { - id: 'learner-123', - name: 'Alice Johnson', - demographics: { - age: 25, - location: 'SF', - }, - }, - context: { - location: { - name: 'Mission Makerspace', - type: 'makerspace', - coordinates: { lat: 37.77, lon: -122.42 }, - capacity: 50, - }, - timestamp: new Date().toISOString(), - collaborators: ['bob', 'charlie'], - }, - experience: { - type: 'project', - domain: ['electronics', 'art', 'sculpture'], - description: 'Multi-domain project', - duration: 120, - tags: ['beginner', 'collaborative'], - }, - }; - - if (!complex.id || !complex.learner || !complex.context) { - throw new Error('Invalid'); - } -}); - -/** - * Performance SLOs (Service Level Objectives) - * - * Target: WASM validation - * - Single validation: < 0.002ms (2μs) - * - Batch 100: < 0.5ms total - * - Complex nested: < 0.005ms - * - * Current: TypeScript baseline - * - Measure and compare against SLOs - */ diff --git a/deno.json b/deno.json deleted file mode 100644 index 9270cd9..0000000 --- a/deno.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "name": "@ubicity/core", - "version": "0.3.0", - "exports": "./src/index.js", - "nodeModulesDir": "auto", - "tasks": { - "dev": "deno run --watch src/cli.js", - "start": "deno run --allow-read --allow-write src/cli.js", - "capture": "deno run --allow-read --allow-write src/capture.js", - "capture:quick": "deno run --allow-read --allow-write src/capture.js quick", - "capture:full": "deno run --allow-read --allow-write src/capture.js full", - "visualize": "deno run --allow-read --allow-write src/visualize.js", - "report": "deno run --allow-read --allow-write src/cli.js report", - "hotspots": "deno run --allow-read --allow-write src/cli.js hotspots", - "stats": "deno run --allow-read --allow-write src/cli.js stats", - "test": "deno test --allow-read --allow-write tests/", - "test:watch": "deno test --watch --allow-read --allow-write tests/", - "bench": "deno bench --allow-read --allow-write benchmarks/", - "check": "deno check src/**/*.js", - "fmt": "deno fmt", - "fmt:check": "deno fmt --check", - "lint": "deno lint", - "cache": "deno cache --reload src/index.js", - "compile": "deno task compile:cli && deno task compile:capture && deno task compile:wasm", - "compile:cli": "deno compile --allow-read --allow-write --output ./bin/ubicity src/cli.js", - "compile:capture": "deno compile --allow-read --allow-write --output ./bin/ubicity-capture src/capture.js", - "compile:wasm": "deno task rescript:build && deno task wasm:build", - "rescript:build": "rescript build", - "rescript:clean": "rescript clean", - "rescript:watch": "rescript build -w", - "wasm:build": "cd wasm && cargo build --release --target wasm32-unknown-unknown", - "wasm:optimize": "wasm-opt -Oz -o wasm/pkg/ubicity_bg.wasm wasm/target/wasm32-unknown-unknown/release/ubicity_wasm.wasm", - "//": "TESTING: Comprehensive test suite including integration checks", - "res:build": "rescript", - "res:dev": "rescript build -w", - "capture:res": "deno run -A src-rescript/CaptureCLI.res.js", - "capture:res:quick": "deno run -A src-rescript/CaptureCLI.res.js quick", - "capture:res:full": "deno run -A src-rescript/CaptureCLI.res.js full", - "migrate": "deno run -A scripts/migrate-data.js", - "migrate:check": "deno run -A scripts/migrate-data.js --check", - "migrate:fix": "deno run -A scripts/migrate-data.js --fix", - "test:integration": "deno run -A test-integration.mjs && deno run -A test-existing-data.mjs", - "test:all": "deno task run test && deno task run test:integration", - "format": "prettier --write \"**/*.{js,json,md}\"" - }, - "imports": { - "@std/assert": "jsr:@std/assert@^1.0.0", - "@std/path": "jsr:@std/path@^1.0.0", - "@std/fs": "jsr:@std/fs@^1.0.0", - "@std/crypto": "jsr:@std/crypto@^1.0.0", - "@std/testing": "jsr:@std/testing@^1.0.0", - "zod": "npm:zod@^3.22.4", - "eslint": "npm:eslint@^9.26.0", - "prettier": "npm:prettier@^3.2.4", - "@rescript/core": "npm:@rescript/core@^1.0.0", - "rescript": "^12.0.0" - }, - "compilerOptions": { - "lib": [ - "deno.window", - "deno.unstable" - ], - "strict": true - }, - "exclude": [ - "node_modules/", - "lib/", - "wasm/target/", - "coverage/", - "examples/", - "hooks/", - "scripts/", - "docs/", - "ubicity-data/", - ".github/", - ".well-known/", - ".machine_readable/" - ], - "lint": { - "include": [ - "src/**/*.js", - "tests/**/*.{ts,js}", - "benchmarks/**/*.ts" - ], - "rules": { - "tags": [ - "recommended" - ], - "exclude": [ - "no-explicit-any" - ] - } - }, - "fmt": { - "include": [ - "src/**/*.js", - "tests/**/*.{ts,js}", - "benchmarks/**/*.ts" - ], - "useTabs": false, - "lineWidth": 80, - "indentWidth": 2, - "semiColons": true, - "singleQuote": true, - "proseWrap": "preserve" - } -} \ No newline at end of file diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 98dbf5e..0000000 --- a/deno.lock +++ /dev/null @@ -1,693 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@std/assert@1": "1.0.16", - "jsr:@std/crypto@1": "1.0.5", - "jsr:@std/fs@1": "1.0.21", - "jsr:@std/internal@^1.0.12": "1.0.12", - "jsr:@std/path@1": "1.1.4", - "jsr:@std/path@^1.1.4": "1.1.4", - "npm:@rescript/core@1": "1.6.1_rescript@12.2.0", - "npm:@types/node@*": "18.16.19", - "npm:eslint@^10.2.0": "10.4.0", - "npm:eslint@^9.26.0": "9.39.4", - "npm:prettier@^3.2.4": "3.8.3", - "npm:rescript@12": "12.2.0", - "npm:zod@^3.22.4": "3.25.76", - "npm:zod@^4.3.6": "4.4.1" - }, - "jsr": { - "@std/assert@1.0.16": { - "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/crypto@1.0.5": { - "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" - }, - "@std/fs@1.0.21": { - "integrity": "d720fe1056d78d43065a4d6e0eeb2b19f34adb8a0bc7caf3a4dbf1d4178252cd", - "dependencies": [ - "jsr:@std/internal", - "jsr:@std/path@^1.1.4" - ] - }, - "@std/internal@1.0.12": { - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" - }, - "@std/path@1.1.4": { - "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", - "dependencies": [ - "jsr:@std/internal" - ] - } - }, - "npm": { - "@eslint-community/eslint-utils@4.9.1_eslint@10.4.0": { - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dependencies": [ - "eslint@10.4.0", - "eslint-visitor-keys@3.4.3" - ] - }, - "@eslint-community/eslint-utils@4.9.1_eslint@9.39.4": { - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dependencies": [ - "eslint@9.39.4", - "eslint-visitor-keys@3.4.3" - ] - }, - "@eslint-community/regexpp@4.12.2": { - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==" - }, - "@eslint/config-array@0.21.2": { - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dependencies": [ - "@eslint/object-schema@2.1.7", - "debug", - "minimatch@3.1.5" - ] - }, - "@eslint/config-array@0.23.5": { - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dependencies": [ - "@eslint/object-schema@3.0.5", - "debug", - "minimatch@10.2.5" - ] - }, - "@eslint/config-helpers@0.4.2": { - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dependencies": [ - "@eslint/core@0.17.0" - ] - }, - "@eslint/config-helpers@0.6.0": { - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", - "dependencies": [ - "@eslint/core@1.2.1" - ] - }, - "@eslint/core@0.17.0": { - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dependencies": [ - "@types/json-schema" - ] - }, - "@eslint/core@1.2.1": { - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dependencies": [ - "@types/json-schema" - ] - }, - "@eslint/eslintrc@3.3.5": { - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dependencies": [ - "ajv", - "debug", - "espree@10.4.0", - "globals", - "ignore", - "import-fresh", - "js-yaml", - "minimatch@3.1.5", - "strip-json-comments" - ] - }, - "@eslint/js@9.39.4": { - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==" - }, - "@eslint/object-schema@2.1.7": { - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==" - }, - "@eslint/object-schema@3.0.5": { - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==" - }, - "@eslint/plugin-kit@0.4.1": { - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dependencies": [ - "@eslint/core@0.17.0", - "levn" - ] - }, - "@eslint/plugin-kit@0.7.1": { - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", - "dependencies": [ - "@eslint/core@1.2.1", - "levn" - ] - }, - "@humanfs/core@0.19.2": { - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dependencies": [ - "@humanfs/types" - ] - }, - "@humanfs/node@0.16.8": { - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dependencies": [ - "@humanfs/core", - "@humanfs/types", - "@humanwhocodes/retry" - ] - }, - "@humanfs/types@0.15.0": { - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==" - }, - "@humanwhocodes/module-importer@1.0.1": { - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" - }, - "@humanwhocodes/retry@0.4.3": { - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" - }, - "@rescript/core@1.6.1_rescript@12.2.0": { - "integrity": "sha512-vyb5k90ck+65Fgui+5vCja/mUfzKaK3kOPT4Z6aAJdHLH1eljEi1zKhXroCiCtpNLSWp8k4ulh1bdB5WS0hvqA==", - "dependencies": [ - "rescript" - ] - }, - "@rescript/darwin-arm64@12.2.0": { - "integrity": "sha512-xc3K/J7Ujl1vPiFY2009mRf3kWRlUe/VZyJWprseKxlcEtUQv89ter7r6pY+YFbtYvA/fcaEncL9CVGEdattAg==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@rescript/darwin-x64@12.2.0": { - "integrity": "sha512-qqcTvnlSeoKkywLjG7cXfYvKZ1e4Gz2kUKcD6SiqDgCqm8TF+spwlFAiM6sloRUOFsc0bpC/0R0B3yr01FCB1A==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@rescript/linux-arm64@12.2.0": { - "integrity": "sha512-ODmpG3ji+Nj/8d5yvXkeHlfKkmbw1Q4t1iIjVuNwtmFpz7TiEa7n/sQqoYdE+WzbDX3DoJfmJNbp3Ob7qCUoOg==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@rescript/linux-x64@12.2.0": { - "integrity": "sha512-2W9Y9/g19Y4F/subl8yV3T8QBG2oRaP+HciNRcBjptyEdw9LmCKH8+rhWO6sp3E+nZLwoE2IAkwH0WKV3wqlxQ==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@rescript/runtime@12.2.0": { - "integrity": "sha512-NwfljDRq1rjFPHUaca1nzFz13xsa9ZGkBkLvMhvVgavJT5+A4rMcLu8XAaVTi/oAhO/tlHf9ZDoOTF1AfyAk9Q==" - }, - "@rescript/win32-x64@12.2.0": { - "integrity": "sha512-fhf8CBj3p1lkIXPeNko3mVTKQfXXm4BoxJtR1xAXxUn43wDpd8Lox4w8/EPBbbW6C/YFQW6H7rtpY+2AKuNaDA==", - "os": ["win32"], - "cpu": ["x64"] - }, - "@types/esrecurse@4.3.1": { - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==" - }, - "@types/estree@1.0.8": { - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" - }, - "@types/json-schema@7.0.15": { - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "@types/node@18.16.19": { - "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==" - }, - "acorn-jsx@5.3.2_acorn@8.16.0": { - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dependencies": [ - "acorn" - ] - }, - "acorn@8.16.0": { - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "bin": true - }, - "ajv@6.15.0": { - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dependencies": [ - "fast-deep-equal", - "fast-json-stable-stringify", - "json-schema-traverse", - "uri-js" - ] - }, - "ansi-styles@4.3.0": { - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": [ - "color-convert" - ] - }, - "argparse@2.0.1": { - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "balanced-match@1.0.2": { - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "balanced-match@4.0.4": { - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" - }, - "brace-expansion@1.1.14": { - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dependencies": [ - "balanced-match@1.0.2", - "concat-map" - ] - }, - "brace-expansion@5.0.5": { - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dependencies": [ - "balanced-match@4.0.4" - ] - }, - "callsites@3.1.0": { - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "chalk@4.1.2": { - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": [ - "ansi-styles", - "supports-color" - ] - }, - "color-convert@2.0.1": { - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": [ - "color-name" - ] - }, - "color-name@1.1.4": { - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "concat-map@0.0.1": { - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "cross-spawn@7.0.6": { - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": [ - "path-key", - "shebang-command", - "which" - ] - }, - "debug@4.4.3": { - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": [ - "ms" - ] - }, - "deep-is@0.1.4": { - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "escape-string-regexp@4.0.0": { - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, - "eslint-scope@8.4.0": { - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dependencies": [ - "esrecurse", - "estraverse" - ] - }, - "eslint-scope@9.1.2": { - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dependencies": [ - "@types/esrecurse", - "@types/estree", - "esrecurse", - "estraverse" - ] - }, - "eslint-visitor-keys@3.4.3": { - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" - }, - "eslint-visitor-keys@4.2.1": { - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" - }, - "eslint-visitor-keys@5.0.1": { - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==" - }, - "eslint@10.4.0": { - "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", - "dependencies": [ - "@eslint-community/eslint-utils@4.9.1_eslint@10.4.0", - "@eslint-community/regexpp", - "@eslint/config-array@0.23.5", - "@eslint/config-helpers@0.6.0", - "@eslint/core@1.2.1", - "@eslint/plugin-kit@0.7.1", - "@humanfs/node", - "@humanwhocodes/module-importer", - "@humanwhocodes/retry", - "@types/estree", - "ajv", - "cross-spawn", - "debug", - "escape-string-regexp", - "eslint-scope@9.1.2", - "eslint-visitor-keys@5.0.1", - "espree@11.2.0", - "esquery", - "esutils", - "fast-deep-equal", - "file-entry-cache", - "find-up", - "glob-parent", - "ignore", - "imurmurhash", - "is-glob", - "json-stable-stringify-without-jsonify", - "minimatch@10.2.5", - "natural-compare", - "optionator" - ], - "bin": true - }, - "eslint@9.39.4": { - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dependencies": [ - "@eslint-community/eslint-utils@4.9.1_eslint@9.39.4", - "@eslint-community/regexpp", - "@eslint/config-array@0.21.2", - "@eslint/config-helpers@0.4.2", - "@eslint/core@0.17.0", - "@eslint/eslintrc", - "@eslint/js", - "@eslint/plugin-kit@0.4.1", - "@humanfs/node", - "@humanwhocodes/module-importer", - "@humanwhocodes/retry", - "@types/estree", - "ajv", - "chalk", - "cross-spawn", - "debug", - "escape-string-regexp", - "eslint-scope@8.4.0", - "eslint-visitor-keys@4.2.1", - "espree@10.4.0", - "esquery", - "esutils", - "fast-deep-equal", - "file-entry-cache", - "find-up", - "glob-parent", - "ignore", - "imurmurhash", - "is-glob", - "json-stable-stringify-without-jsonify", - "lodash.merge", - "minimatch@3.1.5", - "natural-compare", - "optionator" - ], - "bin": true - }, - "espree@10.4.0": { - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dependencies": [ - "acorn", - "acorn-jsx", - "eslint-visitor-keys@4.2.1" - ] - }, - "espree@11.2.0": { - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dependencies": [ - "acorn", - "acorn-jsx", - "eslint-visitor-keys@5.0.1" - ] - }, - "esquery@1.7.0": { - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dependencies": [ - "estraverse" - ] - }, - "esrecurse@4.3.0": { - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": [ - "estraverse" - ] - }, - "estraverse@5.3.0": { - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - }, - "esutils@2.0.3": { - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "fast-deep-equal@3.1.3": { - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify@2.1.0": { - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein@2.0.6": { - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "file-entry-cache@8.0.0": { - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dependencies": [ - "flat-cache" - ] - }, - "find-up@5.0.0": { - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": [ - "locate-path", - "path-exists" - ] - }, - "flat-cache@4.0.1": { - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dependencies": [ - "flatted", - "keyv" - ] - }, - "flatted@3.4.2": { - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==" - }, - "glob-parent@6.0.2": { - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": [ - "is-glob" - ] - }, - "globals@14.0.0": { - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" - }, - "has-flag@4.0.0": { - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "ignore@5.3.2": { - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" - }, - "import-fresh@3.3.1": { - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dependencies": [ - "parent-module", - "resolve-from" - ] - }, - "imurmurhash@0.1.4": { - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - }, - "is-extglob@2.1.1": { - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - }, - "is-glob@4.0.3": { - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": [ - "is-extglob" - ] - }, - "isexe@2.0.0": { - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "js-yaml@4.1.1": { - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dependencies": [ - "argparse" - ], - "bin": true - }, - "json-buffer@3.0.1": { - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "json-schema-traverse@0.4.1": { - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify-without-jsonify@1.0.1": { - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" - }, - "keyv@4.5.4": { - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": [ - "json-buffer" - ] - }, - "levn@0.4.1": { - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dependencies": [ - "prelude-ls", - "type-check" - ] - }, - "locate-path@6.0.0": { - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": [ - "p-locate" - ] - }, - "lodash.merge@4.6.2": { - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "minimatch@10.2.5": { - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dependencies": [ - "brace-expansion@5.0.5" - ] - }, - "minimatch@3.1.5": { - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dependencies": [ - "brace-expansion@1.1.14" - ] - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "natural-compare@1.4.0": { - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "optionator@0.9.4": { - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dependencies": [ - "deep-is", - "fast-levenshtein", - "levn", - "prelude-ls", - "type-check", - "word-wrap" - ] - }, - "p-limit@3.1.0": { - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": [ - "yocto-queue" - ] - }, - "p-locate@5.0.0": { - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": [ - "p-limit" - ] - }, - "parent-module@1.0.1": { - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": [ - "callsites" - ] - }, - "path-exists@4.0.0": { - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-key@3.1.1": { - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "prelude-ls@1.2.1": { - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - }, - "prettier@3.8.3": { - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", - "bin": true - }, - "punycode@2.3.1": { - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - }, - "rescript@12.2.0": { - "integrity": "sha512-1Jf2cmNhyx5Mj2vwZ4XXPcXvNSjGj9D1jPBUcoqIOqRpLPo1ch2Ta/7eWh23xAHWHK5ow7BCDyYFjvZSjyjLzg==", - "dependencies": [ - "@rescript/runtime" - ], - "optionalDependencies": [ - "@rescript/darwin-arm64", - "@rescript/darwin-x64", - "@rescript/linux-arm64", - "@rescript/linux-x64", - "@rescript/win32-x64" - ], - "bin": true - }, - "resolve-from@4.0.0": { - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "shebang-command@2.0.0": { - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": [ - "shebang-regex" - ] - }, - "shebang-regex@3.0.0": { - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "strip-json-comments@3.1.1": { - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "supports-color@7.2.0": { - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": [ - "has-flag" - ] - }, - "type-check@0.4.0": { - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dependencies": [ - "prelude-ls" - ] - }, - "uri-js@4.4.1": { - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": [ - "punycode" - ] - }, - "which@2.0.2": { - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": [ - "isexe" - ], - "bin": true - }, - "word-wrap@1.2.5": { - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" - }, - "yocto-queue@0.1.0": { - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" - }, - "zod@3.25.76": { - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" - }, - "zod@4.4.1": { - "integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==" - } - }, - "workspace": { - "dependencies": [ - "jsr:@std/assert@1", - "jsr:@std/crypto@1", - "jsr:@std/fs@1", - "jsr:@std/path@1", - "jsr:@std/testing@1", - "npm:@rescript/core@1", - "npm:eslint@^9.26.0", - "npm:prettier@^3.2.4", - "npm:zod@^3.22.4" - ], - "packageJson": { - "dependencies": [ - "npm:@rescript/core@1", - "npm:eslint@^10.2.0", - "npm:prettier@^3.2.4", - "npm:rescript@12", - "npm:zod@^4.3.6" - ] - } - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 959eb0c..81a1283 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: - ./ubicity-data:/app/ubicity-data environment: - UBICITY_LOG_LEVEL=info - # Deno entrypoint (see Containerfile). Override per invocation. + # entrypoint (see Containerfile). Override per invocation. command: ["run", "--allow-read", "--allow-write", "src/cli.js", "help"] # Optional: web server for visualization output diff --git a/docs/API.md b/docs/API.md index 4a740df..f050bff 100644 --- a/docs/API.md +++ b/docs/API.md @@ -578,9 +578,9 @@ try { See `examples/api-usage.js` for complete working examples. -## TypeScript Support +## Support -TypeScript definitions coming in v0.3. For now, use JSDoc: + definitions coming in v0.3. For now, use JSDoc: ```javascript /** diff --git a/examples/web-project-deno.json b/examples/web-project-deno.json deleted file mode 100644 index 5ddd3bd..0000000 --- a/examples/web-project-deno.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "// NOTE": "Example deno.json for ReScript web projects", - "tasks": { - "build": "deno run -A npm:rescript", - "clean": "deno run -A npm:rescript clean", - "watch": "deno run -A npm:rescript -w", - "serve": "deno run -A jsr:@std/http/file-server .", - "test": "deno test --allow-all" - }, - "imports": { - "rescript": "^12.0.0", - "@rescript/core": "npm:@rescript/core@^1.6.0", - "safe-dom/": "https://raw.githubusercontent.com/hyperpolymath/rescript-dom-mounter/main/src/", - "proven/": "../proven/bindings/rescript/src/" - }, - "compilerOptions": { - "allowJs": true, - "checkJs": false - } -} diff --git a/rescript.json b/rescript.json deleted file mode 100644 index b84e3ee..0000000 --- a/rescript.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "ubicity", - "version": "0.3.0", - "sources": { - "dir": "src-rescript", - "subdirs": true - }, - "package-specs": { - "module": "es6", - "in-source": true - }, - "suffix": ".res.js", - "bs-dependencies": [], - "warnings": { - "error": "+101" - }, - "bsc-flags": ["-open Belt"], - "refmt": 3, - "gentypeconfig": { - "language": "typescript", - "shims": {}, - "debug": { - "all": false, - "basic": false - } - } -} diff --git a/spec/seams/checklists/analysis-export.adoc b/spec/seams/checklists/analysis-export.adoc index 7c4b6b4..bc02068 100644 --- a/spec/seams/checklists/analysis-export.adoc +++ b/spec/seams/checklists/analysis-export.adoc @@ -23,5 +23,5 @@ == Invariant Verification * **format-agnostic-analysis**: Analysis.res should not import Export/Visualization -* **export-consumes-types**: Type system enforces (ReScript compiler) +* **export-consumes-types**: Type system enforces ( compiler) * **no-computation-in-export**: Manual code review for avg/median/mode/clustering in Export.res diff --git a/spec/seams/checklists/cli-core.adoc b/spec/seams/checklists/cli-core.adoc index 1de2e73..e221e17 100644 --- a/spec/seams/checklists/cli-core.adoc +++ b/spec/seams/checklists/cli-core.adoc @@ -22,6 +22,6 @@ == Invariant Verification -* **no-direct-storage**: grep -r "NodeJs.Fs" src-rescript/Capture*.res (should be empty) +* **no-direct-storage**: grep -r "NodeJs.Fs" src-/Capture*.res (should be empty) * **validation-in-core**: All .make() calls should be from UbiCity module * **no-business-logic-in-cli**: No domain calculations in CLI (only formatting/display) diff --git a/spec/seams/checklists/data-analysis.adoc b/spec/seams/checklists/data-analysis.adoc index 86fccd6..966725c 100644 --- a/spec/seams/checklists/data-analysis.adoc +++ b/spec/seams/checklists/data-analysis.adoc @@ -16,12 +16,12 @@ == Post-Implementation -[ ] Type checker enforces typed parameters (ReScript compilation) +[ ] Type checker enforces typed parameters ( compilation) [ ] No NodeJs.Fs imports in Analysis.res [ ] Decoder tests verify validation catches invalid data == Invariant Verification -* **validated-types-only**: Type system enforces (ReScript compiler) -* **no-raw-json**: grep -r "JSON.parse" src-rescript/Analysis.res (should be empty) -* **storage-abstraction**: grep -r "NodeJs.Fs" src-rescript/Analysis.res (should be empty) +* **validated-types-only**: Type system enforces ( compiler) +* **no-raw-json**: grep -r "JSON.parse" src-/Analysis.res (should be empty) +* **storage-abstraction**: grep -r "NodeJs.Fs" src-/Analysis.res (should be empty) diff --git a/spec/seams/conformance/analysis-export-conformance.adoc b/spec/seams/conformance/analysis-export-conformance.adoc index 6d64c0d..245de7e 100644 --- a/spec/seams/conformance/analysis-export-conformance.adoc +++ b/spec/seams/conformance/analysis-export-conformance.adoc @@ -11,7 +11,7 @@ The Analysis-Export seam separates analytical computations from presentation for === ✓ Analysis produces typed results -[source,rescript] +[source,] ---- // CORRECT: Analysis returns structured types // File: Analysis.res @@ -43,7 +43,7 @@ let csv = `morning,${results.morning->Int.toString}\n...` === ✓ Export formats existing data -[source,rescript] +[source,] ---- // CORRECT: Export.res formats, doesn't compute // File: Export.res @@ -64,7 +64,7 @@ let exportToCSV = (experiences: array): string => { === ✓ Visualization generates presentation, not insights -[source,rescript] +[source,] ---- // CORRECT: Visualization.res generates HTML from pre-computed data // File: Visualization.res @@ -91,7 +91,7 @@ let generateHTML = ( === ✗ Export performs analysis -[source,rescript] +[source,] ---- // WRONG: Export.res calculates statistics // File: Export.res @@ -116,7 +116,7 @@ let exportToCSV = (experiences: array): string => { === ✗ Analysis generates output formats -[source,rescript] +[source,] ---- // WRONG: Analysis.res returns CSV string // File: Analysis.res @@ -137,7 +137,7 @@ module TemporalAnalyzer = { === ✗ Export works with analysis-specific structures -[source,rescript] +[source,] ---- // WRONG: Export coupled to Analysis internals // File: Export.res @@ -163,7 +163,7 @@ let exportAnalysisResults = (internal: analysisInternal): string => { === ✗ Visualization computes network layout -[source,rescript] +[source,] ---- // WRONG: Visualization.res performs graph layout // File: Visualization.res @@ -201,7 +201,7 @@ Output (CSV, HTML, GeoJSON, DOT, etc.) === Lightweight formatting in analysis results -[source,rescript] +[source,] ---- // OK: Simple data formatting for clarity // File: Analysis.res @@ -228,7 +228,7 @@ This is acceptable - ISO date format is data representation, not presentation. === Export doing validation -[source,rescript] +[source,] ---- // CAREFUL: Export can validate inputs // File: Export.res @@ -248,7 +248,7 @@ This is a gray area but acceptable - Export can validate its inputs are suitable === Analysis that outputs strings for non-format reasons -[source,rescript] +[source,] ---- // OK: Analysis can return strings if they're domain data // File: Analysis.res @@ -288,7 +288,7 @@ When modifying Export/Visualization modules: // File: test/seam-analysis-export.test.mjs test('Analysis.res does not import Export.res', () => { - const source = fs.readFileSync('src-rescript/Analysis.res', 'utf-8'); + const source = fs.readFileSync('src-/Analysis.res', 'utf-8'); assert(!source.includes('Export')); assert(!source.includes('Visualization')); }); @@ -308,6 +308,6 @@ test('Analysis functions return typed objects', () => { == References -- Analysis modules: `src-rescript/Analysis.res`, `src-rescript/Mapper.res` (hotspots, networks) -- Export modules: `src-rescript/Export.res`, `src-rescript/Visualization.res` +- Analysis modules: `src-/Analysis.res`, `src-/Mapper.res` (hotspots, networks) +- Export modules: `src-/Export.res`, `src-/Visualization.res` - Tests: `test/analysis.test.mjs`, `test/export.test.mjs`, `test/visualization.test.mjs` diff --git a/spec/seams/conformance/cli-core-conformance.adoc b/spec/seams/conformance/cli-core-conformance.adoc index 03b531c..2a526f7 100644 --- a/spec/seams/conformance/cli-core-conformance.adoc +++ b/spec/seams/conformance/cli-core-conformance.adoc @@ -11,7 +11,7 @@ The CLI-Core seam separates interactive user interface from core business logic. === ✓ CLI delegates to Mapper for storage -[source,rescript] +[source,] ---- // CORRECT: Capture.res delegates to Mapper // File: Capture.res @@ -39,7 +39,7 @@ let captureExperience = async ( === ✓ Validation in core modules -[source,rescript] +[source,] ---- // CORRECT: Validation in UbiCity.res, not CLI // File: UbiCity.res @@ -66,7 +66,7 @@ switch location { === ✓ CLI has no business logic -[source,rescript] +[source,] ---- // CORRECT: CLI only handles presentation // File: CaptureCLI.res @@ -85,7 +85,7 @@ let promptForExperienceType = async () => { === ✗ CLI directly manipulates files -[source,rescript] +[source,] ---- // WRONG: Capture.res directly writes to storage // File: Capture.res @@ -107,7 +107,7 @@ let captureExperience = async (~experience) => { === ✗ Validation in CLI -[source,rescript] +[source,] ---- // WRONG: CLI validates input instead of delegating to core // File: Capture.res @@ -130,7 +130,7 @@ let validateLocationName = (name: string) => { === ✗ Business logic in CLI -[source,rescript] +[source,] ---- // WRONG: CLI calculates domain-specific values // File: CaptureCLI.res @@ -149,7 +149,7 @@ let generateExperienceId = () => { === ✗ CLI queries storage directly -[source,rescript] +[source,] ---- // WRONG: CLI reads experiences for count // File: CaptureCLI.res @@ -189,7 +189,7 @@ File System === Interactive prompts with defaults -[source,rescript] +[source,] ---- // OK: CLI can have default values for UX let promptWithDefault = async (~prompt: string, ~default: string) => { @@ -210,7 +210,7 @@ let privacy = Privacy.make(~level=userInput) // Validates in UbiCity.res === Error formatting -[source,rescript] +[source,] ---- // OK: CLI can format error messages for display // File: Capture.res @@ -229,19 +229,19 @@ switch Location.make(~name=input, ()) { == Testing CLI-Core Boundary -[source,rescript] +[source,] ---- // Unit test: Core modules don't import CLI modules // File: test/seam-cli-core.test.mjs test('UbiCity.res does not import Capture.res', async () => { - const source = fs.readFileSync('src-rescript/UbiCity.res', 'utf-8'); + const source = fs.readFileSync('src-/UbiCity.res', 'utf-8'); assert(!source.includes('Capture')); assert(!source.includes('CaptureCLI')); }); test('Mapper.res does not import Capture.res', async () => { - const source = fs.readFileSync('src-rescript/Mapper.res', 'utf-8'); + const source = fs.readFileSync('src-/Mapper.res', 'utf-8'); assert(!source.includes('Capture')); }); ---- @@ -264,6 +264,6 @@ When modifying core modules: == References -- CLI modules: `src-rescript/Capture.res`, `src-rescript/CaptureCLI.res` -- Core modules: `src-rescript/UbiCity.res`, `src-rescript/Mapper.res` +- CLI modules: `src-/Capture.res`, `src-/CaptureCLI.res` +- Core modules: `src-/UbiCity.res`, `src-/Mapper.res` - CLI tests: `test/cli-smoke.test.mjs` diff --git a/spec/seams/conformance/data-analysis-conformance.adoc b/spec/seams/conformance/data-analysis-conformance.adoc index 2d8483a..52f380c 100644 --- a/spec/seams/conformance/data-analysis-conformance.adoc +++ b/spec/seams/conformance/data-analysis-conformance.adoc @@ -11,7 +11,7 @@ The Data-Analysis seam separates data decoding/storage from analysis logic. Deco === ✓ Analysis consumes validated types -[source,rescript] +[source,] ---- // CORRECT: Analysis.res receives validated types // File: Analysis.res @@ -32,7 +32,7 @@ module TemporalAnalyzer = { === ✓ Decoder validates before analysis -[source,rescript] +[source,] ---- // CORRECT: Decoder ensures valid types // File: Decoder.res @@ -58,7 +58,7 @@ let loadAll = async (mapper) => { === ✓ Analysis uses Mapper queries, not file system -[source,rescript] +[source,] ---- // CORRECT: Analysis calls Mapper, not file system // File: Analysis.res @@ -83,7 +83,7 @@ module RecommendationEngine = { === ✗ Analysis works with raw JSON -[source,rescript] +[source,] ---- // WRONG: Analysis.res directly parses JSON // File: Analysis.res @@ -106,7 +106,7 @@ let analyzeByTimeOfDay = (jsonData: string) => { === ✗ Analysis reads files directly -[source,rescript] +[source,] ---- // WRONG: Analysis.res accesses file system // File: Analysis.res @@ -128,7 +128,7 @@ module CollaborativeNetworkAnalyzer = { === ✗ Unvalidated data passed to analysis -[source,rescript] +[source,] ---- // WRONG: Skipping validation // File: Mapper.res @@ -149,7 +149,7 @@ let loadAll = async (mapper) => { === ✗ Analysis performs I/O operations -[source,rescript] +[source,] ---- // WRONG: Analysis writes intermediate results // File: Analysis.res @@ -192,7 +192,7 @@ Results (typed) === Mapper as both data layer and query layer -[source,rescript] +[source,] ---- // OK: Mapper.res can have both responsibilities // File: Mapper.res @@ -220,7 +220,7 @@ let identifyHotspots = (mapper) => { === Analysis that needs partial data -[source,rescript] +[source,] ---- // OK: Analysis can accept filtered subsets // File: Analysis.res @@ -244,9 +244,9 @@ let stats = Analysis.analyzeByDomain(roboticsExperiences, "robotics") == Type Safety Verification -[source,rescript] +[source,] ---- -// ReScript type system enforces this seam ✓ +// type system enforces this seam ✓ // If Analysis.res tries to accept JSON: let analyzeByTimeOfDay = (jsonData: JSON.t) => { // ❌ Type error! @@ -278,7 +278,7 @@ When modifying Analysis layer: == References -- Data layer: `src-rescript/Decoder.res`, `src-rescript/Mapper.res` (storage) -- Analysis layer: `src-rescript/Analysis.res`, `src-rescript/Mapper.res` (queries) -- Domain types: `src-rescript/UbiCity.res` +- Data layer: `src-/Decoder.res`, `src-/Mapper.res` (storage) +- Analysis layer: `src-/Analysis.res`, `src-/Mapper.res` (queries) +- Domain types: `src-/UbiCity.res` - Tests: `test/decoder.test.mjs`, `test/analysis.test.mjs` diff --git a/spec/seams/conformance/privacy-boundary-conformance.adoc b/spec/seams/conformance/privacy-boundary-conformance.adoc index 26ed522..3645e70 100644 --- a/spec/seams/conformance/privacy-boundary-conformance.adoc +++ b/spec/seams/conformance/privacy-boundary-conformance.adoc @@ -197,6 +197,6 @@ Before exporting data: == References -- Privacy.res module: `src-rescript/Privacy.res` +- Privacy.res module: `src-/Privacy.res` - Privacy tests: `test/privacy.test.mjs` - GDPR considerations: See `docs/PRIVACY.md` (TODO) diff --git a/spec/seams/seam-register.json b/spec/seams/seam-register.json index 490a2b8..8aaa16a 100644 --- a/spec/seams/seam-register.json +++ b/spec/seams/seam-register.json @@ -7,13 +7,13 @@ "name": "CLI to Core Module Boundary", "description": "Separates interactive user interface from core business logic. CLI modules handle user interaction and delegate all data manipulation to core modules.", "side_a": [ - "src-rescript/Capture.res", - "src-rescript/CaptureCLI.res" + "src-/Capture.res", + "src-/CaptureCLI.res" ], "side_b": [ - "src-rescript/UbiCity.res", - "src-rescript/Mapper.res", - "src-rescript/Decoder.res" + "src-/UbiCity.res", + "src-/Mapper.res", + "src-/Decoder.res" ], "seam_type": "module", "invariants": [ @@ -43,7 +43,7 @@ "conformance_paths": [ "spec/seams/conformance/cli-core-conformance.adoc" ], - "boundary_path": "src-rescript/Capture.res", + "boundary_path": "src-/Capture.res", "declared_dependencies": [ "UbiCity.res", "Mapper.res" @@ -54,12 +54,12 @@ "name": "Privacy Protection Boundary", "description": "Security seam that ensures PII protection and anonymization. All data crossing export boundaries must pass through privacy checks.", "side_a": [ - "src-rescript/Mapper.res", - "src-rescript/Analysis.res", - "src-rescript/UbiCity.res" + "src-/Mapper.res", + "src-/Analysis.res", + "src-/UbiCity.res" ], "side_b": [ - "src-rescript/Privacy.res" + "src-/Privacy.res" ], "seam_type": "layer", "invariants": [ @@ -107,7 +107,7 @@ "conformance_paths": [ "spec/seams/conformance/privacy-boundary-conformance.adoc" ], - "boundary_path": "src-rescript/Privacy.res", + "boundary_path": "src-/Privacy.res", "declared_dependencies": [] }, { @@ -115,12 +115,12 @@ "name": "Data Layer to Analysis Logic Boundary", "description": "Separates data decoding/storage from analysis logic. Ensures analysis modules work with validated domain types.", "side_a": [ - "src-rescript/Decoder.res", - "src-rescript/Mapper.res (storage functions)" + "src-/Decoder.res", + "src-/Mapper.res (storage functions)" ], "side_b": [ - "src-rescript/Mapper.res (query functions)", - "src-rescript/Analysis.res" + "src-/Mapper.res (query functions)", + "src-/Analysis.res" ], "seam_type": "module", "invariants": [ @@ -150,7 +150,7 @@ "conformance_paths": [ "spec/seams/conformance/data-analysis-conformance.adoc" ], - "boundary_path": "src-rescript/Decoder.res", + "boundary_path": "src-/Decoder.res", "declared_dependencies": [ "UbiCity.res" ] @@ -160,12 +160,12 @@ "name": "Analysis to Export/Visualization Boundary", "description": "Separates analytical computations from presentation formats. Analysis produces structured data, Export/Visualization format it.", "side_a": [ - "src-rescript/Analysis.res", - "src-rescript/Mapper.res" + "src-/Analysis.res", + "src-/Mapper.res" ], "side_b": [ - "src-rescript/Export.res", - "src-rescript/Visualization.res" + "src-/Export.res", + "src-/Visualization.res" ], "seam_type": "module", "invariants": [ @@ -195,7 +195,7 @@ "conformance_paths": [ "spec/seams/conformance/analysis-export-conformance.adoc" ], - "boundary_path": "src-rescript/Export.res", + "boundary_path": "src-/Export.res", "declared_dependencies": [ "UbiCity.res", "Analysis.res" diff --git a/src-rescript/Analysis.res b/src-rescript/Analysis.res deleted file mode 100644 index 7527d5c..0000000 --- a/src-rescript/Analysis.res +++ /dev/null @@ -1,454 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity Advanced Analysis Features -// Temporal patterns, collaborative networks, recommendations - -open UbiCity - -// Time of day bucket -type timeOfDay = Morning | Afternoon | Evening | Night - -type timeDistribution = { - morning: (int, array), - afternoon: (int, array), - evening: (int, array), - night: (int, array), -} - -type weeklyDistribution = Dict.t<(int, array)> - -type learningStreak = { - start: string, - end_: string, - days: int, - experiences: int, -} - -type collaborationEdge = { - source: string, - target: string, - weight: int, -} - -type collaborationNode = { - id: string, - experiences: int, -} - -type collaborationNetwork = { - nodes: array, - edges: array, -} - -type recommendation = { - learnerId: string, - similarity: float, - sharedDomains: array, -} - -type locationRecommendation = { - location: string, - relevance: float, - matchingDomains: array, -} - -type domainRecommendation = { - domain: string, - relevance: int, -} - -// Helper: Get time of day from timestamp -let getTimeOfDay = (timestamp: string): timeOfDay => { - let date = Date.fromString(timestamp) - let hour = date->Date.getHours->Int.toFloat - - if hour >= 6. && hour < 12. { - Morning - } else if hour >= 12. && hour < 18. { - Afternoon - } else if hour >= 18. && hour < 22. { - Evening - } else { - Night - } -} - -// Helper: Extract unique domains from experiences -let extractDomains = (experiences: array): array => { - experiences - ->Array.reduce([], (acc, exp) => { - switch exp.experience.domains { - | Some(domains) => Array.concat(acc, domains) - | None => acc - } - }) - ->Array.reduce([], (acc, domain) => { - if acc->Array.includes(domain) { - acc - } else { - acc->Array.concat([domain]) - } - }) -} - -// Temporal Analysis -module TemporalAnalyzer = { - let analyzeByTimeOfDay = (experiences: array): timeDistribution => { - let morningExps = [] - let afternoonExps = [] - let eveningExps = [] - let nightExps = [] - - experiences->Array.forEach(exp => { - let tod = getTimeOfDay(exp.timestamp) - switch tod { - | Morning => morningExps->Array.push(exp)->ignore - | Afternoon => afternoonExps->Array.push(exp)->ignore - | Evening => eveningExps->Array.push(exp)->ignore - | Night => nightExps->Array.push(exp)->ignore - } - }) - - { - morning: (morningExps->Array.length, extractDomains(morningExps)), - afternoon: (afternoonExps->Array.length, extractDomains(afternoonExps)), - evening: (eveningExps->Array.length, extractDomains(eveningExps)), - night: (nightExps->Array.length, extractDomains(nightExps)), - } - } - - let analyzeByDayOfWeek = (experiences: array): weeklyDistribution => { - let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] - let distribution = Dict.make() - - // Initialize all days - days->Array.forEach(day => { - distribution->Dict.set(day, (0, [])) - }) - - // Bucket experiences by day - experiences->Array.forEach(exp => { - let date = Date.fromString(exp.timestamp) - let dayIndex = date->Date.getDay - let dayName = days->Array.get(dayIndex)->Option.getOr("Sunday") - - let (count, domains) = distribution->Dict.get(dayName)->Option.getOr((0, [])) - let newDomains = switch exp.experience.domains { - | Some(doms) => Array.concat(domains, doms) - | None => domains - } - distribution->Dict.set(dayName, (count + 1, newDomains)) - }) - - // Deduplicate domains - distribution->Dict.keysToArray->Array.forEach(day => { - let (count, domains) = distribution->Dict.get(day)->Option.getOr((0, [])) - let uniqueDomains = domains->Array.reduce([], (acc, d) => { - if acc->Array.includes(d) {acc} else {acc->Array.concat([d])} - }) - distribution->Dict.set(day, (count, uniqueDomains)) - }) - - distribution - } - - let detectStreaks = ( - experiences: array, - ~minDays=3, - (), - ): array => { - // Sort by timestamp - let sorted = experiences->Array.toSorted((a, b) => { - let aDate = Date.fromString(a.timestamp) - let bDate = Date.fromString(b.timestamp) - aDate < bDate ? -1.0 : 1.0 - }) - - let streaks = [] - let currentStreak = [] - - sorted->Array.forEachWithIndex((exp, i) => { - if i == 0 { - currentStreak->Array.push(exp)->ignore - } else { - let prevExp = sorted[i - 1]->Option.getExn - let prevDate = Date.fromString(prevExp.timestamp) - let currDate = Date.fromString(exp.timestamp) - - let daysDiff = - Math.floor((currDate->Date.getTime -. prevDate->Date.getTime) /. 86400000.0)->Int.fromFloat - - if daysDiff <= 1 { - currentStreak->Array.push(exp)->ignore - } else { - if currentStreak->Array.length >= minDays { - let firstExp = currentStreak[0]->Option.getExn - let lastExp = currentStreak[currentStreak->Array.length - 1]->Option.getExn - streaks - ->Array.push({ - start: firstExp.timestamp, - end_: lastExp.timestamp, - days: currentStreak->Array.length, - experiences: currentStreak->Array.length, - }) - ->ignore - } - currentStreak->Array.splice(~start=0, ~remove=currentStreak->Array.length, ~insert=[])->ignore - currentStreak->Array.push(exp)->ignore - } - } - }) - - // Check final streak - if currentStreak->Array.length >= minDays { - let firstExp = currentStreak[0]->Option.getExn - let lastExp = currentStreak[currentStreak->Array.length - 1]->Option.getExn - streaks - ->Array.push({ - start: firstExp.timestamp, - end_: lastExp.timestamp, - days: currentStreak->Array.length, - experiences: currentStreak->Array.length, - }) - ->ignore - } - - streaks - } -} - -// Collaborative Network Analysis -module CollaborativeNetworkAnalyzer = { - let buildCollaborationNetwork = ( - experiences: array, - ): collaborationNetwork => { - let edges = Dict.make() - let allLearners = [] - - experiences->Array.forEach(exp => { - let learnerId = exp.learner.id - allLearners->Array.push(learnerId)->ignore - - switch exp.context.connections { - | Some(collaborators) => - collaborators->Array.forEach(collaborator => { - let key = [learnerId, collaborator]->Array.toSorted((a, b) => a < b ? -1.0 : 1.0)->Array.join("-") - let count = edges->Dict.get(key)->Option.getOr(0) - edges->Dict.set(key, count + 1) - - allLearners->Array.push(collaborator)->ignore - }) - | None => () - } - }) - - // Deduplicate learners - let uniqueLearners = allLearners->Array.reduce([], (acc, id) => { - if acc->Array.includes(id) {acc} else {acc->Array.concat([id])} - }) - - // Count experiences per learner - let learnerCounts = Dict.make() - experiences->Array.forEach(exp => { - let count = learnerCounts->Dict.get(exp.learner.id)->Option.getOr(0) - learnerCounts->Dict.set(exp.learner.id, count + 1) - }) - - { - nodes: uniqueLearners->Array.map(id => { - {id, experiences: learnerCounts->Dict.get(id)->Option.getOr(0)} - }), - edges: edges - ->Dict.toArray - ->Array.map(((key, weight)) => { - let parts = key->String.split("-") - {source: parts[0]->Option.getOr(""), target: parts[1]->Option.getOr(""), weight} - }), - } - } - - let findMostCollaborative = ( - experiences: array, - ~topN=10, - (), - ): array<(string, int)> => { - let counts = Dict.make() - - experiences->Array.forEach(exp => { - switch exp.context.connections { - | Some(collaborators) if collaborators->Array.length > 0 => { - let count = counts->Dict.get(exp.learner.id)->Option.getOr(0) - counts->Dict.set(exp.learner.id, count + collaborators->Array.length) - } - | _ => () - } - }) - - counts - ->Dict.toArray - ->Array.toSorted((a, b) => { - let (_, countA) = a - let (_, countB) = b - Float.fromInt(countB - countA) - }) - ->Array.slice(~start=0, ~end=topN) - } -} - -// Recommendation Engine -module RecommendationEngine = { - let getLearnerDomains = ( - experiences: array, - learnerId: string, - ): array => { - experiences - ->Array.filter(exp => exp.learner.id == learnerId) - ->Array.reduce([], (acc, exp) => { - switch exp.experience.domains { - | Some(domains) => Array.concat(acc, domains) - | None => acc - } - }) - ->Array.reduce([], (acc, d) => { - if acc->Array.includes(d) {acc} else {acc->Array.concat([d])} - }) - } - - let getVisitedLocations = ( - experiences: array, - learnerId: string, - ): array => { - experiences - ->Array.filter(exp => exp.learner.id == learnerId) - ->Array.map(exp => exp.context.location.name) - ->Array.reduce([], (acc, loc) => { - if acc->Array.includes(loc) {acc} else {acc->Array.concat([loc])} - }) - } - - let jaccardSimilarity = (set1: array, set2: array): float => { - let intersection = set1->Array.filter(x => set2->Array.includes(x))->Array.length - let union = Array.concat(set1, set2)->Array.reduce([], (acc, x) => { - if acc->Array.includes(x) {acc} else {acc->Array.concat([x])} - })->Array.length - - union == 0 ? 0.0 : Int.toFloat(intersection) /. Int.toFloat(union) - } - - let recommendSimilarLearners = ( - experiences: array, - learnerId: string, - ~topN=5, - (), - ): array => { - let targetDomains = getLearnerDomains(experiences, learnerId) - - if targetDomains->Array.length == 0 { - [] - } else { - let allLearners = experiences - ->Array.map(exp => exp.learner.id) - ->Array.reduce([], (acc, id) => { - if acc->Array.includes(id) {acc} else {acc->Array.concat([id])} - }) - - let similarities = allLearners - ->Array.filter(id => id != learnerId) - ->Array.map(otherLearnerId => { - let otherDomains = getLearnerDomains(experiences, otherLearnerId) - let similarity = jaccardSimilarity(targetDomains, otherDomains) - - { - learnerId: otherLearnerId, - similarity, - sharedDomains: targetDomains->Array.filter(d => otherDomains->Array.includes(d)), - } - }) - ->Array.filter(r => r.similarity > 0.0) - - similarities - ->Array.toSorted((a, b) => Float.compare(b.similarity, a.similarity)) - ->Array.slice(~start=0, ~end=topN) - } - } - - let recommendLocations = ( - experiences: array, - learnerId: string, - ~topN=5, - (), - ): array => { - let learnerDomains = getLearnerDomains(experiences, learnerId) - let visitedLocations = getVisitedLocations(experiences, learnerId) - - // Group experiences by location - let locationExps = experiences->Array.reduce(Dict.make(), (acc, exp) => { - let location = exp.context.location.name - let exps = acc->Dict.get(location)->Option.getOr([]) - acc->Dict.set(location, exps->Array.concat([exp])) - acc - }) - - let scores = [] - - locationExps->Dict.toArray->Array.forEach(((locationName, exps)) => { - if !(visitedLocations->Array.includes(locationName)) { - let locationDomains = extractDomains(exps) - let relevance = jaccardSimilarity(learnerDomains, locationDomains) - - if relevance > 0.0 { - scores - ->Array.push({ - location: locationName, - relevance, - matchingDomains: learnerDomains->Array.filter(d => locationDomains->Array.includes(d)), - }) - ->ignore - } - } - }) - - scores - ->Array.toSorted((a, b) => Float.compare(b.relevance, a.relevance)) - ->Array.slice(~start=0, ~end=topN) - } - - let recommendDomains = ( - experiences: array, - learnerId: string, - ~topN=5, - (), - ): array => { - let learnerDomains = getLearnerDomains(experiences, learnerId) - - // Find domains that co-occur with learner's domains - let coOccurrences = Dict.make() - - experiences->Array.forEach(exp => { - switch exp.experience.domains { - | Some(domains) => { - let hasLearnerDomain = domains->Array.some(d => learnerDomains->Array.includes(d)) - - if hasLearnerDomain { - domains->Array.forEach(d => { - if !(learnerDomains->Array.includes(d)) { - let count = coOccurrences->Dict.get(d)->Option.getOr(0) - coOccurrences->Dict.set(d, count + 1) - } - }) - } - } - | None => () - } - }) - - coOccurrences - ->Dict.toArray - ->Array.toSorted((a, b) => { - let (_, countA) = a - let (_, countB) = b - Float.fromInt(countB - countA) - }) - ->Array.slice(~start=0, ~end=topN) - ->Array.map(((domain, count)) => {domain, relevance: count}) - } -} diff --git a/src-rescript/Analysis.res.js b/src-rescript/Analysis.res.js deleted file mode 100644 index c7ed8b9..0000000 --- a/src-rescript/Analysis.res.js +++ /dev/null @@ -1,445 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Caml from "rescript/lib/es6/caml.js"; -import * as Caml_obj from "rescript/lib/es6/caml_obj.js"; -import * as Core__Array from "@rescript/core/src/Core__Array.res.js"; -import * as Core__Option from "@rescript/core/src/Core__Option.res.js"; - -function getTimeOfDay(timestamp) { - var date = new Date(timestamp); - var hour = date.getHours(); - if (hour >= 6 && hour < 12) { - return "Morning"; - } else if (hour >= 12 && hour < 18) { - return "Afternoon"; - } else if (hour >= 18 && hour < 22) { - return "Evening"; - } else { - return "Night"; - } -} - -function extractDomains(experiences) { - return Core__Array.reduce(Core__Array.reduce(experiences, [], (function (acc, exp) { - var domains = exp.experience.domains; - if (domains !== undefined) { - return acc.concat(domains); - } else { - return acc; - } - })), [], (function (acc, domain) { - if (acc.includes(domain)) { - return acc; - } else { - return acc.concat([domain]); - } - })); -} - -function analyzeByTimeOfDay(experiences) { - var morningExps = []; - var afternoonExps = []; - var eveningExps = []; - var nightExps = []; - experiences.forEach(function (exp) { - var tod = getTimeOfDay(exp.timestamp); - switch (tod) { - case "Morning" : - morningExps.push(exp); - return ; - case "Afternoon" : - afternoonExps.push(exp); - return ; - case "Evening" : - eveningExps.push(exp); - return ; - case "Night" : - nightExps.push(exp); - return ; - - } - }); - return { - morning: [ - morningExps.length, - extractDomains(morningExps) - ], - afternoon: [ - afternoonExps.length, - extractDomains(afternoonExps) - ], - evening: [ - eveningExps.length, - extractDomains(eveningExps) - ], - night: [ - nightExps.length, - extractDomains(nightExps) - ] - }; -} - -function analyzeByDayOfWeek(experiences) { - var days = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" - ]; - var distribution = {}; - days.forEach(function (day) { - distribution[day] = [ - 0, - [] - ]; - }); - experiences.forEach(function (exp) { - var date = new Date(exp.timestamp); - var dayIndex = date.getDay(); - var dayName = Core__Option.getOr(days[dayIndex], "Sunday"); - var match = Core__Option.getOr(distribution[dayName], [ - 0, - [] - ]); - var domains = match[1]; - var doms = exp.experience.domains; - var newDomains = doms !== undefined ? domains.concat(doms) : domains; - distribution[dayName] = [ - match[0] + 1 | 0, - newDomains - ]; - }); - Object.keys(distribution).forEach(function (day) { - var match = Core__Option.getOr(distribution[day], [ - 0, - [] - ]); - var uniqueDomains = Core__Array.reduce(match[1], [], (function (acc, d) { - if (acc.includes(d)) { - return acc; - } else { - return acc.concat([d]); - } - })); - distribution[day] = [ - match[0], - uniqueDomains - ]; - }); - return distribution; -} - -function detectStreaks(experiences, minDaysOpt, param) { - var minDays = minDaysOpt !== undefined ? minDaysOpt : 3; - var sorted = experiences.toSorted(function (a, b) { - var aDate = new Date(a.timestamp); - var bDate = new Date(b.timestamp); - if (Caml_obj.lessthan(aDate, bDate)) { - return -1.0; - } else { - return 1.0; - } - }); - var streaks = []; - var currentStreak = []; - sorted.forEach(function (exp, i) { - if (i === 0) { - currentStreak.push(exp); - return ; - } - var prevExp = Core__Option.getExn(sorted[i - 1 | 0], undefined); - var prevDate = new Date(prevExp.timestamp); - var currDate = new Date(exp.timestamp); - var daysDiff = Math.floor((currDate.getTime() - prevDate.getTime()) / 86400000.0) | 0; - if (daysDiff <= 1) { - currentStreak.push(exp); - return ; - } - if (currentStreak.length >= minDays) { - var firstExp = Core__Option.getExn(currentStreak[0], undefined); - var lastExp = Core__Option.getExn(currentStreak[currentStreak.length - 1 | 0], undefined); - streaks.push({ - start: firstExp.timestamp, - end_: lastExp.timestamp, - days: currentStreak.length, - experiences: currentStreak.length - }); - } - currentStreak.splice(0, currentStreak.length); - currentStreak.push(exp); - }); - if (currentStreak.length >= minDays) { - var firstExp = Core__Option.getExn(currentStreak[0], undefined); - var lastExp = Core__Option.getExn(currentStreak[currentStreak.length - 1 | 0], undefined); - streaks.push({ - start: firstExp.timestamp, - end_: lastExp.timestamp, - days: currentStreak.length, - experiences: currentStreak.length - }); - } - return streaks; -} - -var TemporalAnalyzer = { - analyzeByTimeOfDay: analyzeByTimeOfDay, - analyzeByDayOfWeek: analyzeByDayOfWeek, - detectStreaks: detectStreaks -}; - -function buildCollaborationNetwork(experiences) { - var edges = {}; - var allLearners = []; - experiences.forEach(function (exp) { - var learnerId = exp.learner.id; - allLearners.push(learnerId); - var collaborators = exp.context.connections; - if (collaborators !== undefined) { - collaborators.forEach(function (collaborator) { - var key = [ - learnerId, - collaborator - ].toSorted(function (a, b) { - if (a < b) { - return -1.0; - } else { - return 1.0; - } - }).join("-"); - var count = Core__Option.getOr(edges[key], 0); - edges[key] = count + 1 | 0; - allLearners.push(collaborator); - }); - return ; - } - - }); - var uniqueLearners = Core__Array.reduce(allLearners, [], (function (acc, id) { - if (acc.includes(id)) { - return acc; - } else { - return acc.concat([id]); - } - })); - var learnerCounts = {}; - experiences.forEach(function (exp) { - var count = Core__Option.getOr(learnerCounts[exp.learner.id], 0); - learnerCounts[exp.learner.id] = count + 1 | 0; - }); - return { - nodes: uniqueLearners.map(function (id) { - return { - id: id, - experiences: Core__Option.getOr(learnerCounts[id], 0) - }; - }), - edges: Object.entries(edges).map(function (param) { - var parts = param[0].split("-"); - return { - source: Core__Option.getOr(parts[0], ""), - target: Core__Option.getOr(parts[1], ""), - weight: param[1] - }; - }) - }; -} - -function findMostCollaborative(experiences, topNOpt, param) { - var topN = topNOpt !== undefined ? topNOpt : 10; - var counts = {}; - experiences.forEach(function (exp) { - var collaborators = exp.context.connections; - if (collaborators === undefined) { - return ; - } - if (collaborators.length <= 0) { - return ; - } - var count = Core__Option.getOr(counts[exp.learner.id], 0); - counts[exp.learner.id] = count + collaborators.length | 0; - }); - return Object.entries(counts).toSorted(function (a, b) { - return b[1] - a[1] | 0; - }).slice(0, topN); -} - -var CollaborativeNetworkAnalyzer = { - buildCollaborationNetwork: buildCollaborationNetwork, - findMostCollaborative: findMostCollaborative -}; - -function getLearnerDomains(experiences, learnerId) { - return Core__Array.reduce(Core__Array.reduce(experiences.filter(function (exp) { - return exp.learner.id === learnerId; - }), [], (function (acc, exp) { - var domains = exp.experience.domains; - if (domains !== undefined) { - return acc.concat(domains); - } else { - return acc; - } - })), [], (function (acc, d) { - if (acc.includes(d)) { - return acc; - } else { - return acc.concat([d]); - } - })); -} - -function getVisitedLocations(experiences, learnerId) { - return Core__Array.reduce(experiences.filter(function (exp) { - return exp.learner.id === learnerId; - }).map(function (exp) { - return exp.context.location.name; - }), [], (function (acc, loc) { - if (acc.includes(loc)) { - return acc; - } else { - return acc.concat([loc]); - } - })); -} - -function jaccardSimilarity(set1, set2) { - var intersection = set1.filter(function (x) { - return set2.includes(x); - }).length; - var union = Core__Array.reduce(set1.concat(set2), [], (function (acc, x) { - if (acc.includes(x)) { - return acc; - } else { - return acc.concat([x]); - } - })).length; - if (union === 0) { - return 0.0; - } else { - return intersection / union; - } -} - -function recommendSimilarLearners(experiences, learnerId, topNOpt, param) { - var topN = topNOpt !== undefined ? topNOpt : 5; - var targetDomains = getLearnerDomains(experiences, learnerId); - if (targetDomains.length === 0) { - return []; - } - var allLearners = Core__Array.reduce(experiences.map(function (exp) { - return exp.learner.id; - }), [], (function (acc, id) { - if (acc.includes(id)) { - return acc; - } else { - return acc.concat([id]); - } - })); - var similarities = allLearners.filter(function (id) { - return id !== learnerId; - }).map(function (otherLearnerId) { - var otherDomains = getLearnerDomains(experiences, otherLearnerId); - var similarity = jaccardSimilarity(targetDomains, otherDomains); - return { - learnerId: otherLearnerId, - similarity: similarity, - sharedDomains: targetDomains.filter(function (d) { - return otherDomains.includes(d); - }) - }; - }).filter(function (r) { - return r.similarity > 0.0; - }); - return similarities.toSorted(function (a, b) { - return Caml.float_compare(b.similarity, a.similarity); - }).slice(0, topN); -} - -function recommendLocations(experiences, learnerId, topNOpt, param) { - var topN = topNOpt !== undefined ? topNOpt : 5; - var learnerDomains = getLearnerDomains(experiences, learnerId); - var visitedLocations = getVisitedLocations(experiences, learnerId); - var locationExps = Core__Array.reduce(experiences, {}, (function (acc, exp) { - var $$location = exp.context.location.name; - var exps = Core__Option.getOr(acc[$$location], []); - acc[$$location] = exps.concat([exp]); - return acc; - })); - var scores = []; - Object.entries(locationExps).forEach(function (param) { - var locationName = param[0]; - if (visitedLocations.includes(locationName)) { - return ; - } - var locationDomains = extractDomains(param[1]); - var relevance = jaccardSimilarity(learnerDomains, locationDomains); - if (relevance > 0.0) { - scores.push({ - location: locationName, - relevance: relevance, - matchingDomains: learnerDomains.filter(function (d) { - return locationDomains.includes(d); - }) - }); - return ; - } - - }); - return scores.toSorted(function (a, b) { - return Caml.float_compare(b.relevance, a.relevance); - }).slice(0, topN); -} - -function recommendDomains(experiences, learnerId, topNOpt, param) { - var topN = topNOpt !== undefined ? topNOpt : 5; - var learnerDomains = getLearnerDomains(experiences, learnerId); - var coOccurrences = {}; - experiences.forEach(function (exp) { - var domains = exp.experience.domains; - if (domains === undefined) { - return ; - } - var hasLearnerDomain = domains.some(function (d) { - return learnerDomains.includes(d); - }); - if (hasLearnerDomain) { - domains.forEach(function (d) { - if (learnerDomains.includes(d)) { - return ; - } - var count = Core__Option.getOr(coOccurrences[d], 0); - coOccurrences[d] = count + 1 | 0; - }); - return ; - } - - }); - return Object.entries(coOccurrences).toSorted(function (a, b) { - return b[1] - a[1] | 0; - }).slice(0, topN).map(function (param) { - return { - domain: param[0], - relevance: param[1] - }; - }); -} - -var RecommendationEngine = { - getLearnerDomains: getLearnerDomains, - getVisitedLocations: getVisitedLocations, - jaccardSimilarity: jaccardSimilarity, - recommendSimilarLearners: recommendSimilarLearners, - recommendLocations: recommendLocations, - recommendDomains: recommendDomains -}; - -export { - getTimeOfDay , - extractDomains , - TemporalAnalyzer , - CollaborativeNetworkAnalyzer , - RecommendationEngine , -} -/* No side effect */ diff --git a/src-rescript/Capture.res b/src-rescript/Capture.res deleted file mode 100644 index ed24888..0000000 --- a/src-rescript/Capture.res +++ /dev/null @@ -1,398 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity Learning Experience Capture Tool -// This module implements the interactive capture logic for the CLI. - -open UbiCity - -// Workflow modes for data capture. -type captureMode = Quick | Full | Template - -module CaptureSession = { - // Session state container. - type t = { - mode: captureMode, - mapper: option, // Initialized mapper service for storage/retrieval - } - - // Initialize a new session record. - let make = (~mode=Quick, ()): t => { - {mode, mapper: None} - } - - // Async initialization logic. Loads the Mapper service and handles errors. - let initialize = (session: t): promise> => { - // Initialize mapper - Mapper.make() - ->Promise.then(mapper => { - Promise.resolve(Ok({...session, mapper: Some(mapper)})) - }) - ->Promise.catch(_ => { - Promise.resolve(Error("Failed to initialize mapper")) - }) - } - - // FFI: Process bindings for standard input/output. - module Process = { - @val external stdin: 'a = "process.stdin" - @val external stdout: 'a = "process.stdout" - } - - // FFI: Node.js readline/promises module for interactive CLI input. - module Readline = { - type t - - @send - external question: (t, string) => promise = "question" - - @send - external close: t => unit = "close" - - @module("readline/promises") - external createInterface: {..} => t = "createInterface" - - let make = (): t => { - createInterface({ - "input": Process.stdin, - "output": Process.stdout, - }) - } - } - - // PROMPT: WHO is learning? - let captureLearner = async ( - readline: Readline.t, - mode: captureMode, - ): result => { - Console.log("WHO learned?\n") - - let id = await readline->Readline.question("Learner ID (pseudonym): ") - - if id->String.trim->String.length == 0 { - Error("Learner ID is required") - } else { - let learnerId = id->String.trim - - switch mode { - | Full => { - let name = await readline->Readline.question("Full name (optional): ") - let nameOpt = if name->String.trim->String.length > 0 { - Some(name->String.trim) - } else { - None - } - - let interests = await readline->Readline.question( - "Interests (comma-separated, optional): ", - ) - let interestsOpt = if interests->String.trim->String.length > 0 { - Some( - interests - ->String.split(",") - ->Array.map(String.trim) - ->Array.filter(s => s->String.length > 0), - ) - } else { - None - } - - Learner.make(~id=learnerId, ~name=?nameOpt, ~interests=?interestsOpt, ()) - } - | Quick | Template => Learner.make(~id=learnerId, ()) - } - } - } - - // PROMPT: WHERE did it happen? (Includes GPS support in Full mode) - let captureLocation = async ( - readline: Readline.t, - mode: captureMode, - ): result => { - let locationName = await readline->Readline.question("Location name: ") - - if locationName->String.trim->String.length == 0 { - Error("Location name is required") - } else { - let name = locationName->String.trim - - switch mode { - | Full => { - let hasCoords = await readline->Readline.question("Add GPS coordinates? (y/n): ") - - let coords = if hasCoords->String.toLowerCase == "y" { - let lat = await readline->Readline.question("Latitude: ") - let lon = await readline->Readline.question("Longitude: ") - - switch (lat->Float.fromString, lon->Float.fromString) { - | (Some(latitude), Some(longitude)) => - Coordinates.make(~latitude, ~longitude) - | _ => None - } - } else { - None - } - - let locationType = await readline->Readline.question( - "Location type (makerspace/library/park/etc, optional): ", - ) - let typeOpt = if locationType->String.trim->String.length > 0 { - Some(locationType->String.trim) - } else { - None - } - - Location.make(~name, ~coordinates=?coords, ~type_=?typeOpt, ()) - } - | Quick | Template => Location.make(~name, ()) - } - } - } - - // Orchestrates the capture of physical and situational context. - let captureContext = async ( - readline: Readline.t, - mode: captureMode, - ): result => { - Console.log("\nWHERE did learning happen?\n") - - let locationResult = await captureLocation(readline, mode) - - switch locationResult { - | Error(err) => Error(err) - | Ok(location) => - switch mode { - | Full => { - let situation = await readline->Readline.question("Situation/context (optional): ") - let situationOpt = if situation->String.trim->String.length > 0 { - Some(situation->String.trim) - } else { - None - } - - let connections = await readline->Readline.question( - "Others involved (comma-separated, optional): ", - ) - let connectionsOpt = if connections->String.trim->String.length > 0 { - Some( - connections - ->String.split(",") - ->Array.map(String.trim) - ->Array.filter(s => s->String.length > 0), - ) - } else { - None - } - - Ok(Context.make(~location, ~situation=?situationOpt, ~connections=?connectionsOpt, ())) - } - | Quick | Template => Ok(Context.make(~location, ())) - } - } - } - - // PROMPT: What was the outcome? - let captureOutcome = async (readline: Readline.t): Outcome.t => { - Console.log("\nOutcome (optional):\n") - - let success = await readline->Readline.question("Was it successful? (y/n/skip): ") - - let outcome = switch success->String.toLowerCase { - | "y" => {...Outcome.empty, success: Some(true)} - | "n" => {...Outcome.empty, success: Some(false)} - | _ => Outcome.empty - } - - switch outcome.success { - | Some(_) => { - let connections = await readline->Readline.question( - "Unexpected connections made (optional): ", - ) - let connectionsMade = if connections->String.trim->String.length > 0 { - Some([connections->String.trim]) - } else { - None - } - - let questions = await readline->Readline.question("New questions emerged (optional): ") - let nextQuestions = if questions->String.trim->String.length > 0 { - Some([questions->String.trim]) - } else { - None - } - - { - ...outcome, - connections_made: connectionsMade, - next_questions: nextQuestions, - } - } - | None => outcome - } - } - - // PROMPT: WHAT was learned? - let captureExperience = async ( - readline: Readline.t, - mode: captureMode, - ): result => { - Console.log("\nWHAT was learned?\n") - - let type_ = await readline->Readline.question( - "Type (experiment/workshop/observation/conversation/reading/making): ", - ) - - if type_->String.trim->String.length == 0 { - Error("Experience type is required") - } else { - let description = await readline->Readline.question("Description: ") - - if description->String.trim->String.length == 0 { - Error("Description is required") - } else { - let domains = await readline->Readline.question( - "Domains/disciplines (comma-separated, optional but useful): ", - ) - let domainsOpt = if domains->String.trim->String.length > 0 { - Some( - domains - ->String.split(",") - ->Array.map(String.trim) - ->Array.filter(s => s->String.length > 0), - ) - } else { - None - } - - switch mode { - | Full => { - let outcome = await captureOutcome(readline) - - let intensity = await readline->Readline.question( - "Intensity (low/medium/high, optional): ", - ) - let intensityOpt = switch intensity->String.trim->String.toLowerCase { - | "low" => Some(#low) - | "medium" => Some(#medium) - | "high" => Some(#high) - | _ => None - } - - ExperienceData.make( - ~type_=type_->String.trim, - ~description=description->String.trim, - ~domains=?domainsOpt, - ~outcome=outcome, - ~intensity=?intensityOpt, - (), - ) - } - | Quick | Template => - ExperienceData.make( - ~type_=type_->String.trim, - ~description=description->String.trim, - ~domains=?domainsOpt, - (), - ) - } - } - } - } - - // CAPTURE PIPELINE: The main entry point for the capture process. - let capture = async (session: t): result => { - let readline = Readline.make() - - Console.log("\n🏙️ UbiCity Learning Capture\n") - - switch session.mode { - | Template => { - // Generation logic for a blank JSON template. - let _ = await generateTemplate(readline) - Ok("template-generated") - } - | mode => { - let sessionResult = await session->initialize - - switch sessionResult { - | Error(err) => { - readline->Readline.close - Error(err) - } - | Ok(initializedSession) => - switch initializedSession.mapper { - | None => { - readline->Readline.close - Error("Mapper not initialized") - } - | Some(mapper) => { - // STEP 1: Capture identity - let learnerResult = await captureLearner(readline, mode) - - switch learnerResult { - | Error(err) => { - readline->Readline.close - Error(err) - } - | Ok(learner) => { - // STEP 2: Capture location - let contextResult = await captureContext(readline, mode) - - switch contextResult { - | Error(err) => { - readline->Readline.close - Error(err) - } - | Ok(context) => { - // STEP 3: Capture the learning content - let experienceResult = await captureExperience(readline, mode) - - switch experienceResult { - | Error(err) => { - readline->Readline.close - Error(err) - } - | Ok(experience) => { - // STEP 4: Metadata and Privacy - let (privacy, tags) = switch mode { - | Full => await captureOptionalFields(readline) - | Quick | Template => (None, None) - } - - // STEP 5: Aggregate and Save - let learningExperience = LearningExperience.make( - ~learner, - ~context, - ~experience, - ~privacy=?privacy, - ~tags=?tags, - (), - ) - - let saveResult = await Mapper.captureExperience( - mapper, - learningExperience, - ) - - readline->Readline.close - - switch saveResult { - | Ok(id) => { - Console.log(`\n✅ Experience captured: ${id}`) - let count = Mapper.getExperienceCount(mapper) - Console.log(`📊 Total experiences: ${count->Int.toString}\n`) - Ok(id) - } - | Error(err) => Error(err) - } - } - } - } - } - } - } - } - } - } - } - } - } -} diff --git a/src-rescript/Capture.res.js b/src-rescript/Capture.res.js deleted file mode 100644 index 7b0b02e..0000000 --- a/src-rescript/Capture.res.js +++ /dev/null @@ -1,435 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Mapper from "./Mapper.res.js"; -import * as UbiCity from "./UbiCity.res.js"; -import * as Core__Float from "@rescript/core/src/Core__Float.res.js"; -import * as Core__Promise from "@rescript/core/src/Core__Promise.res.js"; -import * as Promises from "readline/promises"; - -function make(modeOpt, param) { - var mode = modeOpt !== undefined ? modeOpt : "Quick"; - return { - mode: mode, - mapper: undefined - }; -} - -function initialize(session) { - return Core__Promise.$$catch(Mapper.make(undefined, undefined).then(function (mapper) { - return Promise.resolve({ - TAG: "Ok", - _0: { - mode: session.mode, - mapper: mapper - } - }); - }), (function (param) { - return Promise.resolve({ - TAG: "Error", - _0: "Failed to initialize mapper" - }); - })); -} - -var Process = {}; - -function make$1() { - return Promises.createInterface({ - input: process.stdin, - output: process.stdout - }); -} - -var Readline = { - make: make$1 -}; - -async function captureLearner(readline, mode) { - console.log("WHO learned?\n"); - var id = await readline.question("Learner ID (pseudonym): "); - if (id.trim().length === 0) { - return { - TAG: "Error", - _0: "Learner ID is required" - }; - } - var learnerId = id.trim(); - switch (mode) { - case "Full" : - var name = await readline.question("Full name (optional): "); - var nameOpt = name.trim().length > 0 ? name.trim() : undefined; - var interests = await readline.question("Interests (comma-separated, optional): "); - var interestsOpt = interests.trim().length > 0 ? interests.split(",").map(function (prim) { - return prim.trim(); - }).filter(function (s) { - return s.length > 0; - }) : undefined; - return UbiCity.Learner.make(learnerId, nameOpt, interestsOpt, undefined); - case "Quick" : - case "Template" : - break; - - } - return UbiCity.Learner.make(learnerId, undefined, undefined, undefined); -} - -async function captureLocation(readline, mode) { - var locationName = await readline.question("Location name: "); - if (locationName.trim().length === 0) { - return { - TAG: "Error", - _0: "Location name is required" - }; - } - var name = locationName.trim(); - switch (mode) { - case "Full" : - var hasCoords = await readline.question("Add GPS coordinates? (y/n): "); - var coords; - if (hasCoords.toLowerCase() === "y") { - var lat = await readline.question("Latitude: "); - var lon = await readline.question("Longitude: "); - var match = Core__Float.fromString(lat); - var match$1 = Core__Float.fromString(lon); - coords = match !== undefined && match$1 !== undefined ? UbiCity.Coordinates.make(match, match$1) : undefined; - } else { - coords = undefined; - } - var locationType = await readline.question("Location type (makerspace/library/park/etc, optional): "); - var typeOpt = locationType.trim().length > 0 ? locationType.trim() : undefined; - return UbiCity.$$Location.make(name, coords, typeOpt, undefined, undefined); - case "Quick" : - case "Template" : - break; - - } - return UbiCity.$$Location.make(name, undefined, undefined, undefined, undefined); -} - -async function captureContext(readline, mode) { - console.log("\nWHERE did learning happen?\n"); - var locationResult = await captureLocation(readline, mode); - if (locationResult.TAG !== "Ok") { - return { - TAG: "Error", - _0: locationResult._0 - }; - } - var $$location = locationResult._0; - switch (mode) { - case "Full" : - var situation = await readline.question("Situation/context (optional): "); - var situationOpt = situation.trim().length > 0 ? situation.trim() : undefined; - var connections = await readline.question("Others involved (comma-separated, optional): "); - var connectionsOpt = connections.trim().length > 0 ? connections.split(",").map(function (prim) { - return prim.trim(); - }).filter(function (s) { - return s.length > 0; - }) : undefined; - return { - TAG: "Ok", - _0: UbiCity.Context.make($$location, situationOpt, connectionsOpt, undefined, undefined) - }; - case "Quick" : - case "Template" : - break; - - } - return { - TAG: "Ok", - _0: UbiCity.Context.make($$location, undefined, undefined, undefined, undefined) - }; -} - -async function captureOutcome(readline) { - console.log("\nOutcome (optional):\n"); - var success = await readline.question("Was it successful? (y/n/skip): "); - var match = success.toLowerCase(); - var outcome; - switch (match) { - case "n" : - var init = UbiCity.Outcome.empty; - outcome = { - success: false, - connections_made: init.connections_made, - next_questions: init.next_questions, - artifacts: init.artifacts - }; - break; - case "y" : - var init$1 = UbiCity.Outcome.empty; - outcome = { - success: true, - connections_made: init$1.connections_made, - next_questions: init$1.next_questions, - artifacts: init$1.artifacts - }; - break; - default: - outcome = UbiCity.Outcome.empty; - } - var match$1 = outcome.success; - if (match$1 === undefined) { - return outcome; - } - var connections = await readline.question("Unexpected connections made (optional): "); - var connectionsMade = connections.trim().length > 0 ? [connections.trim()] : undefined; - var questions = await readline.question("New questions emerged (optional): "); - var nextQuestions = questions.trim().length > 0 ? [questions.trim()] : undefined; - return { - success: outcome.success, - connections_made: connectionsMade, - next_questions: nextQuestions, - artifacts: outcome.artifacts - }; -} - -async function captureExperience(readline, mode) { - console.log("\nWHAT was learned?\n"); - var type_ = await readline.question("Type (experiment/workshop/observation/conversation/reading/making): "); - if (type_.trim().length === 0) { - return { - TAG: "Error", - _0: "Experience type is required" - }; - } - var description = await readline.question("Description: "); - if (description.trim().length === 0) { - return { - TAG: "Error", - _0: "Description is required" - }; - } - var domains = await readline.question("Domains/disciplines (comma-separated, optional but useful): "); - var domainsOpt = domains.trim().length > 0 ? domains.split(",").map(function (prim) { - return prim.trim(); - }).filter(function (s) { - return s.length > 0; - }) : undefined; - switch (mode) { - case "Full" : - var outcome = await captureOutcome(readline); - var intensity = await readline.question("Intensity (low/medium/high, optional): "); - var match = intensity.trim().toLowerCase(); - var intensityOpt; - switch (match) { - case "high" : - intensityOpt = "high"; - break; - case "low" : - intensityOpt = "low"; - break; - case "medium" : - intensityOpt = "medium"; - break; - default: - intensityOpt = undefined; - } - return UbiCity.ExperienceData.make(type_.trim(), description.trim(), domainsOpt, outcome, undefined, intensityOpt, undefined); - case "Quick" : - case "Template" : - break; - - } - return UbiCity.ExperienceData.make(type_.trim(), description.trim(), domainsOpt, undefined, undefined, undefined, undefined); -} - -async function captureOptionalFields(readline) { - console.log("\nOptional metadata:\n"); - var privacy = await readline.question("Privacy level (private/anonymous/public) [default: anonymous]: "); - var match = privacy.trim().toLowerCase(); - var privacyOpt; - switch (match) { - case "private" : - privacyOpt = { - level: "private", - shareableWith: undefined - }; - break; - case "public" : - privacyOpt = { - level: "public", - shareableWith: undefined - }; - break; - default: - privacyOpt = UbiCity.Privacy.makeAnonymous; - } - var tags = await readline.question("Tags (comma-separated, optional): "); - var tagsOpt = tags.trim().length > 0 ? tags.split(",").map(function (prim) { - return prim.trim(); - }).filter(function (s) { - return s.length > 0; - }) : undefined; - return [ - privacyOpt, - tagsOpt - ]; -} - -function generateTemplate(readline) { - var template = { - learner: { - id: "your-pseudonym", - name: "Your Name (optional)", - interests: [ - "interest1", - "interest2" - ] - }, - context: { - location: { - name: "Location Name", - coordinates: { - latitude: 0.0, - longitude: 0.0 - }, - type: "makerspace" - }, - situation: "What was happening", - connections: [ - "person1", - "person2" - ] - }, - experience: { - type: "experiment", - description: "What you learned", - domains: [ - "domain1", - "domain2" - ], - outcome: { - success: true, - connections_made: ["Unexpected insight"], - next_questions: ["What question emerged?"] - }, - duration: 60, - intensity: "medium" - }, - privacy: { - level: "anonymous" - }, - tags: [ - "tag1", - "tag2" - ] - }; - var json = JSON.stringify(template); - if (json !== undefined) { - console.log(json); - } else { - console.log("Error: Could not generate template"); - } - readline.close(); - return Promise.resolve(); -} - -async function capture(session) { - var readline = make$1(); - console.log("\n🏙️ UbiCity Learning Capture\n"); - var mode = session.mode; - switch (mode) { - case "Quick" : - case "Full" : - break; - case "Template" : - await generateTemplate(readline); - return { - TAG: "Ok", - _0: "template-generated" - }; - - } - var sessionResult = await initialize(session); - if (sessionResult.TAG === "Ok") { - var mapper = sessionResult._0.mapper; - if (mapper !== undefined) { - var learnerResult = await captureLearner(readline, mode); - if (learnerResult.TAG === "Ok") { - var contextResult = await captureContext(readline, mode); - if (contextResult.TAG === "Ok") { - var experienceResult = await captureExperience(readline, mode); - if (experienceResult.TAG === "Ok") { - var match; - switch (mode) { - case "Full" : - match = await captureOptionalFields(readline); - break; - case "Quick" : - case "Template" : - match = [ - undefined, - undefined - ]; - break; - - } - var learningExperience = UbiCity.LearningExperience.make(undefined, undefined, learnerResult._0, contextResult._0, experienceResult._0, match[0], match[1], undefined, undefined); - var saveResult = await Mapper.captureExperience(mapper, learningExperience); - readline.close(); - if (saveResult.TAG !== "Ok") { - return { - TAG: "Error", - _0: saveResult._0 - }; - } - var id = saveResult._0; - console.log("\n✅ Experience captured: " + id); - var count = Mapper.getExperienceCount(mapper); - console.log("📊 Total experiences: " + count.toString() + "\n"); - return { - TAG: "Ok", - _0: id - }; - } - readline.close(); - return { - TAG: "Error", - _0: experienceResult._0 - }; - } - readline.close(); - return { - TAG: "Error", - _0: contextResult._0 - }; - } - readline.close(); - return { - TAG: "Error", - _0: learnerResult._0 - }; - } - readline.close(); - return { - TAG: "Error", - _0: "Mapper not initialized" - }; - } - readline.close(); - return { - TAG: "Error", - _0: sessionResult._0 - }; -} - -var CaptureSession = { - make: make, - initialize: initialize, - Process: Process, - Readline: Readline, - captureLearner: captureLearner, - captureLocation: captureLocation, - captureContext: captureContext, - captureOutcome: captureOutcome, - captureExperience: captureExperience, - captureOptionalFields: captureOptionalFields, - generateTemplate: generateTemplate, - capture: capture -}; - -export { - CaptureSession , -} -/* Mapper Not a pure module */ diff --git a/src-rescript/CaptureCLI.res b/src-rescript/CaptureCLI.res deleted file mode 100644 index 12c881c..0000000 --- a/src-rescript/CaptureCLI.res +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity Capture CLI Entry Point - -@val external argv: array = "process.argv" -@val external exit: int => unit = "process.exit" - -let parseMode = (modeStr: option): Capture.captureMode => { - switch modeStr { - | Some("full") => Full - | Some("template") => Template - | Some("quick") | Some(_) | None => Quick - } -} - -let main = async () => { - let mode = argv->Array.get(2)->parseMode - - let session = Capture.CaptureSession.make(~mode, ()) - - let result = await session->Capture.CaptureSession.capture - - switch result { - | Ok(id) => { - Console.log(`\n✅ Success! Experience ID: ${id}`) - exit(0) - } - | Error(err) => { - Console.error(`\n❌ Error: ${err}`) - exit(1) - } - } -} - -// Run -let _ = main() diff --git a/src-rescript/CaptureCLI.res.js b/src-rescript/CaptureCLI.res.js deleted file mode 100644 index fe36cdc..0000000 --- a/src-rescript/CaptureCLI.res.js +++ /dev/null @@ -1,38 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Capture from "./Capture.res.js"; - -function parseMode(modeStr) { - if (modeStr === undefined) { - return "Quick"; - } - switch (modeStr) { - case "full" : - return "Full"; - case "template" : - return "Template"; - default: - return "Quick"; - } -} - -async function main() { - var mode = parseMode(process.argv[2]); - var session = Capture.CaptureSession.make(mode, undefined); - var result = await Capture.CaptureSession.capture(session); - if (result.TAG === "Ok") { - console.log("\n✅ Success! Experience ID: " + result._0); - process.exit(0); - return ; - } - console.error("\n❌ Error: " + result._0); - process.exit(1); -} - -main(); - -export { - parseMode , - main , -} -/* Not a pure module */ diff --git a/src-rescript/Decoder.res b/src-rescript/Decoder.res deleted file mode 100644 index 0d38265..0000000 --- a/src-rescript/Decoder.res +++ /dev/null @@ -1,331 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity JSON Decoders -// Type-safe decoding of JSON data from storage into ReScript types - -open UbiCity - -// FFI bindings for generating IDs -@module("crypto") @val -external randomUUID: unit => string = "randomUUID" - -// Helper to get string field from JSON object -let getString = (json: JSON.t, field: string): result => { - switch json->JSON.Decode.object { - | Some(obj) => - switch obj->Dict.get(field) { - | Some(value) => - switch value->JSON.Decode.string { - | Some(str) => Ok(str) - | None => Error(`Field "${field}" is not a string`) - } - | None => Error(`Missing field "${field}"`) - } - | None => Error("Expected JSON object") - } -} - -// Helper to get optional string field -let getOptionalString = (json: JSON.t, field: string): option => { - json - ->JSON.Decode.object - ->Option.flatMap(obj => obj->Dict.get(field)) - ->Option.flatMap(JSON.Decode.string) -} - -// Helper to get array field -let getArray = (json: JSON.t, field: string): result, string> => { - switch json->JSON.Decode.object { - | Some(obj) => - switch obj->Dict.get(field) { - | Some(value) => - switch value->JSON.Decode.array { - | Some(arr) => Ok(arr) - | None => Error(`Field "${field}" is not an array`) - } - | None => Ok([]) // Missing array field = empty array - } - | None => Error("Expected JSON object") - } -} - -// Helper to get optional array field -let getOptionalArray = (json: JSON.t, field: string): option> => { - json - ->JSON.Decode.object - ->Option.flatMap(obj => obj->Dict.get(field)) - ->Option.flatMap(JSON.Decode.array) -} - -// Helper to get object field -let getObject = (json: JSON.t, field: string): result => { - switch json->JSON.Decode.object { - | Some(obj) => - switch obj->Dict.get(field) { - | Some(value) => Ok(value) - | None => Error(`Missing field "${field}"`) - } - | None => Error("Expected JSON object") - } -} - -// Helper to get optional object field -let getOptionalObject = (json: JSON.t, field: string): option => { - json->JSON.Decode.object->Option.flatMap(obj => obj->Dict.get(field)) -} - -// Helper to get number field -let getNumber = (json: JSON.t, field: string): result => { - switch json->JSON.Decode.object { - | Some(obj) => - switch obj->Dict.get(field) { - | Some(value) => - switch value->JSON.Decode.float { - | Some(num) => Ok(num) - | None => Error(`Field "${field}" is not a number`) - } - | None => Error(`Missing field "${field}"`) - } - | None => Error("Expected JSON object") - } -} - -// Helper to get optional number field -let getOptionalNumber = (json: JSON.t, field: string): option => { - json - ->JSON.Decode.object - ->Option.flatMap(obj => obj->Dict.get(field)) - ->Option.flatMap(JSON.Decode.float) -} - -// Decode Coordinates (handles both lat/lon and latitude/longitude) -let decodeCoordinates = (json: JSON.t): result => { - // Try latitude/longitude first, then fall back to lat/lon - let latResult = switch getNumber(json, "latitude") { - | Ok(lat) => Ok(lat) - | Error(_) => getNumber(json, "lat") - } - - let lonResult = switch getNumber(json, "longitude") { - | Ok(lon) => Ok(lon) - | Error(_) => getNumber(json, "lon") - } - - switch (latResult, lonResult) { - | (Ok(latitude), Ok(longitude)) => Ok({latitude, longitude}) - | (Error(err), _) | (_, Error(err)) => Error(`Coordinates decode error: ${err}`) - } -} - -// Decode Location -let decodeLocation = (json: JSON.t): result => { - switch getString(json, "name") { - | Error(err) => Error(`Location decode error: ${err}`) - | Ok(name) => { - let coordinates = getOptionalObject(json, "coordinates")->Option.flatMap(coordJson => { - switch decodeCoordinates(coordJson) { - | Ok(coords) => Some(coords) - | Error(_) => None - } - }) - - let type_ = getOptionalString(json, "type") - let address = getOptionalString(json, "address") - - Ok({name, coordinates, type_, address}) - } - } -} - -// Decode Learner -let decodeLearner = (json: JSON.t): result => { - switch getString(json, "id") { - | Error(err) => Error(`Learner decode error: ${err}`) - | Ok(id) => { - let name = getOptionalString(json, "name") - let interests = getOptionalArray(json, "interests")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - - Ok({id, name, interests}) - } - } -} - -// Decode Context -let decodeContext = (json: JSON.t): result => { - switch getObject(json, "location") { - | Error(err) => Error(`Context decode error: ${err}`) - | Ok(locationJson) => - switch decodeLocation(locationJson) { - | Error(err) => Error(`Context.location decode error: ${err}`) - | Ok(location) => { - let situation = getOptionalString(json, "situation") - let connections = getOptionalArray(json, "connections")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - let timeOfDay = getOptionalString(json, "timeOfDay")->Option.map(str => { - switch str { - | "morning" => #morning - | "afternoon" => #afternoon - | "evening" => #evening - | "night" | _ => #night - } - }) - - Ok({location, situation, connections, timeOfDay}) - } - } - } -} - -// Decode Outcome -let decodeOutcome = (json: JSON.t): result => { - let success = json - ->JSON.Decode.object - ->Option.flatMap(obj => obj->Dict.get("success")) - ->Option.flatMap(JSON.Decode.bool) - - let connections_made = getOptionalArray(json, "connections_made")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - - let next_questions = getOptionalArray(json, "next_questions")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - - let artifacts = getOptionalArray(json, "artifacts")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - - Ok({ - success, - connections_made, - next_questions, - artifacts, - }) -} - -// Decode ExperienceData -let decodeExperienceData = (json: JSON.t): result => { - switch getString(json, "type") { - | Error(err) => Error(`ExperienceData decode error: ${err}`) - | Ok(type_) => - switch getString(json, "description") { - | Error(err) => Error(`ExperienceData.description decode error: ${err}`) - | Ok(description) => { - let domains = getOptionalArray(json, "domains")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - - let outcome = getOptionalObject(json, "outcome")->Option.flatMap(outcomeJson => { - switch decodeOutcome(outcomeJson) { - | Ok(out) => Some(out) - | Error(_) => None - } - }) - - let duration = getOptionalNumber(json, "duration")->Option.map(Float.toInt) - let intensity = getOptionalString(json, "intensity")->Option.map(str => { - switch str { - | "low" => #low - | "medium" => #medium - | "high" | _ => #high - } - }) - - Ok({ - type_, - description, - domains, - outcome, - duration, - intensity, - }) - } - } - } -} - -// Decode Privacy -let decodePrivacy = (json: JSON.t): result => { - switch getString(json, "level") { - | Error(err) => Error(`Privacy decode error: ${err}`) - | Ok(levelStr) => { - let level = switch levelStr { - | "private" => #"private" - | "anonymous" => #anonymous - | "public" | _ => #public - } - - let shareableWith = getOptionalArray(json, "shareableWith")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - - Ok({level, shareableWith}) - } - } -} - -// Decode LearningExperience (with fallbacks for legacy data) -let decodeLearningExperience = (json: JSON.t): result => { - // Get core required fields - let learnerResult = getObject(json, "learner")->Result.flatMap(decodeLearner) - let contextResult = getObject(json, "context")->Result.flatMap(decodeContext) - let experienceResult = getObject(json, "experience")->Result.flatMap(decodeExperienceData) - - // Get optional fields with defaults for legacy data - let id = getOptionalString(json, "id")->Option.getOr(`ubi-${randomUUID()}`) - let timestamp = getOptionalString(json, "timestamp")->Option.getOr(Date.now()->Float.toString) - let version = getOptionalString(json, "version")->Option.getOr("1.0") - - // Combine all results - switch (learnerResult, contextResult, experienceResult) { - | (Ok(learner), Ok(context), Ok(experience)) => { - // Get optional fields - let privacy = getOptionalObject(json, "privacy")->Option.flatMap(privJson => { - switch decodePrivacy(privJson) { - | Ok(priv) => Some(priv) - | Error(_) => None - } - }) - - let tags = getOptionalArray(json, "tags")->Option.map(arr => - arr->Array.map(JSON.Decode.string)->Array.filterMap(x => x) - ) - - Ok({ - id, - timestamp, - version, - learner, - context, - experience, - privacy, - tags, - }) - } - | (Error(err), _, _) - | (_, Error(err), _) - | (_, _, Error(err)) => - Error(`LearningExperience decode error: ${err}`) - } -} - -// Decode array of experiences -let decodeExperiences = (jsonArray: array): result, string> => { - let experiences = [] - let errors = [] - - jsonArray->Array.forEachWithIndex((json, i) => { - switch decodeLearningExperience(json) { - | Ok(exp) => experiences->Array.push(exp)->ignore - | Error(err) => errors->Array.push(`Experience ${Int.toString(i)}: ${err}`)->ignore - } - }) - - if errors->Array.length > 0 { - Error(`Failed to decode ${Int.toString(errors->Array.length)} experiences:\n${errors->Array.join("\n")}`) - } else { - Ok(experiences) - } -} diff --git a/src-rescript/Decoder.res.js b/src-rescript/Decoder.res.js deleted file mode 100644 index 2fdb308..0000000 --- a/src-rescript/Decoder.res.js +++ /dev/null @@ -1,483 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as $$Crypto from "crypto"; -import * as Core__JSON from "@rescript/core/src/Core__JSON.res.js"; -import * as Core__Array from "@rescript/core/src/Core__Array.res.js"; -import * as Core__Option from "@rescript/core/src/Core__Option.res.js"; -import * as Core__Result from "@rescript/core/src/Core__Result.res.js"; - -function getString(json, field) { - var obj = Core__JSON.Decode.object(json); - if (obj === undefined) { - return { - TAG: "Error", - _0: "Expected JSON object" - }; - } - var value = obj[field]; - if (value === undefined) { - return { - TAG: "Error", - _0: "Missing field \"" + field + "\"" - }; - } - var str = Core__JSON.Decode.string(value); - if (str !== undefined) { - return { - TAG: "Ok", - _0: str - }; - } else { - return { - TAG: "Error", - _0: "Field \"" + field + "\" is not a string" - }; - } -} - -function getOptionalString(json, field) { - return Core__Option.flatMap(Core__Option.flatMap(Core__JSON.Decode.object(json), (function (obj) { - return obj[field]; - })), Core__JSON.Decode.string); -} - -function getArray(json, field) { - var obj = Core__JSON.Decode.object(json); - if (obj === undefined) { - return { - TAG: "Error", - _0: "Expected JSON object" - }; - } - var value = obj[field]; - if (value === undefined) { - return { - TAG: "Ok", - _0: [] - }; - } - var arr = Core__JSON.Decode.array(value); - if (arr !== undefined) { - return { - TAG: "Ok", - _0: arr - }; - } else { - return { - TAG: "Error", - _0: "Field \"" + field + "\" is not an array" - }; - } -} - -function getOptionalArray(json, field) { - return Core__Option.flatMap(Core__Option.flatMap(Core__JSON.Decode.object(json), (function (obj) { - return obj[field]; - })), Core__JSON.Decode.array); -} - -function getObject(json, field) { - var obj = Core__JSON.Decode.object(json); - if (obj === undefined) { - return { - TAG: "Error", - _0: "Expected JSON object" - }; - } - var value = obj[field]; - if (value !== undefined) { - return { - TAG: "Ok", - _0: value - }; - } else { - return { - TAG: "Error", - _0: "Missing field \"" + field + "\"" - }; - } -} - -function getOptionalObject(json, field) { - return Core__Option.flatMap(Core__JSON.Decode.object(json), (function (obj) { - return obj[field]; - })); -} - -function getNumber(json, field) { - var obj = Core__JSON.Decode.object(json); - if (obj === undefined) { - return { - TAG: "Error", - _0: "Expected JSON object" - }; - } - var value = obj[field]; - if (value === undefined) { - return { - TAG: "Error", - _0: "Missing field \"" + field + "\"" - }; - } - var num = Core__JSON.Decode.$$float(value); - if (num !== undefined) { - return { - TAG: "Ok", - _0: num - }; - } else { - return { - TAG: "Error", - _0: "Field \"" + field + "\" is not a number" - }; - } -} - -function getOptionalNumber(json, field) { - return Core__Option.flatMap(Core__Option.flatMap(Core__JSON.Decode.object(json), (function (obj) { - return obj[field]; - })), Core__JSON.Decode.$$float); -} - -function decodeCoordinates(json) { - var lat = getNumber(json, "latitude"); - var latResult; - latResult = lat.TAG === "Ok" ? ({ - TAG: "Ok", - _0: lat._0 - }) : getNumber(json, "lat"); - var lon = getNumber(json, "longitude"); - var lonResult; - lonResult = lon.TAG === "Ok" ? ({ - TAG: "Ok", - _0: lon._0 - }) : getNumber(json, "lon"); - if (latResult.TAG === "Ok") { - if (lonResult.TAG === "Ok") { - return { - TAG: "Ok", - _0: { - latitude: latResult._0, - longitude: lonResult._0 - } - }; - } else { - return { - TAG: "Error", - _0: "Coordinates decode error: " + lonResult._0 - }; - } - } else { - return { - TAG: "Error", - _0: "Coordinates decode error: " + latResult._0 - }; - } -} - -function decodeLocation(json) { - var err = getString(json, "name"); - if (err.TAG !== "Ok") { - return { - TAG: "Error", - _0: "Location decode error: " + err._0 - }; - } - var coordinates = Core__Option.flatMap(getOptionalObject(json, "coordinates"), (function (coordJson) { - var coords = decodeCoordinates(coordJson); - if (coords.TAG === "Ok") { - return coords._0; - } - - })); - var type_ = getOptionalString(json, "type"); - var address = getOptionalString(json, "address"); - return { - TAG: "Ok", - _0: { - name: err._0, - coordinates: coordinates, - type: type_, - address: address - } - }; -} - -function decodeLearner(json) { - var err = getString(json, "id"); - if (err.TAG !== "Ok") { - return { - TAG: "Error", - _0: "Learner decode error: " + err._0 - }; - } - var name = getOptionalString(json, "name"); - var interests = Core__Option.map(getOptionalArray(json, "interests"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - return { - TAG: "Ok", - _0: { - id: err._0, - name: name, - interests: interests - } - }; -} - -function decodeContext(json) { - var err = getObject(json, "location"); - if (err.TAG !== "Ok") { - return { - TAG: "Error", - _0: "Context decode error: " + err._0 - }; - } - var err$1 = decodeLocation(err._0); - if (err$1.TAG !== "Ok") { - return { - TAG: "Error", - _0: "Context.location decode error: " + err$1._0 - }; - } - var situation = getOptionalString(json, "situation"); - var connections = Core__Option.map(getOptionalArray(json, "connections"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - var timeOfDay = Core__Option.map(getOptionalString(json, "timeOfDay"), (function (str) { - switch (str) { - case "afternoon" : - return "afternoon"; - case "evening" : - return "evening"; - case "morning" : - return "morning"; - default: - return "night"; - } - })); - return { - TAG: "Ok", - _0: { - location: err$1._0, - situation: situation, - connections: connections, - timeOfDay: timeOfDay - } - }; -} - -function decodeOutcome(json) { - var success = Core__Option.flatMap(Core__Option.flatMap(Core__JSON.Decode.object(json), (function (obj) { - return obj["success"]; - })), Core__JSON.Decode.bool); - var connections_made = Core__Option.map(getOptionalArray(json, "connections_made"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - var next_questions = Core__Option.map(getOptionalArray(json, "next_questions"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - var artifacts = Core__Option.map(getOptionalArray(json, "artifacts"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - return { - TAG: "Ok", - _0: { - success: success, - connections_made: connections_made, - next_questions: next_questions, - artifacts: artifacts - } - }; -} - -function decodeExperienceData(json) { - var err = getString(json, "type"); - if (err.TAG !== "Ok") { - return { - TAG: "Error", - _0: "ExperienceData decode error: " + err._0 - }; - } - var err$1 = getString(json, "description"); - if (err$1.TAG !== "Ok") { - return { - TAG: "Error", - _0: "ExperienceData.description decode error: " + err$1._0 - }; - } - var domains = Core__Option.map(getOptionalArray(json, "domains"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - var outcome = Core__Option.flatMap(getOptionalObject(json, "outcome"), (function (outcomeJson) { - var out = decodeOutcome(outcomeJson); - if (out.TAG === "Ok") { - return out._0; - } - - })); - var duration = Core__Option.map(getOptionalNumber(json, "duration"), (function (prim) { - return prim | 0; - })); - var intensity = Core__Option.map(getOptionalString(json, "intensity"), (function (str) { - switch (str) { - case "low" : - return "low"; - case "medium" : - return "medium"; - default: - return "high"; - } - })); - return { - TAG: "Ok", - _0: { - type: err._0, - description: err$1._0, - domains: domains, - outcome: outcome, - duration: duration, - intensity: intensity - } - }; -} - -function decodePrivacy(json) { - var err = getString(json, "level"); - if (err.TAG !== "Ok") { - return { - TAG: "Error", - _0: "Privacy decode error: " + err._0 - }; - } - var level; - switch (err._0) { - case "anonymous" : - level = "anonymous"; - break; - case "private" : - level = "private"; - break; - default: - level = "public"; - } - var shareableWith = Core__Option.map(getOptionalArray(json, "shareableWith"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - return { - TAG: "Ok", - _0: { - level: level, - shareableWith: shareableWith - } - }; -} - -function decodeLearningExperience(json) { - var learnerResult = Core__Result.flatMap(getObject(json, "learner"), decodeLearner); - var contextResult = Core__Result.flatMap(getObject(json, "context"), decodeContext); - var experienceResult = Core__Result.flatMap(getObject(json, "experience"), decodeExperienceData); - var id = Core__Option.getOr(getOptionalString(json, "id"), "ubi-" + $$Crypto.randomUUID()); - var timestamp = Core__Option.getOr(getOptionalString(json, "timestamp"), Date.now().toString()); - var version = Core__Option.getOr(getOptionalString(json, "version"), "1.0"); - var err; - if (learnerResult.TAG === "Ok") { - if (contextResult.TAG === "Ok") { - if (experienceResult.TAG === "Ok") { - var privacy = Core__Option.flatMap(getOptionalObject(json, "privacy"), (function (privJson) { - var priv = decodePrivacy(privJson); - if (priv.TAG === "Ok") { - return priv._0; - } - - })); - var tags = Core__Option.map(getOptionalArray(json, "tags"), (function (arr) { - return Core__Array.filterMap(arr.map(Core__JSON.Decode.string), (function (x) { - return x; - })); - })); - return { - TAG: "Ok", - _0: { - id: id, - timestamp: timestamp, - learner: learnerResult._0, - context: contextResult._0, - experience: experienceResult._0, - privacy: privacy, - tags: tags, - version: version - } - }; - } - err = experienceResult._0; - } else { - err = contextResult._0; - } - } else { - err = learnerResult._0; - } - return { - TAG: "Error", - _0: "LearningExperience decode error: " + err - }; -} - -function decodeExperiences(jsonArray) { - var experiences = []; - var errors = []; - jsonArray.forEach(function (json, i) { - var exp = decodeLearningExperience(json); - if (exp.TAG === "Ok") { - experiences.push(exp._0); - return ; - } - errors.push("Experience " + i.toString() + ": " + exp._0); - }); - if (errors.length > 0) { - return { - TAG: "Error", - _0: "Failed to decode " + errors.length.toString() + " experiences:\n" + errors.join("\n") - }; - } else { - return { - TAG: "Ok", - _0: experiences - }; - } -} - -export { - getString , - getOptionalString , - getArray , - getOptionalArray , - getObject , - getOptionalObject , - getNumber , - getOptionalNumber , - decodeCoordinates , - decodeLocation , - decodeLearner , - decodeContext , - decodeOutcome , - decodeExperienceData , - decodePrivacy , - decodeLearningExperience , - decodeExperiences , -} -/* crypto Not a pure module */ diff --git a/src-rescript/Export.res b/src-rescript/Export.res deleted file mode 100644 index d9cdf83..0000000 --- a/src-rescript/Export.res +++ /dev/null @@ -1,330 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity Export Utilities -// Supports CSV, GeoJSON, DOT, Markdown, and JSON formats - -open UbiCity - -type exportFormat = CSV | GeoJSON | DOT | Markdown | JSON - -type geoJSONGeometry = { - @as("type") type_: string, - coordinates: array, -} - -type geoJSONProperties = { - name: string, - experiences: int, - domains: array, -} - -type geoJSONFeature = { - @as("type") type_: string, - geometry: geoJSONGeometry, - properties: geoJSONProperties, -} - -type geoJSONCollection = { - @as("type") type_: string, - features: array, -} - -// CSV Export -let exportToCSV = (experiences: array): string => { - let headers = [ - "id", - "timestamp", - "learner_id", - "location", - "type", - "description", - "domains", - "success", - "latitude", - "longitude", - ] - - let rows = [headers->Array.join(",")] - - experiences->Array.forEach(exp => { - let locationName = exp.context.location.name - let description = exp.experience.description->String.replaceAll("\"", "\"\"") - let domains = exp.experience.domains->Option.getOr([])->Array.join("; ") - let success = exp.experience.outcome->Option.flatMap(o => o.success)->Option.map(b => - b ? "true" : "false" - )->Option.getOr("") - let latitude = exp.context.location.coordinates->Option.map(c => - Float.toString(c.latitude) - )->Option.getOr("") - let longitude = exp.context.location.coordinates->Option.map(c => - Float.toString(c.longitude) - )->Option.getOr("") - - let row = [ - exp.id, - exp.timestamp, - exp.learner.id, - `"${locationName}"`, - exp.experience.type_, - `"${description}"`, - `"${domains}"`, - success, - latitude, - longitude, - ] - - rows->Array.push(row->Array.join(","))->ignore - }) - - rows->Array.join("\n") -} - -// GeoJSON Export -let exportToGeoJSON = (experiences: array): geoJSONCollection => { - // Group by location - let locationExps = experiences->Array.reduce(Dict.make(), (acc, exp) => { - let location = exp.context.location.name - let exps = acc->Dict.get(location)->Option.getOr([]) - acc->Dict.set(location, exps->Array.concat([exp])) - acc - }) - - let features = [] - - locationExps->Dict.toArray->Array.forEach(((name, exps)) => { - // Get first experience with coordinates - let coordsOpt = exps->Array.find(exp => exp.context.location.coordinates->Option.isSome) - - switch coordsOpt { - | Some(exp) => - switch exp.context.location.coordinates { - | Some(coords) => { - let domains = exps - ->Array.reduce([], (acc, e) => { - switch e.experience.domains { - | Some(doms) => Array.concat(acc, doms) - | None => acc - } - }) - ->Array.reduce([], (acc, d) => { - if acc->Array.includes(d) {acc} else {acc->Array.concat([d])} - }) - - let geometry: geoJSONGeometry = { - type_: "Point", - coordinates: [coords.longitude, coords.latitude], - } - - let properties: geoJSONProperties = { - name: name, - experiences: exps->Array.length, - domains: domains, - } - - let feature: geoJSONFeature = { - type_: "Feature", - geometry: geometry, - properties: properties, - } - - features->Array.push(feature)->ignore - } - | None => () - } - | None => () - } - }) - - { - type_: "FeatureCollection", - features: features, - } -} - -// DOT (GraphViz) Export for domain network -let exportToDOT = (experiences: array): string => { - // Build domain co-occurrence network - let domainCounts = Dict.make() - let coOccurrences = Dict.make() - - experiences->Array.forEach(exp => { - switch exp.experience.domains { - | Some(domains) => { - // Count domain occurrences - domains->Array.forEach(d => { - let count = domainCounts->Dict.get(d)->Option.getOr(0) - domainCounts->Dict.set(d, count + 1) - }) - - // Count co-occurrences - domains->Array.forEach(d1 => { - domains->Array.forEach(d2 => { - if d1 < d2 { - let key = `${d1}--${d2}` - let count = coOccurrences->Dict.get(key)->Option.getOr(0) - coOccurrences->Dict.set(key, count + 1) - } - }) - }) - } - | None => () - } - }) - - let lines = ["graph DomainNetwork {", " layout=neato;", " overlap=false;", ""] - - // Nodes - domainCounts->Dict.toArray->Array.forEach(((domain, count)) => { - let size = Math.max(0.5, Math.min(3.0, Int.toFloat(count) /. 5.0))->Float.toString - lines->Array.push(` "${domain}" [width=${size}];`)->ignore - }) - - lines->Array.push("")->ignore - - // Edges - coOccurrences->Dict.toArray->Array.forEach(((key, weight)) => { - let parts = key->String.split("--") - let source = parts[0]->Option.getOr("") - let target = parts[1]->Option.getOr("") - let penwidth = Math.max(1.0, Math.min(5.0, Int.toFloat(weight)))->Float.toString - lines->Array.push(` "${source}" -- "${target}" [penwidth=${penwidth}];`)->ignore - }) - - lines->Array.push("}")->ignore - - lines->Array.join("\n") -} - -// Markdown Export for learner journeys -let exportJourneysToMarkdown = (experiences: array): string => { - let lines = ["# UbiCity Learner Journeys", ""] - - // Group by learner - let learnerExps = experiences->Array.reduce(Dict.make(), (acc, exp) => { - let learnerId = exp.learner.id - let exps = acc->Dict.get(learnerId)->Option.getOr([]) - acc->Dict.set(learnerId, exps->Array.concat([exp])) - acc - }) - - learnerExps->Dict.toArray->Array.forEach(((learnerId, exps)) => { - lines->Array.push(`## Learner: ${learnerId}`)->ignore - lines->Array.push("")->ignore - lines->Array.push(`**Total Experiences:** ${Int.toString(exps->Array.length)}`)->ignore - lines->Array.push("")->ignore - - // Sort by timestamp - let sorted = exps->Array.toSorted((a, b) => { - a.timestamp < b.timestamp ? -1.0 : 1.0 - }) - - lines->Array.push("### Timeline")->ignore - lines->Array.push("")->ignore - - sorted->Array.forEachWithIndex((exp, i) => { - let date = Date.fromString(exp.timestamp)->Date.toLocaleDateString - let location = exp.context.location.name - let type_ = exp.experience.type_ - let description = exp.experience.description - - lines->Array.push(`${Int.toString(i + 1)}. **${date}** - ${location} (${type_})`)->ignore - - switch exp.experience.domains { - | Some(domains) if domains->Array.length > 0 => - lines->Array.push(` - Domains: ${domains->Array.join(", ")}`)->ignore - | _ => () - } - - lines->Array.push(` - ${description}`)->ignore - lines->Array.push("")->ignore - }) - - // Questions emerged - let questions = sorted->Array.reduce([], (acc, exp) => { - switch exp.experience.outcome { - | Some(outcome) => - switch outcome.next_questions { - | Some(qs) => Array.concat(acc, qs) - | None => acc - } - | None => acc - } - }) - - if questions->Array.length > 0 { - lines->Array.push("### Questions Emerged")->ignore - lines->Array.push("")->ignore - questions->Array.forEach(q => { - lines->Array.push(`- ${q}`)->ignore - }) - lines->Array.push("")->ignore - } - - lines->Array.push("---")->ignore - lines->Array.push("")->ignore - }) - - lines->Array.join("\n") -} - -// JSON Export -let exportToJSON = (experiences: array): string => { - // Convert to JS objects for serialization - let jsObjects = experiences->Array.map(exp => { - { - "id": exp.id, - "timestamp": exp.timestamp, - "version": exp.version, - "learner": { - "id": exp.learner.id, - "name": exp.learner.name, - "interests": exp.learner.interests, - }, - "context": { - "location": { - "name": exp.context.location.name, - "coordinates": exp.context.location.coordinates, - "type": exp.context.location.type_, - "address": exp.context.location.address, - }, - "situation": exp.context.situation, - "connections": exp.context.connections, - "timeOfDay": exp.context.timeOfDay, - }, - "experience": { - "type": exp.experience.type_, - "description": exp.experience.description, - "domains": exp.experience.domains, - "outcome": exp.experience.outcome, - "duration": exp.experience.duration, - "intensity": exp.experience.intensity, - }, - "privacy": exp.privacy, - "tags": exp.tags, - } - }) - - switch JSON.stringifyAny(jsObjects) { - | Some(json) => json - | None => "[]" - } -} - -// Main export dispatcher -let exportData = ( - experiences: array, - format: exportFormat, -): string => { - switch format { - | CSV => exportToCSV(experiences) - | GeoJSON => { - let geoJSON = exportToGeoJSON(experiences) - switch JSON.stringifyAny(geoJSON) { - | Some(json) => json - | None => "{\"type\":\"FeatureCollection\",\"features\":[]}" - } - } - | DOT => exportToDOT(experiences) - | Markdown => exportJourneysToMarkdown(experiences) - | JSON => exportToJSON(experiences) - } -} diff --git a/src-rescript/Export.res.js b/src-rescript/Export.res.js deleted file mode 100644 index 8982d17..0000000 --- a/src-rescript/Export.res.js +++ /dev/null @@ -1,301 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Core__Array from "@rescript/core/src/Core__Array.res.js"; -import * as Core__Option from "@rescript/core/src/Core__Option.res.js"; - -function exportToCSV(experiences) { - var headers = [ - "id", - "timestamp", - "learner_id", - "location", - "type", - "description", - "domains", - "success", - "latitude", - "longitude" - ]; - var rows = [headers.join(",")]; - experiences.forEach(function (exp) { - var locationName = exp.context.location.name; - var description = exp.experience.description.replaceAll("\"", "\"\""); - var domains = Core__Option.getOr(exp.experience.domains, []).join("; "); - var success = Core__Option.getOr(Core__Option.map(Core__Option.flatMap(exp.experience.outcome, (function (o) { - return o.success; - })), (function (b) { - if (b) { - return "true"; - } else { - return "false"; - } - })), ""); - var latitude = Core__Option.getOr(Core__Option.map(exp.context.location.coordinates, (function (c) { - return c.latitude.toString(); - })), ""); - var longitude = Core__Option.getOr(Core__Option.map(exp.context.location.coordinates, (function (c) { - return c.longitude.toString(); - })), ""); - var row = [ - exp.id, - exp.timestamp, - exp.learner.id, - "\"" + locationName + "\"", - exp.experience.type, - "\"" + description + "\"", - "\"" + domains + "\"", - success, - latitude, - longitude - ]; - rows.push(row.join(",")); - }); - return rows.join("\n"); -} - -function exportToGeoJSON(experiences) { - var locationExps = Core__Array.reduce(experiences, {}, (function (acc, exp) { - var $$location = exp.context.location.name; - var exps = Core__Option.getOr(acc[$$location], []); - acc[$$location] = exps.concat([exp]); - return acc; - })); - var features = []; - Object.entries(locationExps).forEach(function (param) { - var exps = param[1]; - var coordsOpt = exps.find(function (exp) { - return Core__Option.isSome(exp.context.location.coordinates); - }); - if (coordsOpt === undefined) { - return ; - } - var coords = coordsOpt.context.location.coordinates; - if (coords === undefined) { - return ; - } - var domains = Core__Array.reduce(Core__Array.reduce(exps, [], (function (acc, e) { - var doms = e.experience.domains; - if (doms !== undefined) { - return acc.concat(doms); - } else { - return acc; - } - })), [], (function (acc, d) { - if (acc.includes(d)) { - return acc; - } else { - return acc.concat([d]); - } - })); - var geometry_coordinates = [ - coords.longitude, - coords.latitude - ]; - var geometry = { - type: "Point", - coordinates: geometry_coordinates - }; - var properties_name = param[0]; - var properties_experiences = exps.length; - var properties = { - name: properties_name, - experiences: properties_experiences, - domains: domains - }; - var feature = { - type: "Feature", - geometry: geometry, - properties: properties - }; - features.push(feature); - }); - return { - type: "FeatureCollection", - features: features - }; -} - -function exportToDOT(experiences) { - var domainCounts = {}; - var coOccurrences = {}; - experiences.forEach(function (exp) { - var domains = exp.experience.domains; - if (domains !== undefined) { - domains.forEach(function (d) { - var count = Core__Option.getOr(domainCounts[d], 0); - domainCounts[d] = count + 1 | 0; - }); - domains.forEach(function (d1) { - domains.forEach(function (d2) { - if (d1 >= d2) { - return ; - } - var key = d1 + "--" + d2; - var count = Core__Option.getOr(coOccurrences[key], 0); - coOccurrences[key] = count + 1 | 0; - }); - }); - return ; - } - - }); - var lines = [ - "graph DomainNetwork {", - " layout=neato;", - " overlap=false;", - "" - ]; - Object.entries(domainCounts).forEach(function (param) { - var size = Math.max(0.5, Math.min(3.0, param[1] / 5.0)).toString(); - lines.push(" \"" + param[0] + "\" [width=" + size + "];"); - }); - lines.push(""); - Object.entries(coOccurrences).forEach(function (param) { - var parts = param[0].split("--"); - var source = Core__Option.getOr(parts[0], ""); - var target = Core__Option.getOr(parts[1], ""); - var penwidth = Math.max(1.0, Math.min(5.0, param[1])).toString(); - lines.push(" \"" + source + "\" -- \"" + target + "\" [penwidth=" + penwidth + "];"); - }); - lines.push("}"); - return lines.join("\n"); -} - -function exportJourneysToMarkdown(experiences) { - var lines = [ - "# UbiCity Learner Journeys", - "" - ]; - var learnerExps = Core__Array.reduce(experiences, {}, (function (acc, exp) { - var learnerId = exp.learner.id; - var exps = Core__Option.getOr(acc[learnerId], []); - acc[learnerId] = exps.concat([exp]); - return acc; - })); - Object.entries(learnerExps).forEach(function (param) { - var exps = param[1]; - lines.push("## Learner: " + param[0]); - lines.push(""); - lines.push("**Total Experiences:** " + exps.length.toString()); - lines.push(""); - var sorted = exps.toSorted(function (a, b) { - if (a.timestamp < b.timestamp) { - return -1.0; - } else { - return 1.0; - } - }); - lines.push("### Timeline"); - lines.push(""); - sorted.forEach(function (exp, i) { - var date = new Date(exp.timestamp).toLocaleDateString(); - var $$location = exp.context.location.name; - var type_ = exp.experience.type; - var description = exp.experience.description; - lines.push((i + 1 | 0).toString() + ". **" + date + "** - " + $$location + " (" + type_ + ")"); - var domains = exp.experience.domains; - if (domains !== undefined && domains.length > 0) { - lines.push(" - Domains: " + domains.join(", ")); - } - lines.push(" - " + description); - lines.push(""); - }); - var questions = Core__Array.reduce(sorted, [], (function (acc, exp) { - var outcome = exp.experience.outcome; - if (outcome === undefined) { - return acc; - } - var qs = outcome.next_questions; - if (qs !== undefined) { - return acc.concat(qs); - } else { - return acc; - } - })); - if (questions.length > 0) { - lines.push("### Questions Emerged"); - lines.push(""); - questions.forEach(function (q) { - lines.push("- " + q); - }); - lines.push(""); - } - lines.push("---"); - lines.push(""); - }); - return lines.join("\n"); -} - -function exportToJSON(experiences) { - var jsObjects = experiences.map(function (exp) { - return { - id: exp.id, - timestamp: exp.timestamp, - version: exp.version, - learner: { - id: exp.learner.id, - name: exp.learner.name, - interests: exp.learner.interests - }, - context: { - location: { - name: exp.context.location.name, - coordinates: exp.context.location.coordinates, - type: exp.context.location.type, - address: exp.context.location.address - }, - situation: exp.context.situation, - connections: exp.context.connections, - timeOfDay: exp.context.timeOfDay - }, - experience: { - type: exp.experience.type, - description: exp.experience.description, - domains: exp.experience.domains, - outcome: exp.experience.outcome, - duration: exp.experience.duration, - intensity: exp.experience.intensity - }, - privacy: exp.privacy, - tags: exp.tags - }; - }); - var json = JSON.stringify(jsObjects); - if (json !== undefined) { - return json; - } else { - return "[]"; - } -} - -function exportData(experiences, format) { - switch (format) { - case "CSV" : - return exportToCSV(experiences); - case "GeoJSON" : - var geoJSON = exportToGeoJSON(experiences); - var json = JSON.stringify(geoJSON); - if (json !== undefined) { - return json; - } else { - return "{\"type\":\"FeatureCollection\",\"features\":[]}"; - } - case "DOT" : - return exportToDOT(experiences); - case "Markdown" : - return exportJourneysToMarkdown(experiences); - case "JSON" : - return exportToJSON(experiences); - - } -} - -export { - exportToCSV , - exportToGeoJSON , - exportToDOT , - exportJourneysToMarkdown , - exportToJSON , - exportData , -} -/* No side effect */ diff --git a/src-rescript/Mapper.res b/src-rescript/Mapper.res deleted file mode 100644 index 523f4cc..0000000 --- a/src-rescript/Mapper.res +++ /dev/null @@ -1,443 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity Urban Knowledge Mapper (ReScript) -// Maps learning experiences across urban space - -open UbiCity - -module Storage = { - type t - - @module("./storage.js") - @new - external make: string => t = "ExperienceStorage" - - @send - external ensureDirectories: t => promise = "ensureDirectories" - - @send - external saveExperience: (t, 'a) => promise = "saveExperience" - - @send - external loadAllExperiences: t => promise> = "loadAllExperiences" - - @send - external saveVisualization: (t, string, string) => promise = "saveVisualization" -} - -type indices = { - location: Dict.t>, - domain: Dict.t>, - learner: Dict.t>, -} - -type t = { - storage: Storage.t, - experiences: Dict.t, - indices: indices, -} - -let make = (~storageDir="./ubicity-data", ()): promise => { - let storage = Storage.make(storageDir) - let mapper = { - storage, - experiences: Dict.make(), - indices: { - location: Dict.make(), - domain: Dict.make(), - learner: Dict.make(), - }, - } - - storage - ->Storage.ensureDirectories - ->Promise.then(() => Promise.resolve(mapper)) - ->Promise.catch(_ => Promise.resolve(mapper)) -} - -let addToIndex = (index: Dict.t>, key: string, id: string): Dict.t> => { - let existing = index->Dict.get(key)->Option.getOr([]) - if existing->Array.includes(id) { - index - } else { - index->Dict.set(key, existing->Array.concat([id])) - index - } -} - -let updateIndices = (mapper: t, experience: LearningExperience.t): t => { - let id = experience.id - - // Location index - let locationIndex = addToIndex( - mapper.indices.location, - experience.context.location.name, - id - ) - - // Domain index - let domainIndex = switch experience.experience.domains { - | Some(domains) => - domains->Array.reduce(mapper.indices.domain, (acc, domain) => { - addToIndex(acc, domain, id) - }) - | None => mapper.indices.domain - } - - // Learner index - let learnerIndex = addToIndex( - mapper.indices.learner, - experience.learner.id, - id - ) - - { - ...mapper, - indices: { - location: locationIndex, - domain: domainIndex, - learner: learnerIndex, - }, - } -} - -let captureExperience = ( - mapper: t, - experience: LearningExperience.t, -): promise> => { - mapper.experiences->Dict.set(experience.id, experience) - - let updated = updateIndices(mapper, experience) - - // Convert experience to JS object for storage - let experienceJs = { - "id": experience.id, - "timestamp": experience.timestamp, - "version": experience.version, - "learner": { - "id": experience.learner.id, - "name": experience.learner.name, - "interests": experience.learner.interests, - }, - "context": { - "location": { - "name": experience.context.location.name, - "coordinates": experience.context.location.coordinates, - "type": experience.context.location.type_, - "address": experience.context.location.address, - }, - "situation": experience.context.situation, - "connections": experience.context.connections, - "timeOfDay": experience.context.timeOfDay, - }, - "experience": { - "type": experience.experience.type_, - "description": experience.experience.description, - "domains": experience.experience.domains, - "outcome": experience.experience.outcome, - "duration": experience.experience.duration, - "intensity": experience.experience.intensity, - }, - "privacy": experience.privacy, - "tags": experience.tags, - } - - updated.storage - ->Storage.saveExperience(experienceJs) - ->Promise.then(() => Promise.resolve(Ok(experience.id))) - ->Promise.catch(_ => Promise.resolve(Error("Failed to save experience"))) -} - -let getExperienceCount = (mapper: t): int => { - mapper.experiences->Dict.keysToArray->Array.length -} - -let loadAll = (mapper: t): promise> => { - mapper.storage - ->Storage.loadAllExperiences - ->Promise.then(experiencesData => { - // Decode JS objects to ReScript types - switch Decoder.decodeExperiences(experiencesData) { - | Error(err) => Promise.resolve(Error(`Decode failed: ${err}`)) - | Ok(experiences) => { - // Load experiences into mapper - experiences->Array.forEach(exp => { - mapper.experiences->Dict.set(exp.id, exp) - }) - - // Update indices - let updated = experiences->Array.reduce(mapper, (m, exp) => updateIndices(m, exp)) - - // Copy indices back to mapper (since we created a new record with updated) - // Dict doesn't have clear(), so we just copy all entries from updated - updated.indices.location - ->Dict.toArray - ->Array.forEach(((k, v)) => mapper.indices.location->Dict.set(k, v)) - - updated.indices.domain - ->Dict.toArray - ->Array.forEach(((k, v)) => mapper.indices.domain->Dict.set(k, v)) - - updated.indices.learner - ->Dict.toArray - ->Array.forEach(((k, v)) => mapper.indices.learner->Dict.set(k, v)) - - Promise.resolve(Ok(experiences->Array.length)) - } - } - }) - ->Promise.catch(_ => Promise.resolve(Error("Failed to load experiences"))) -} - -// Analysis functions using UbiCity.Analysis -let findInterdisciplinary = (mapper: t): array => { - let experiences = mapper.experiences->Dict.valuesToArray - Analysis.findInterdisciplinary(experiences) -} - -let groupByLocation = (mapper: t): Dict.t> => { - let experiences = mapper.experiences->Dict.valuesToArray - Analysis.groupByLocation(experiences) -} - -let groupByLearner = (mapper: t): Dict.t> => { - let experiences = mapper.experiences->Dict.valuesToArray - Analysis.groupByLearner(experiences) -} - -let calculateDiversity = (mapper: t): int => { - let experiences = mapper.experiences->Dict.valuesToArray - Analysis.calculateDiversity(experiences) -} - -// Find experiences by location -let findByLocation = (mapper: t, locationName: string): array => { - switch mapper.indices.location->Dict.get(locationName) { - | Some(ids) => - ids - ->Array.map(id => mapper.experiences->Dict.get(id)) - ->Array.filterMap(x => x) - | None => [] - } -} - -// Find experiences by domain -let findByDomain = (mapper: t, domain: string): array => { - switch mapper.indices.domain->Dict.get(domain) { - | Some(ids) => - ids - ->Array.map(id => mapper.experiences->Dict.get(id)) - ->Array.filterMap(x => x) - | None => [] - } -} - -// Find experiences by learner -let findByLearner = (mapper: t, learnerId: string): array => { - switch mapper.indices.learner->Dict.get(learnerId) { - | Some(ids) => - ids - ->Array.map(id => mapper.experiences->Dict.get(id)) - ->Array.filterMap(x => x) - | None => [] - } -} - -// Get hotspot locations (most experiences) -let getHotspots = (mapper: t, ~limit=10, ()): array<(string, int)> => { - mapper.indices.location - ->Dict.toArray - ->Array.map(((location, ids)) => (location, ids->Array.length)) - ->Array.toSorted((a, b) => { - let (_, countA) = a - let (_, countB) = b - Float.fromInt(countB - countA) - }) - ->Array.slice(~start=0, ~end=limit) -} - -// Get top domains -let getTopDomains = (mapper: t, ~limit=10, ()): array<(string, int)> => { - mapper.indices.domain - ->Dict.toArray - ->Array.map(((domain, ids)) => (domain, ids->Array.length)) - ->Array.toSorted((a, b) => { - let (_, countA) = a - let (_, countB) = b - Float.fromInt(countB - countA) - }) - ->Array.slice(~start=0, ~end=limit) -} - -// Types for report generation -type reportSummary = { - totalExperiences: int, - uniqueLocations: int, - uniqueDomains: int, - uniqueLearners: int, - interdisciplinaryExperiences: int, -} - -type learningHotspot = { - location: string, - count: int, - diversity: int, - learners: int, - domains: array, -} - -type domainNode = { - id: string, - size: int, -} - -type domainEdge = { - source: string, - target: string, - weight: int, -} - -type domainNetwork = { - nodes: array, - edges: array, -} - -type locationData = { - coordinates: option, - count: int, - domains: array, -} - -type report = { - summary: reportSummary, - learningHotspots: array, -} - -// Generate comprehensive report -let generateReport = (mapper: t): promise> => { - let experiences = mapper.experiences->Dict.valuesToArray - - let summary: reportSummary = { - totalExperiences: experiences->Array.length, - uniqueLocations: mapper.indices.location->Dict.keysToArray->Array.length, - uniqueDomains: mapper.indices.domain->Dict.keysToArray->Array.length, - uniqueLearners: mapper.indices.learner->Dict.keysToArray->Array.length, - interdisciplinaryExperiences: Analysis.findInterdisciplinary(experiences)->Array.length, - } - - let learningHotspots = mapper.indices.location - ->Dict.toArray - ->Array.map(((location, experienceIds)) => { - let locationExps = experienceIds - ->Array.map(id => mapper.experiences->Dict.get(id)) - ->Array.filterMap(x => x) - - let domains = locationExps - ->Array.reduce([], (acc, exp) => { - switch exp.experience.domains { - | Some(doms) => Array.concat(acc, doms) - | None => acc - } - }) - ->Array.reduce([], (acc, d) => { - if acc->Array.includes(d) {acc} else {acc->Array.concat([d])} - }) - - let learners = locationExps - ->Array.map(exp => exp.learner.id) - ->Array.reduce([], (acc, id) => { - if acc->Array.includes(id) {acc} else {acc->Array.concat([id])} - }) - - { - location, - count: experienceIds->Array.length, - diversity: domains->Array.length, - learners: learners->Array.length, - domains, - } - }) - ->Array.toSorted((a, b) => Float.fromInt(b.diversity - a.diversity)) - - Promise.resolve(Ok({summary, learningHotspots})) -} - -// Map by location -let mapByLocation = (mapper: t): Dict.t => { - let result = Dict.make() - - mapper.indices.location->Dict.toArray->Array.forEach(((location, experienceIds)) => { - let locationExps = experienceIds - ->Array.map(id => mapper.experiences->Dict.get(id)) - ->Array.filterMap(x => x) - - let coordinates = locationExps - ->Array.find(exp => exp.context.location.coordinates->Option.isSome) - ->Option.flatMap(exp => exp.context.location.coordinates) - - let domains = locationExps - ->Array.reduce([], (acc, exp) => { - switch exp.experience.domains { - | Some(doms) => Array.concat(acc, doms) - | None => acc - } - }) - ->Array.reduce([], (acc, d) => { - if acc->Array.includes(d) {acc} else {acc->Array.concat([d])} - }) - - result->Dict.set(location, { - coordinates, - count: experienceIds->Array.length, - domains, - }) - }) - - result -} - -// Generate domain network -let generateDomainNetwork = (mapper: t): domainNetwork => { - let experiences = mapper.experiences->Dict.valuesToArray - - let domainCounts = Dict.make() - let coOccurrences = Dict.make() - - experiences->Array.forEach(exp => { - switch exp.experience.domains { - | Some(domains) => { - domains->Array.forEach(d => { - let count = domainCounts->Dict.get(d)->Option.getOr(0) - domainCounts->Dict.set(d, count + 1) - }) - - domains->Array.forEach(d1 => { - domains->Array.forEach(d2 => { - if d1 < d2 { - let key = `${d1}--${d2}` - let count = coOccurrences->Dict.get(key)->Option.getOr(0) - coOccurrences->Dict.set(key, count + 1) - } - }) - }) - } - | None => () - } - }) - - let nodes = domainCounts - ->Dict.toArray - ->Array.map(((id, size)) => {id, size}) - - let edges = coOccurrences - ->Dict.toArray - ->Array.map(((key, weight)) => { - let parts = key->String.split("--") - { - source: parts[0]->Option.getOr(""), - target: parts[1]->Option.getOr(""), - weight, - } - }) - - {nodes, edges} -} diff --git a/src-rescript/Mapper.res.js b/src-rescript/Mapper.res.js deleted file mode 100644 index 6723150..0000000 --- a/src-rescript/Mapper.res.js +++ /dev/null @@ -1,405 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Decoder from "./Decoder.res.js"; -import * as UbiCity from "./UbiCity.res.js"; -import * as Core__Array from "@rescript/core/src/Core__Array.res.js"; -import * as StorageJs from "./storage.js"; -import * as Core__Option from "@rescript/core/src/Core__Option.res.js"; -import * as Core__Promise from "@rescript/core/src/Core__Promise.res.js"; - -function make(prim) { - return new StorageJs.ExperienceStorage(prim); -} - -var $$Storage = { - make: make -}; - -function make$1(storageDirOpt, param) { - var storageDir = storageDirOpt !== undefined ? storageDirOpt : "./ubicity-data"; - var storage = new StorageJs.ExperienceStorage(storageDir); - var mapper_experiences = {}; - var mapper_indices = { - location: {}, - domain: {}, - learner: {} - }; - var mapper = { - storage: storage, - experiences: mapper_experiences, - indices: mapper_indices - }; - return Core__Promise.$$catch(storage.ensureDirectories().then(function () { - return Promise.resolve(mapper); - }), (function (param) { - return Promise.resolve(mapper); - })); -} - -function addToIndex(index, key, id) { - var existing = Core__Option.getOr(index[key], []); - if (existing.includes(id)) { - return index; - } else { - index[key] = existing.concat([id]); - return index; - } -} - -function updateIndices(mapper, experience) { - var id = experience.id; - var locationIndex = addToIndex(mapper.indices.location, experience.context.location.name, id); - var domains = experience.experience.domains; - var domainIndex = domains !== undefined ? Core__Array.reduce(domains, mapper.indices.domain, (function (acc, domain) { - return addToIndex(acc, domain, id); - })) : mapper.indices.domain; - var learnerIndex = addToIndex(mapper.indices.learner, experience.learner.id, id); - return { - storage: mapper.storage, - experiences: mapper.experiences, - indices: { - location: locationIndex, - domain: domainIndex, - learner: learnerIndex - } - }; -} - -function captureExperience(mapper, experience) { - mapper.experiences[experience.id] = experience; - var updated = updateIndices(mapper, experience); - var experienceJs = { - id: experience.id, - timestamp: experience.timestamp, - version: experience.version, - learner: { - id: experience.learner.id, - name: experience.learner.name, - interests: experience.learner.interests - }, - context: { - location: { - name: experience.context.location.name, - coordinates: experience.context.location.coordinates, - type: experience.context.location.type, - address: experience.context.location.address - }, - situation: experience.context.situation, - connections: experience.context.connections, - timeOfDay: experience.context.timeOfDay - }, - experience: { - type: experience.experience.type, - description: experience.experience.description, - domains: experience.experience.domains, - outcome: experience.experience.outcome, - duration: experience.experience.duration, - intensity: experience.experience.intensity - }, - privacy: experience.privacy, - tags: experience.tags - }; - return Core__Promise.$$catch(updated.storage.saveExperience(experienceJs).then(function () { - return Promise.resolve({ - TAG: "Ok", - _0: experience.id - }); - }), (function (param) { - return Promise.resolve({ - TAG: "Error", - _0: "Failed to save experience" - }); - })); -} - -function getExperienceCount(mapper) { - return Object.keys(mapper.experiences).length; -} - -function loadAll(mapper) { - return Core__Promise.$$catch(mapper.storage.loadAllExperiences().then(function (experiencesData) { - var err = Decoder.decodeExperiences(experiencesData); - if (err.TAG !== "Ok") { - return Promise.resolve({ - TAG: "Error", - _0: "Decode failed: " + err._0 - }); - } - var experiences = err._0; - experiences.forEach(function (exp) { - mapper.experiences[exp.id] = exp; - }); - var updated = Core__Array.reduce(experiences, mapper, (function (m, exp) { - return updateIndices(m, exp); - })); - Object.entries(updated.indices.location).forEach(function (param) { - mapper.indices.location[param[0]] = param[1]; - }); - Object.entries(updated.indices.domain).forEach(function (param) { - mapper.indices.domain[param[0]] = param[1]; - }); - Object.entries(updated.indices.learner).forEach(function (param) { - mapper.indices.learner[param[0]] = param[1]; - }); - return Promise.resolve({ - TAG: "Ok", - _0: experiences.length - }); - }), (function (param) { - return Promise.resolve({ - TAG: "Error", - _0: "Failed to load experiences" - }); - })); -} - -function findInterdisciplinary(mapper) { - var experiences = Object.values(mapper.experiences); - return UbiCity.Analysis.findInterdisciplinary(experiences); -} - -function groupByLocation(mapper) { - var experiences = Object.values(mapper.experiences); - return UbiCity.Analysis.groupByLocation(experiences); -} - -function groupByLearner(mapper) { - var experiences = Object.values(mapper.experiences); - return UbiCity.Analysis.groupByLearner(experiences); -} - -function calculateDiversity(mapper) { - var experiences = Object.values(mapper.experiences); - return UbiCity.Analysis.calculateDiversity(experiences); -} - -function findByLocation(mapper, locationName) { - var ids = mapper.indices.location[locationName]; - if (ids !== undefined) { - return Core__Array.filterMap(ids.map(function (id) { - return mapper.experiences[id]; - }), (function (x) { - return x; - })); - } else { - return []; - } -} - -function findByDomain(mapper, domain) { - var ids = mapper.indices.domain[domain]; - if (ids !== undefined) { - return Core__Array.filterMap(ids.map(function (id) { - return mapper.experiences[id]; - }), (function (x) { - return x; - })); - } else { - return []; - } -} - -function findByLearner(mapper, learnerId) { - var ids = mapper.indices.learner[learnerId]; - if (ids !== undefined) { - return Core__Array.filterMap(ids.map(function (id) { - return mapper.experiences[id]; - }), (function (x) { - return x; - })); - } else { - return []; - } -} - -function getHotspots(mapper, limitOpt, param) { - var limit = limitOpt !== undefined ? limitOpt : 10; - return Object.entries(mapper.indices.location).map(function (param) { - return [ - param[0], - param[1].length - ]; - }).toSorted(function (a, b) { - return b[1] - a[1] | 0; - }).slice(0, limit); -} - -function getTopDomains(mapper, limitOpt, param) { - var limit = limitOpt !== undefined ? limitOpt : 10; - return Object.entries(mapper.indices.domain).map(function (param) { - return [ - param[0], - param[1].length - ]; - }).toSorted(function (a, b) { - return b[1] - a[1] | 0; - }).slice(0, limit); -} - -function generateReport(mapper) { - var experiences = Object.values(mapper.experiences); - var summary_totalExperiences = experiences.length; - var summary_uniqueLocations = Object.keys(mapper.indices.location).length; - var summary_uniqueDomains = Object.keys(mapper.indices.domain).length; - var summary_uniqueLearners = Object.keys(mapper.indices.learner).length; - var summary_interdisciplinaryExperiences = UbiCity.Analysis.findInterdisciplinary(experiences).length; - var summary = { - totalExperiences: summary_totalExperiences, - uniqueLocations: summary_uniqueLocations, - uniqueDomains: summary_uniqueDomains, - uniqueLearners: summary_uniqueLearners, - interdisciplinaryExperiences: summary_interdisciplinaryExperiences - }; - var learningHotspots = Object.entries(mapper.indices.location).map(function (param) { - var experienceIds = param[1]; - var locationExps = Core__Array.filterMap(experienceIds.map(function (id) { - return mapper.experiences[id]; - }), (function (x) { - return x; - })); - var domains = Core__Array.reduce(Core__Array.reduce(locationExps, [], (function (acc, exp) { - var doms = exp.experience.domains; - if (doms !== undefined) { - return acc.concat(doms); - } else { - return acc; - } - })), [], (function (acc, d) { - if (acc.includes(d)) { - return acc; - } else { - return acc.concat([d]); - } - })); - var learners = Core__Array.reduce(locationExps.map(function (exp) { - return exp.learner.id; - }), [], (function (acc, id) { - if (acc.includes(id)) { - return acc; - } else { - return acc.concat([id]); - } - })); - return { - location: param[0], - count: experienceIds.length, - diversity: domains.length, - learners: learners.length, - domains: domains - }; - }).toSorted(function (a, b) { - return b.diversity - a.diversity | 0; - }); - return Promise.resolve({ - TAG: "Ok", - _0: { - summary: summary, - learningHotspots: learningHotspots - } - }); -} - -function mapByLocation(mapper) { - var result = {}; - Object.entries(mapper.indices.location).forEach(function (param) { - var experienceIds = param[1]; - var locationExps = Core__Array.filterMap(experienceIds.map(function (id) { - return mapper.experiences[id]; - }), (function (x) { - return x; - })); - var coordinates = Core__Option.flatMap(locationExps.find(function (exp) { - return Core__Option.isSome(exp.context.location.coordinates); - }), (function (exp) { - return exp.context.location.coordinates; - })); - var domains = Core__Array.reduce(Core__Array.reduce(locationExps, [], (function (acc, exp) { - var doms = exp.experience.domains; - if (doms !== undefined) { - return acc.concat(doms); - } else { - return acc; - } - })), [], (function (acc, d) { - if (acc.includes(d)) { - return acc; - } else { - return acc.concat([d]); - } - })); - result[param[0]] = { - coordinates: coordinates, - count: experienceIds.length, - domains: domains - }; - }); - return result; -} - -function generateDomainNetwork(mapper) { - var experiences = Object.values(mapper.experiences); - var domainCounts = {}; - var coOccurrences = {}; - experiences.forEach(function (exp) { - var domains = exp.experience.domains; - if (domains !== undefined) { - domains.forEach(function (d) { - var count = Core__Option.getOr(domainCounts[d], 0); - domainCounts[d] = count + 1 | 0; - }); - domains.forEach(function (d1) { - domains.forEach(function (d2) { - if (d1 >= d2) { - return ; - } - var key = d1 + "--" + d2; - var count = Core__Option.getOr(coOccurrences[key], 0); - coOccurrences[key] = count + 1 | 0; - }); - }); - return ; - } - - }); - var nodes = Object.entries(domainCounts).map(function (param) { - return { - id: param[0], - size: param[1] - }; - }); - var edges = Object.entries(coOccurrences).map(function (param) { - var parts = param[0].split("--"); - return { - source: Core__Option.getOr(parts[0], ""), - target: Core__Option.getOr(parts[1], ""), - weight: param[1] - }; - }); - return { - nodes: nodes, - edges: edges - }; -} - -export { - $$Storage , - make$1 as make, - addToIndex , - updateIndices , - captureExperience , - getExperienceCount , - loadAll , - findInterdisciplinary , - groupByLocation , - groupByLearner , - calculateDiversity , - findByLocation , - findByDomain , - findByLearner , - getHotspots , - getTopDomains , - generateReport , - mapByLocation , - generateDomainNetwork , -} -/* Decoder Not a pure module */ diff --git a/src-rescript/Privacy.res b/src-rescript/Privacy.res deleted file mode 100644 index ff9fda5..0000000 --- a/src-rescript/Privacy.res +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity Privacy and Anonymization Utilities -// This module provides tools to scrub PII (Personally Identifiable Information) -// from captured learning data while preserving analytical utility. - -open UbiCity - -// Configuration for learner anonymization. -type anonymizeLearnerOptions = { - preserveIds: bool, // Keep original IDs if needed for internal tracking - hashIds: bool, // Deterministically hash IDs to track the same learner across sessions - removeName: bool, // Strip 'name' field - removeInterests: bool, // Strip 'interests' field -} - -// Configuration for location blurring. -type anonymizeLocationOptions = { - fuzzyCoordinates: bool, // Round coordinates to reduce precision - fuzzRadius: float, // The rounding factor (e.g., 0.01 degrees) - removeAddress: bool, // Strip physical address - generalizeType: bool, // Convert specific place names to general categories -} - -type anonymizationLevel = Full | Partial | None_ - -module Crypto = { - @module("crypto") @val external randomUUID: unit => string = "randomUUID" -} - -// HASHING: Implementation of the DJB2 algorithm for deterministic ID blurring. -let hashString = (str: string): string => { - let hash = ref(5381) - - for i in 0 to str->String.length - 1 { - let char = str->String.charCodeAt(i)->Int.fromFloat - let shifted = hash.contents * 33 - hash := shifted + char - } - - Math.abs(Int.toFloat(hash.contents))->Float.toString(~radix=16)->String.slice(~start=0, ~end=8) -} - -// FFI: Regex-based text replacement. -@send external replaceRegex: (string, Js.Re.t, string) => string = "replace" - -// SCRUBBING: Replaces common PII patterns (emails, phones, URLs) with placeholders. -let sanitizeText = (text: string): string => { - let maxLength = 10000 - let sanitized = text->String.length > maxLength ? text->String.slice(~start=0, ~end=maxLength) : text - - // PATTERN: Email addresses - let sanitized = sanitized->replaceRegex(%re("/[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}/g"), "[email]") - - // PATTERN: Phone numbers - let sanitized = sanitized->replaceRegex(%re("/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g"), "[phone]") - - // PATTERN: URLs (potentially containing tracking parameters) - let sanitized = sanitized->replaceRegex(%re("/https?:\/\/[^\s]{1,2000}/g"), "[url]") - - // PATTERN: Common name-revealing phrases - sanitized->replaceRegex( - %re("/\b(I met|met with|talked to|spoke with)\s+([A-Z][a-z]+)\b/g"), - "$1 [person]", - ) -} - -// PIPELINE: Anonymizes the learner profile. -let anonymizeLearner = ( - experience: LearningExperience.t, - options: option, -): LearningExperience.t => { - let opts = options->Option.getOr({ - preserveIds: false, - hashIds: true, - removeName: true, - removeInterests: false, - }) - - let newLearnerId = if !opts.preserveIds { - if opts.hashIds { - "anon-" ++ hashString(experience.learner.id) - } else { - "anon-" ++ Crypto.randomUUID()->String.slice(~start=0, ~end=8) - } - } else { - experience.learner.id - } - - let newName = if opts.removeName { None } else { experience.learner.name } - let newInterests = if opts.removeInterests { None } else { experience.learner.interests } - - { - ...experience, - learner: { - id: newLearnerId, - name: newName, - interests: newInterests, - }, - } -} - -// PIPELINE: Blurs location data to protect physical privacy. -let anonymizeLocation = ( - experience: LearningExperience.t, - options: option, -): LearningExperience.t => { - let opts = options->Option.getOr({ - fuzzyCoordinates: true, - fuzzRadius: 0.01, // ~1.1km precision - removeAddress: true, - generalizeType: false, - }) - - let newCoordinates = switch experience.context.location.coordinates { - | Some(coords) if opts.fuzzyCoordinates => { - // GRID-BASED FUZZING: Snap coordinates to a grid defined by fuzzRadius. - let latitude = Math.round(coords.latitude /. opts.fuzzRadius) *. opts.fuzzRadius - let longitude = Math.round(coords.longitude /. opts.fuzzRadius) *. opts.fuzzRadius - let fuzzed: Coordinates.t = {latitude, longitude} - Some(fuzzed) - } - | coords => coords - } - - let newAddress = if opts.removeAddress { None } else { experience.context.location.address } - - let newType = switch experience.context.location.type_ { - | Some(t) if opts.generalizeType => { - // GENERALIZATION: Map specific place types to high-level categories. - let generalizations = Dict.fromArray([ - ("coffee shop", "cafe"), - ("library branch", "library"), - ("university", "educational institution"), - ]) - - let lower = t->String.toLowerCase - Some(generalizations->Dict.get(lower)->Option.getOr(t)) - } - | t => t - } - - { - ...experience, - context: { - ...experience.context, - location: { - ...experience.context.location, - coordinates: newCoordinates, - address: newAddress, - type_: newType, - }, - }, - } -} - -// PIPELINE: Scrub all free-text fields for PII. -let removePII = (experience: LearningExperience.t): LearningExperience.t => { - let newConnections = switch experience.context.connections { - | Some(conns) => Some(conns->Array.mapWithIndex((_name, i) => `person-${Int.toString(i + 1)}`)) - | None => None - } - - let newDescription = sanitizeText(experience.experience.description) - - let newOutcome = switch experience.experience.outcome { - | Some(outcome) => - Some({ - ...outcome, - connections_made: outcome.connections_made->Option.map(arr => arr->Array.map(sanitizeText)), - next_questions: outcome.next_questions->Option.map(arr => arr->Array.map(sanitizeText)), - }) - | None => None - } - - { - ...experience, - context: { ...experience.context, connections: newConnections }, - experience: { ...experience.experience, description: newDescription, outcome: newOutcome }, - } -} - -// Orchestrates the entire anonymization process. -let fullyAnonymize = ( - experience: LearningExperience.t, - options: option, -): LearningExperience.t => { - let opts = options->Option.getOr({learner: None, location: None}) - - experience - ->anonymizeLearner(opts.learner) - ->anonymizeLocation(opts.location) - ->removePII - ->(result => { - ...result, - privacy: Some({ level: #anonymous, shareableWith: None }), - }) -} diff --git a/src-rescript/Privacy.res.js b/src-rescript/Privacy.res.js deleted file mode 100644 index f7f08b7..0000000 --- a/src-rescript/Privacy.res.js +++ /dev/null @@ -1,233 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as $$Crypto from "crypto"; -import * as Core__Option from "@rescript/core/src/Core__Option.res.js"; - -var $$Crypto$1 = {}; - -function hashString(str) { - var hash = 5381; - for(var i = 0 ,i_finish = str.length; i < i_finish; ++i){ - var $$char = str.charCodeAt(i) | 0; - var shifted = Math.imul(hash, 33); - hash = shifted + $$char | 0; - } - return Math.abs(hash).toString(16).slice(0, 8); -} - -function sanitizeText(text) { - var sanitized = text.length > 10000 ? text.slice(0, 10000) : text; - var sanitized$1 = sanitized.replace(/[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}/g, "[email]"); - var sanitized$2 = sanitized$1.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, "[phone]"); - var sanitized$3 = sanitized$2.replace(/https?:\/\/[^\s]{1,2000}/g, "[url]"); - return sanitized$3.replace(/\b(I met|met with|talked to|spoke with)\s+([A-Z][a-z]+)\b/g, "$1 [person]"); -} - -function anonymizeLearner(experience, options) { - var opts = Core__Option.getOr(options, { - preserveIds: false, - hashIds: true, - removeName: true, - removeInterests: false - }); - var newLearnerId = opts.preserveIds ? experience.learner.id : ( - opts.hashIds ? "anon-" + hashString(experience.learner.id) : "anon-" + $$Crypto.randomUUID().slice(0, 8) - ); - var newName = opts.removeName ? undefined : experience.learner.name; - var newInterests = opts.removeInterests ? undefined : experience.learner.interests; - return { - id: experience.id, - timestamp: experience.timestamp, - learner: { - id: newLearnerId, - name: newName, - interests: newInterests - }, - context: experience.context, - experience: experience.experience, - privacy: experience.privacy, - tags: experience.tags, - version: experience.version - }; -} - -function anonymizeLocation(experience, options) { - var opts = Core__Option.getOr(options, { - fuzzyCoordinates: true, - fuzzRadius: 0.01, - removeAddress: true, - generalizeType: false - }); - var coords = experience.context.location.coordinates; - var newCoordinates; - if (coords !== undefined && opts.fuzzyCoordinates) { - var latitude = Math.round(coords.latitude / opts.fuzzRadius) * opts.fuzzRadius; - var longitude = Math.round(coords.longitude / opts.fuzzRadius) * opts.fuzzRadius; - newCoordinates = { - latitude: latitude, - longitude: longitude - }; - } else { - newCoordinates = coords; - } - var newAddress = opts.removeAddress ? undefined : experience.context.location.address; - var t = experience.context.location.type; - var newType; - if (t !== undefined && opts.generalizeType) { - var generalizations = Object.fromEntries([ - [ - "coffee shop", - "cafe" - ], - [ - "starbucks", - "cafe" - ], - [ - "library branch", - "library" - ], - [ - "community college", - "educational institution" - ], - [ - "university", - "educational institution" - ] - ]); - var lower = t.toLowerCase(); - newType = Core__Option.getOr(generalizations[lower], t); - } else { - newType = t; - } - var init = experience.context; - var init$1 = experience.context.location; - return { - id: experience.id, - timestamp: experience.timestamp, - learner: experience.learner, - context: { - location: { - name: init$1.name, - coordinates: newCoordinates, - type: newType, - address: newAddress - }, - situation: init.situation, - connections: init.connections, - timeOfDay: init.timeOfDay - }, - experience: experience.experience, - privacy: experience.privacy, - tags: experience.tags, - version: experience.version - }; -} - -function removePII(experience) { - var conns = experience.context.connections; - var newConnections = conns !== undefined ? conns.map(function (_name, i) { - return "person-" + (i + 1 | 0).toString(); - }) : undefined; - var newDescription = sanitizeText(experience.experience.description); - var outcome = experience.experience.outcome; - var newOutcome = outcome !== undefined ? ({ - success: outcome.success, - connections_made: Core__Option.map(outcome.connections_made, (function (arr) { - return arr.map(sanitizeText); - })), - next_questions: Core__Option.map(outcome.next_questions, (function (arr) { - return arr.map(sanitizeText); - })), - artifacts: outcome.artifacts - }) : undefined; - var init = experience.context; - var init$1 = experience.experience; - return { - id: experience.id, - timestamp: experience.timestamp, - learner: experience.learner, - context: { - location: init.location, - situation: init.situation, - connections: newConnections, - timeOfDay: init.timeOfDay - }, - experience: { - type: init$1.type, - description: newDescription, - domains: init$1.domains, - outcome: newOutcome, - duration: init$1.duration, - intensity: init$1.intensity - }, - privacy: experience.privacy, - tags: experience.tags, - version: experience.version - }; -} - -function fullyAnonymize(experience, options) { - var opts = Core__Option.getOr(options, { - learner: undefined, - location: undefined - }); - var result = anonymizeLearner(experience, opts.learner); - var result$1 = anonymizeLocation(result, opts.location); - var result$2 = removePII(result$1); - return { - id: result$2.id, - timestamp: result$2.timestamp, - learner: result$2.learner, - context: result$2.context, - experience: result$2.experience, - privacy: { - level: "anonymous", - shareableWith: undefined - }, - tags: result$2.tags, - version: result$2.version - }; -} - -function filterByPrivacyLevel(experiences, includePrivateOpt, param) { - var includePrivate = includePrivateOpt !== undefined ? includePrivateOpt : false; - return experiences.filter(function (exp) { - var match = exp.privacy; - if (match !== undefined && match.level === "private") { - return includePrivate; - } else { - return true; - } - }); -} - -function generateShareableDataset(experiences, includePrivateOpt, anonymizationLevelOpt, param) { - var includePrivate = includePrivateOpt !== undefined ? includePrivateOpt : false; - var anonymizationLevel = anonymizationLevelOpt !== undefined ? anonymizationLevelOpt : "Full"; - return filterByPrivacyLevel(experiences, includePrivate, undefined).map(function (exp) { - switch (anonymizationLevel) { - case "Full" : - return fullyAnonymize(exp, undefined); - case "Partial" : - return anonymizeLearner(exp, undefined); - case "None_" : - return exp; - - } - }); -} - -export { - $$Crypto$1 as $$Crypto, - hashString , - sanitizeText , - anonymizeLearner , - anonymizeLocation , - removePII , - fullyAnonymize , - filterByPrivacyLevel , - generateShareableDataset , -} -/* crypto Not a pure module */ diff --git a/src-rescript/README.md b/src-rescript/README.md deleted file mode 100644 index e54fc05..0000000 --- a/src-rescript/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# UbiCity ReScript Modules - -Type-safe ReScript implementation of UbiCity core functionality. - -## Modules - -### UbiCity.res -Core domain types and functional utilities: -- `Coordinates` - GPS coordinates with validation -- `Location` - Learning locations -- `Learner` - Learner identity -- `Context` - WHERE learning happened -- `ExperienceData` - WHAT was learned -- `LearningExperience` - Complete learning experience record -- `Privacy` - Privacy settings -- `Analysis` - Functional analysis utilities - -### Capture.res -CLI capture tool for recording learning experiences: -- `CaptureSession` - Interactive capture session -- Supports Quick, Full, and Template modes -- Type-safe input validation -- Async/await interface - -### Mapper.res -Urban knowledge mapper for storing and analyzing experiences: -- Storage integration -- Fast lookup indices (location, domain, learner) -- Analysis functions (hotspots, interdisciplinary connections) -- Domain diversity calculations - -### CaptureCLI.res -CLI entry point for the capture tool. - -## Compilation - -ReScript modules compile to ES6 JavaScript with `.res.js` extension: - -```bash -# Compile ReScript to JavaScript -npm run res:build - -# Watch mode -npm run res:dev -``` - -## Usage - -### Capture a Learning Experience - -```bash -# Quick mode (minimal prompts) -node src-rescript/CaptureCLI.res.js - -# Full mode (all optional fields) -node src-rescript/CaptureCLI.res.js full - -# Generate template -node src-rescript/CaptureCLI.res.js template -``` - -### Programmatic API - -```rescript -open UbiCity - -// Create a learning experience -let learnerResult = Learner.make(~id="alice", ()) -let locationResult = Location.make(~name="City Library", ()) - -switch (learnerResult, locationResult) { -| (Ok(learner), Ok(location)) => { - let context = Context.make(~location, ()) - - let experienceResult = ExperienceData.make( - ~type_="reading", - ~description="Explored urban planning books", - ~domains=["architecture", "sociology"], - () - ) - - switch experienceResult { - | Ok(experience) => { - let learning = LearningExperience.make( - ~learner, - ~context, - ~experience, - () - ) - Console.log(learning.id) - } - | Error(err) => Console.error(err) - } - } -| _ => Console.error("Failed to create learner or location") -} -``` - -### Using the Mapper - -```rescript -// Initialize mapper -let mapper = await Mapper.make(~storageDir="./ubicity-data", ()) - -// Capture an experience -let result = await Mapper.captureExperience(mapper, learningExperience) - -// Find interdisciplinary connections -let connections = Mapper.findInterdisciplinary(mapper) - -// Get hotspot locations -let hotspots = Mapper.getHotspots(mapper, ~limit=5, ()) - -// Calculate domain diversity -let diversity = Mapper.calculateDiversity(mapper) -``` - -## Type Safety Benefits - -ReScript provides: -- **Compile-time type checking** - Catch errors before runtime -- **Sound type system** - No `null` or `undefined` surprises -- **Pattern matching** - Exhaustive error handling -- **Immutability** - Default immutability prevents bugs -- **Interop with JS** - Seamless integration with existing JS code - -## Migration Status - -- ✅ Core domain types (UbiCity.res) -- ✅ Capture tool (Capture.res) -- ✅ Mapper (Mapper.res) -- ✅ CLI entry point (CaptureCLI.res) -- ⏳ Storage validation integration -- ⏳ Analysis module (full port) -- ⏳ Visualization module -- ⏳ Privacy module -- ⏳ Export module - -## Architecture - -``` -ReScript Modules JavaScript Integration -┌─────────────────┐ ┌──────────────────┐ -│ UbiCity.res │ │ storage.js │ -│ (Domain Types) │ │ (File I/O) │ -└────────┬────────┘ └─────────┬────────┘ - │ │ - ▼ ▼ -┌─────────────────┐ ┌──────────────────┐ -│ Mapper.res │◄──────│ Bindings │ -│ (Analysis) │ │ (FFI) │ -└────────┬────────┘ └──────────────────┘ - │ - ▼ -┌─────────────────┐ -│ Capture.res │ -│ (CLI Tool) │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ CaptureCLI.res │ -│ (Entry Point) │ -└─────────────────┘ -``` - -## License - -PMPL-1.0-or-later diff --git a/src-rescript/UbiCity.res b/src-rescript/UbiCity.res deleted file mode 100644 index 9d6f987..0000000 --- a/src-rescript/UbiCity.res +++ /dev/null @@ -1,256 +0,0 @@ -// ReScript types for UbiCity domain model -// These modules define the core data structures used throughout the application. -// The ReScript compiler generates highly optimized and type-safe JavaScript from these definitions. - -module Coordinates = { - // Representation of a physical location on Earth using latitude and longitude. - type t = { - latitude: float, - longitude: float, - } - - // Safe constructor for coordinates. Ensures values are within valid geographic bounds. - let make = (~latitude: float, ~longitude: float): option => { - if latitude >= -90.0 && latitude <= 90.0 && longitude >= -180.0 && longitude <= 180.0 { - Some({latitude, longitude}) - } else { - None - } - } - - // Runtime validation for coordinates. - let isValid = (coords: t): bool => { - coords.latitude >= -90.0 && - coords.latitude <= 90.0 && - coords.longitude >= -180.0 && - coords.longitude <= 180.0 - } -} - -module Location = { - // A named place where learning occurs. - type t = { - name: string, - coordinates: option, - @as("type") type_: option, // e.g., "makerspace", "library" - address: option, - } - - // Factory function for Location with basic validation on the 'name' field. - let make = (~name: string, ~coordinates=?, ~type_=?, ~address=?, ()): result => { - if name->String.length == 0 { - Error("Location name is required") - } else { - Ok({ - name, - coordinates, - type_, - address, - }) - } - } -} - -module Learner = { - // The person engaged in the learning experience. - type t = { - id: string, // Unique identifier or pseudonym - name: option, - interests: option>, - } - - // Factory function for Learner. - let make = (~id: string, ~name=?, ~interests=?, ()): result => { - if id->String.length == 0 { - Error("Learner ID is required") - } else { - Ok({id, name, interests}) - } - } -} - -module Context = { - // The situational context of the learning event. - type t = { - location: Location.t, - situation: option, // Description of what was happening - connections: option>, // People involved (pseudonyms or IDs) - timeOfDay: option<[#morning | #afternoon | #evening | #night]>, - } - - // Constructor for learning context. - let make = ( - ~location: Location.t, - ~situation=?, - ~connections=?, - ~timeOfDay=?, - (), - ): t => { - {location, situation, connections, timeOfDay} - } -} - -module Outcome = { - // The result or impact of the learning experience. - type t = { - success: option, - connections_made: option>, // New insights or links identified - next_questions: option>, // Questions that emerged from the experience - artifacts: option>, // Created items (URLs, file paths, etc.) - } - - // Default empty outcome. - let empty: t = { - success: None, - connections_made: None, - next_questions: None, - artifacts: None, - } -} - -module ExperienceData = { - // Quantitative/Qualitative data describing the learning activity. - type intensity = [#low | #medium | #high] - - type t = { - @as("type") type_: string, // e.g., "experiment", "reading" - description: string, - domains: option>, // Subject areas involved - outcome: option, - duration: option, // Duration in minutes - intensity: option, - } - - // Factory function for experience data. - let make = ( - ~type_: string, - ~description: string, - ~domains=?, - ~outcome=?, - ~duration=?, - ~intensity=?, - (), - ): result => { - if type_->String.length == 0 { - Error("Experience type is required") - } else if description->String.length == 0 { - Error("Description is required") - } else { - Ok({type_, description, domains, outcome, duration, intensity}) - } - } -} - -module Privacy = { - // Privacy preferences for the learning data. - type level = [#\"private" | #anonymous | #public] - - type t = { - level: level, - shareableWith: option>, // Specific groups or individuals - } - - // Helper for anonymous-by-default logic. - let makeAnonymous: t = {level: #anonymous, shareableWith: None} -} - -module LearningExperience = { - // The top-level container for a complete UbiCity learning capture. - type t = { - id: string, - timestamp: string, - learner: Learner.t, - context: Context.t, - experience: ExperienceData.t, - privacy: option, - tags: option>, - version: string, - } - - // FFI: Bindings to Node.js crypto API for ID generation. - module Crypto = { - @module("crypto") @val external randomUUID: unit => string = "randomUUID" - } - - // ID GENERATION: Creates a unique identifier with the 'ubi-' prefix. - let generateId = (): string => { - let uuid = Crypto.randomUUID() - "ubi-" ++ uuid - } - - // PRIMARY CONSTRUCTOR: Aggregates all components into a LearningExperience object. - let make = ( - ~id=?, - ~timestamp=?, - ~learner: Learner.t, - ~context: Context.t, - ~experience: ExperienceData.t, - ~privacy=?, - ~tags=?, - ~version="0.3.0", - (), - ): t => { - let id = id->Option.getOr(generateId()) - let timestamp = timestamp->Option.getOr(Date.now()->Float.toString) - - {id, timestamp, learner, context, experience, privacy, tags, version} - } -} - -// ANALYSIS ENGINE: Functional utilities for processing learning datasets. -module Analysis = { - // Returns experiences that involve more than one domain. - let findInterdisciplinary = (experiences: array): array< - LearningExperience.t, - > => { - experiences->Array.filter(exp => { - switch exp.experience.domains { - | Some(domains) => domains->Array.length > 1 - | None => false - } - }) - } - - // Group experiences by the location name. - let groupByLocation = ( - experiences: array, - ): Dict.t> => { - experiences->Array.reduce(Dict.make(), (acc, exp) => { - let locationName = exp.context.location.name - let existing = acc->Dict.get(locationName)->Option.getOr([]) - acc->Dict.set(locationName, existing->Array.concat([exp])) - acc - }) - } - - // Group experiences by the learner identifier. - let groupByLearner = ( - experiences: array, - ): Dict.t> => { - experiences->Array.reduce(Dict.make(), (acc, exp) => { - let learnerId = exp.learner.id - let existing = acc->Dict.get(learnerId)->Option.getOr([]) - acc->Dict.set(learnerId, existing->Array.concat([exp])) - acc - }) - } - - // Calculates the number of unique domains represented in a dataset. - let calculateDiversity = (experiences: array): int => { - let domains = experiences->Array.reduce([], (acc, exp) => { - switch exp.experience.domains { - | Some(doms) => Array.concat(acc, doms) - | None => acc - } - }) - // Get unique domains - let uniqueDomains = domains->Array.reduce([], (acc, domain) => { - if acc->Array.includes(domain) { - acc - } else { - acc->Array.concat([domain]) - } - }) - uniqueDomains->Array.length - } -} diff --git a/src-rescript/UbiCity.res.js b/src-rescript/UbiCity.res.js deleted file mode 100644 index ff6f326..0000000 --- a/src-rescript/UbiCity.res.js +++ /dev/null @@ -1,228 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as $$Crypto from "crypto"; -import * as Core__Array from "@rescript/core/src/Core__Array.res.js"; -import * as Core__Option from "@rescript/core/src/Core__Option.res.js"; - -function make(latitude, longitude) { - if (latitude >= -90.0 && latitude <= 90.0 && longitude >= -180.0 && longitude <= 180.0) { - return { - latitude: latitude, - longitude: longitude - }; - } - -} - -function isValid(coords) { - if (coords.latitude >= -90.0 && coords.latitude <= 90.0 && coords.longitude >= -180.0) { - return coords.longitude <= 180.0; - } else { - return false; - } -} - -var Coordinates = { - make: make, - isValid: isValid -}; - -function make$1(name, coordinates, type_, address, param) { - if (name.length === 0) { - return { - TAG: "Error", - _0: "Location name is required" - }; - } else { - return { - TAG: "Ok", - _0: { - name: name, - coordinates: coordinates, - type: type_, - address: address - } - }; - } -} - -var $$Location = { - make: make$1 -}; - -function make$2(id, name, interests, param) { - if (id.length === 0) { - return { - TAG: "Error", - _0: "Learner ID is required" - }; - } else { - return { - TAG: "Ok", - _0: { - id: id, - name: name, - interests: interests - } - }; - } -} - -var Learner = { - make: make$2 -}; - -function make$3($$location, situation, connections, timeOfDay, param) { - return { - location: $$location, - situation: situation, - connections: connections, - timeOfDay: timeOfDay - }; -} - -var Context = { - make: make$3 -}; - -var Outcome = { - empty: { - success: undefined, - connections_made: undefined, - next_questions: undefined, - artifacts: undefined - } -}; - -function make$4(type_, description, domains, outcome, duration, intensity, param) { - if (type_.length === 0) { - return { - TAG: "Error", - _0: "Experience type is required" - }; - } else if (description.length === 0) { - return { - TAG: "Error", - _0: "Description is required" - }; - } else { - return { - TAG: "Ok", - _0: { - type: type_, - description: description, - domains: domains, - outcome: outcome, - duration: duration, - intensity: intensity - } - }; - } -} - -var ExperienceData = { - make: make$4 -}; - -var Privacy = { - makeAnonymous: { - level: "anonymous", - shareableWith: undefined - } -}; - -var $$Crypto$1 = {}; - -function generateId() { - var uuid = $$Crypto.randomUUID(); - return "ubi-" + uuid; -} - -function make$5(id, timestamp, learner, context, experience, privacy, tags, versionOpt, param) { - var version = versionOpt !== undefined ? versionOpt : "0.3.0"; - var id$1 = Core__Option.getOr(id, generateId()); - var timestamp$1 = Core__Option.getOr(timestamp, Date.now().toString()); - return { - id: id$1, - timestamp: timestamp$1, - learner: learner, - context: context, - experience: experience, - privacy: privacy, - tags: tags, - version: version - }; -} - -var LearningExperience = { - $$Crypto: $$Crypto$1, - generateId: generateId, - make: make$5 -}; - -function findInterdisciplinary(experiences) { - return experiences.filter(function (exp) { - var domains = exp.experience.domains; - if (domains !== undefined) { - return domains.length > 1; - } else { - return false; - } - }); -} - -function groupByLocation(experiences) { - return Core__Array.reduce(experiences, {}, (function (acc, exp) { - var locationName = exp.context.location.name; - var existing = Core__Option.getOr(acc[locationName], []); - acc[locationName] = existing.concat([exp]); - return acc; - })); -} - -function groupByLearner(experiences) { - return Core__Array.reduce(experiences, {}, (function (acc, exp) { - var learnerId = exp.learner.id; - var existing = Core__Option.getOr(acc[learnerId], []); - acc[learnerId] = existing.concat([exp]); - return acc; - })); -} - -function calculateDiversity(experiences) { - var domains = Core__Array.reduce(experiences, [], (function (acc, exp) { - var doms = exp.experience.domains; - if (doms !== undefined) { - return acc.concat(doms); - } else { - return acc; - } - })); - return Core__Array.reduce(domains, [], (function (acc, domain) { - if (acc.includes(domain)) { - return acc; - } else { - return acc.concat([domain]); - } - })).length; -} - -var Analysis = { - findInterdisciplinary: findInterdisciplinary, - groupByLocation: groupByLocation, - groupByLearner: groupByLearner, - calculateDiversity: calculateDiversity -}; - -export { - Coordinates , - $$Location , - Learner , - Context , - Outcome , - ExperienceData , - Privacy , - LearningExperience , - Analysis , -} -/* crypto Not a pure module */ diff --git a/src-rescript/Visualization.res b/src-rescript/Visualization.res deleted file mode 100644 index e723fe4..0000000 --- a/src-rescript/Visualization.res +++ /dev/null @@ -1,569 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// UbiCity Visualization Generator -// Creates interactive HTML reports with filtering and search - -type visualizationOptions = { - outputFile: string, - includeMap: bool, - includeTimeline: bool, - includeNetwork: bool, -} - -let defaultOptions: visualizationOptions = { - outputFile: "ubicity-map.html", - includeMap: true, - includeTimeline: true, - includeNetwork: true, -} - -// Generate CSS styles -let generateCSS = (): string => { - ` - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - - body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #f5f5f5; - color: #333; - } - - header { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 2rem; - text-align: center; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - } - - header h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; - } - - header p { - opacity: 0.9; - font-size: 1.1rem; - } - - .container { - max-width: 1200px; - margin: 2rem auto; - padding: 0 1rem; - } - - .stats { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; - } - - .stat-card { - background: white; - padding: 1.5rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - text-align: center; - } - - .stat-value { - font-size: 2.5rem; - font-weight: bold; - color: #667eea; - margin-bottom: 0.5rem; - } - - .stat-label { - color: #666; - font-size: 0.9rem; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - .section { - background: white; - border-radius: 8px; - padding: 2rem; - margin-bottom: 2rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - } - - .section h2 { - color: #667eea; - margin-bottom: 1.5rem; - font-size: 1.8rem; - } - - .search-bar { - width: 100%; - padding: 0.75rem; - font-size: 1rem; - border: 2px solid #ddd; - border-radius: 4px; - margin-bottom: 1rem; - transition: border-color 0.3s; - } - - .search-bar:focus { - outline: none; - border-color: #667eea; - } - - .filters { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; - margin-bottom: 1rem; - } - - .filter-tag { - padding: 0.5rem 1rem; - background: #f0f0f0; - border: 1px solid #ddd; - border-radius: 20px; - cursor: pointer; - transition: all 0.3s; - font-size: 0.9rem; - } - - .filter-tag:hover { - background: #e0e0e0; - } - - .filter-tag.active { - background: #667eea; - color: white; - border-color: #667eea; - } - - .location-list { - display: grid; - gap: 1rem; - } - - .location-card { - padding: 1.5rem; - border: 1px solid #e0e0e0; - border-radius: 8px; - transition: all 0.3s; - } - - .location-card:hover { - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - border-color: #667eea; - } - - .location-name { - font-size: 1.3rem; - font-weight: bold; - color: #333; - margin-bottom: 0.5rem; - } - - .location-meta { - display: flex; - gap: 1.5rem; - margin-bottom: 1rem; - font-size: 0.9rem; - color: #666; - } - - .location-meta span { - display: flex; - align-items: center; - gap: 0.3rem; - } - - .domains { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; - } - - .domain-tag { - padding: 0.3rem 0.8rem; - background: #f0f4ff; - color: #667eea; - border-radius: 15px; - font-size: 0.85rem; - font-weight: 500; - } - - footer { - text-align: center; - padding: 2rem; - color: #666; - } - - @media (max-width: 768px) { - header h1 { - font-size: 1.8rem; - } - - .stats { - grid-template-columns: repeat(2, 1fr); - } - - .location-meta { - flex-direction: column; - gap: 0.5rem; - } - } - ` -} - -// Generate stats section -let generateStats = (report: Mapper.reportSummary): string => { - ` -
-
-
${Int.toString(report.totalExperiences)}
-
Experiences
-
-
-
${Int.toString(report.uniqueLocations)}
-
Locations
-
-
-
${Int.toString(report.uniqueDomains)}
-
Domains
-
-
-
${Int.toString(report.uniqueLearners)}
-
Learners
-
-
- ` -} - -// Generate hotspot card -let generateHotspotCard = (hotspot: Mapper.learningHotspot): string => { - let domainTags = hotspot.domains->Array.map(d => `${d}`)->Array.join("") - let domainsAttr = hotspot.domains->Array.join(" ") - - ` -
-
${hotspot.location}
-
- 📊 ${Int.toString(hotspot.count)} experiences - 👥 ${Int.toString(hotspot.learners)} learners - 🎯 ${Int.toString(hotspot.diversity)} diversity -
-
- ${domainTags} -
-
- ` -} - -// Generate hotspots section -let generateHotspotsSection = (hotspots: array): string => { - let cards = hotspots->Array.map(generateHotspotCard)->Array.join("\n") - - ` -
-

🔥 Learning Hotspots

-

- Locations with high disciplinary diversity (${Int.toString(hotspots->Array.length)} found) -

- - - -
- -
- ${cards} -
-
- ` -} - -// Generate geographic section -let generateGeographicSection = (locationsWithCoords: int): string => { - if locationsWithCoords > 0 { - ` -
-

🗺️ Geographic Distribution

-

- ${Int.toString(locationsWithCoords)} locations with GPS coordinates -

-
-

- 📍 Coordinates available for mapping -

-

- Export to GeoJSON to visualize in mapping tools: - - deno run --allow-read --allow-write src-rescript/Export.res.js geojson map.json - -

-
-
- ` - } else { - "" - } -} - -// Generate domain network section -let generateNetworkSection = (network: Mapper.domainNetwork): string => { - let topDomains = network.nodes - ->Array.toSorted((a, b) => Float.fromInt(b.size - a.size)) - ->Array.slice(~start=0, ~end=10) - ->Array.map(node => { - ` -
- ${node.id}: ${Int.toString(node.size)} experiences -
- ` - }) - ->Array.join("") - - let strongestConnections = network.edges - ->Array.toSorted((a, b) => Float.fromInt(b.weight - a.weight)) - ->Array.slice(~start=0, ~end=10) - ->Array.map(edge => { - ` -
- ${edge.source} ↔ ${edge.target} - ${Int.toString(edge.weight)}x -
- ` - }) - ->Array.join("") - - ` -
-

🕸️ Domain Network

-

- ${Int.toString(network.nodes->Array.length)} domains, ${Int.toString(network.edges->Array.length)} connections -

- -
-

Top Domains

-
- ${topDomains} -
-
- -
-

Strongest Connections

-
- ${strongestConnections} -
-
- -
- 💡 Export to DOT format for graph visualization: - - deno run --allow-read --allow-write src-rescript/Export.res.js dot network.dot - -
-
- ` -} - -// Generate insights section -let generateInsightsSection = (report: Mapper.reportSummary, hotspots: array): string => { - let mostDiverseLocation = hotspots->Array.get(0) - let locationName = mostDiverseLocation->Option.map(h => h.location)->Option.getOr("N/A") - let diversity = mostDiverseLocation->Option.map(h => Int.toString(h.diversity))->Option.getOr("0") - - let avgDiversity = if report.uniqueLocations > 0 { - Float.toString(Int.toFloat(report.uniqueDomains) /. Int.toFloat(report.uniqueLocations)) - } else { - "0" - } - - ` -
-

💡 Insights

-
-
- Most diverse location: - ${locationName} (${diversity} domains) -
-
- Interdisciplinary learning: - ${Int.toString(report.interdisciplinaryExperiences)} experiences span multiple domains -
-
- Average diversity per location: - ${avgDiversity} domains -
-
-
- ` -} - -// Generate JavaScript for interactive features -let generateJavaScript = (hotspots: array): string => { - // Extract all unique domains - let allDomains = hotspots - ->Array.reduce([], (acc, h) => Array.concat(acc, h.domains)) - ->Array.reduce([], (acc, d) => { - if acc->Array.includes(d) {acc} else {acc->Array.concat([d])} - }) - - let domainsJSON = switch JSON.stringifyAny(allDomains) { - | Some(json) => json - | None => "[]" - } - - ` - // Search functionality - const searchBar = document.getElementById('searchHotspots'); - const cards = document.querySelectorAll('.location-card'); - - searchBar.addEventListener('input', (e) => { - const query = e.target.value.toLowerCase(); - - cards.forEach(card => { - const text = card.textContent.toLowerCase(); - card.style.display = text.includes(query) ? 'block' : 'none'; - }); - }); - - // Domain filters - const allDomains = ${domainsJSON}; - const filtersContainer = document.getElementById('domainFilters'); - let activeDomains = new Set(); - - allDomains.forEach(domain => { - const tag = document.createElement('div'); - tag.className = 'filter-tag'; - tag.textContent = domain; - tag.addEventListener('click', () => { - tag.classList.toggle('active'); - - if (tag.classList.contains('active')) { - activeDomains.add(domain); - } else { - activeDomains.delete(domain); - } - - filterCards(); - }); - - filtersContainer.appendChild(tag); - }); - - function filterCards() { - if (activeDomains.size === 0) { - cards.forEach(card => card.style.display = 'block'); - return; - } - - cards.forEach(card => { - const cardDomains = card.dataset.domains.split(' '); - const hasActiveDomain = cardDomains.some(d => activeDomains.has(d)); - card.style.display = hasActiveDomain ? 'block' : 'none'; - }); - } - ` -} - -// Generate complete HTML document -let generateHTML = ( - report: Mapper.reportSummary, - hotspots: array, - network: Mapper.domainNetwork, - locationsWithCoords: int, -): string => { - let currentDate = Date.make()->Date.toLocaleDateString - - ` - - - - - UbiCity Learning Map - - - -
-

🏙️ UbiCity Learning Map

-

Mapping informal learning across urban space

-

- Generated: ${currentDate} -

-
- -
- ${generateStats(report)} - ${generateHotspotsSection(hotspots)} - ${generateGeographicSection(locationsWithCoords)} - ${generateNetworkSection(network)} - ${generateInsightsSection(report, hotspots)} -
- - - - - -` -} - -// Main visualization generation function -let generateVisualization = async ( - mapper: Mapper.t, - options: option, -): promise> => { - let opts = options->Option.getOr(defaultOptions) - - // Load all experiences - let loadResult = await Mapper.loadAll(mapper) - switch loadResult { - | Error(err) => Promise.resolve(Error("Failed to load experiences: " ++ err)) - | Ok(_count) => { - // Generate report - let reportResult = await Mapper.generateReport(mapper) - switch reportResult { - | Error(err) => Promise.resolve(Error("Failed to generate report: " ++ err)) - | Ok(report) => { - // Get location map - let locationMap = Mapper.mapByLocation(mapper) - - // Count locations with coordinates - let locationsWithCoords = locationMap - ->Dict.toArray - ->Array.filter(((_, data)) => data.coordinates->Option.isSome) - ->Array.length - - // Generate domain network - let network = Mapper.generateDomainNetwork(mapper) - - // Generate HTML - let html = generateHTML( - report.summary, - report.learningHotspots, - network, - locationsWithCoords, - ) - - // Save visualization - mapper.storage - ->Mapper.Storage.saveVisualization(html, opts.outputFile) - ->Promise.then(_filepath => Promise.resolve(Ok(opts.outputFile))) - ->Promise.catch(_err => Promise.resolve(Error("Failed to save visualization"))) - } - } - } - } -} diff --git a/src-rescript/Visualization.res.js b/src-rescript/Visualization.res.js deleted file mode 100644 index e662db2..0000000 --- a/src-rescript/Visualization.res.js +++ /dev/null @@ -1,139 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Mapper from "./Mapper.res.js"; -import * as Core__Array from "@rescript/core/src/Core__Array.res.js"; -import * as Core__Option from "@rescript/core/src/Core__Option.res.js"; -import * as Core__Promise from "@rescript/core/src/Core__Promise.res.js"; - -var defaultOptions = { - outputFile: "ubicity-map.html", - includeMap: true, - includeTimeline: true, - includeNetwork: true -}; - -function generateCSS() { - return "\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #f5f5f5;\n color: #333;\n }\n\n header {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n padding: 2rem;\n text-align: center;\n box-shadow: 0 2px 10px rgba(0,0,0,0.1);\n }\n\n header h1 {\n font-size: 2.5rem;\n margin-bottom: 0.5rem;\n }\n\n header p {\n opacity: 0.9;\n font-size: 1.1rem;\n }\n\n .container {\n max-width: 1200px;\n margin: 2rem auto;\n padding: 0 1rem;\n }\n\n .stats {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;\n margin-bottom: 2rem;\n }\n\n .stat-card {\n background: white;\n padding: 1.5rem;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n text-align: center;\n }\n\n .stat-value {\n font-size: 2.5rem;\n font-weight: bold;\n color: #667eea;\n margin-bottom: 0.5rem;\n }\n\n .stat-label {\n color: #666;\n font-size: 0.9rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .section {\n background: white;\n border-radius: 8px;\n padding: 2rem;\n margin-bottom: 2rem;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n .section h2 {\n color: #667eea;\n margin-bottom: 1.5rem;\n font-size: 1.8rem;\n }\n\n .search-bar {\n width: 100%;\n padding: 0.75rem;\n font-size: 1rem;\n border: 2px solid #ddd;\n border-radius: 4px;\n margin-bottom: 1rem;\n transition: border-color 0.3s;\n }\n\n .search-bar:focus {\n outline: none;\n border-color: #667eea;\n }\n\n .filters {\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n margin-bottom: 1rem;\n }\n\n .filter-tag {\n padding: 0.5rem 1rem;\n background: #f0f0f0;\n border: 1px solid #ddd;\n border-radius: 20px;\n cursor: pointer;\n transition: all 0.3s;\n font-size: 0.9rem;\n }\n\n .filter-tag:hover {\n background: #e0e0e0;\n }\n\n .filter-tag.active {\n background: #667eea;\n color: white;\n border-color: #667eea;\n }\n\n .location-list {\n display: grid;\n gap: 1rem;\n }\n\n .location-card {\n padding: 1.5rem;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n transition: all 0.3s;\n }\n\n .location-card:hover {\n box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n border-color: #667eea;\n }\n\n .location-name {\n font-size: 1.3rem;\n font-weight: bold;\n color: #333;\n margin-bottom: 0.5rem;\n }\n\n .location-meta {\n display: flex;\n gap: 1.5rem;\n margin-bottom: 1rem;\n font-size: 0.9rem;\n color: #666;\n }\n\n .location-meta span {\n display: flex;\n align-items: center;\n gap: 0.3rem;\n }\n\n .domains {\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n }\n\n .domain-tag {\n padding: 0.3rem 0.8rem;\n background: #f0f4ff;\n color: #667eea;\n border-radius: 15px;\n font-size: 0.85rem;\n font-weight: 500;\n }\n\n footer {\n text-align: center;\n padding: 2rem;\n color: #666;\n }\n\n @media (max-width: 768px) {\n header h1 {\n font-size: 1.8rem;\n }\n\n .stats {\n grid-template-columns: repeat(2, 1fr);\n }\n\n .location-meta {\n flex-direction: column;\n gap: 0.5rem;\n }\n }\n "; -} - -function generateStats(report) { - return "\n
\n
\n
" + report.totalExperiences.toString() + "
\n
Experiences
\n
\n
\n
" + report.uniqueLocations.toString() + "
\n
Locations
\n
\n
\n
" + report.uniqueDomains.toString() + "
\n
Domains
\n
\n
\n
" + report.uniqueLearners.toString() + "
\n
Learners
\n
\n
\n "; -} - -function generateHotspotCard(hotspot) { - var domainTags = hotspot.domains.map(function (d) { - return "" + d + ""; - }).join(""); - var domainsAttr = hotspot.domains.join(" "); - return "\n
\n
" + hotspot.location + "
\n
\n 📊 " + hotspot.count.toString() + " experiences\n 👥 " + hotspot.learners.toString() + " learners\n 🎯 " + hotspot.diversity.toString() + " diversity\n
\n
\n " + domainTags + "\n
\n
\n "; -} - -function generateHotspotsSection(hotspots) { - var cards = hotspots.map(generateHotspotCard).join("\n"); - return "\n
\n

🔥 Learning Hotspots

\n

\n Locations with high disciplinary diversity (" + hotspots.length.toString() + " found)\n

\n\n \n\n
\n\n
\n " + cards + "\n
\n
\n "; -} - -function generateGeographicSection(locationsWithCoords) { - if (locationsWithCoords > 0) { - return "\n
\n

🗺️ Geographic Distribution

\n

\n " + locationsWithCoords.toString() + " locations with GPS coordinates\n

\n
\n

\n 📍 Coordinates available for mapping\n

\n

\n Export to GeoJSON to visualize in mapping tools:\n \n deno run --allow-read --allow-write src-rescript/Export.res.js geojson map.json\n \n

\n
\n
\n "; - } else { - return ""; - } -} - -function generateNetworkSection(network) { - var topDomains = network.nodes.toSorted(function (a, b) { - return b.size - a.size | 0; - }).slice(0, 10).map(function (node) { - return "\n
\n " + node.id + ": " + node.size.toString() + " experiences\n
\n "; - }).join(""); - var strongestConnections = network.edges.toSorted(function (a, b) { - return b.weight - a.weight | 0; - }).slice(0, 10).map(function (edge) { - return "\n
\n " + edge.source + " ↔ " + edge.target + "\n " + edge.weight.toString() + "x\n
\n "; - }).join(""); - return "\n
\n

🕸️ Domain Network

\n

\n " + network.nodes.length.toString() + " domains, " + network.edges.length.toString() + " connections\n

\n\n
\n

Top Domains

\n
\n " + topDomains + "\n
\n
\n\n
\n

Strongest Connections

\n
\n " + strongestConnections + "\n
\n
\n\n
\n 💡 Export to DOT format for graph visualization:\n \n deno run --allow-read --allow-write src-rescript/Export.res.js dot network.dot\n \n
\n
\n "; -} - -function generateInsightsSection(report, hotspots) { - var mostDiverseLocation = hotspots[0]; - var locationName = Core__Option.getOr(Core__Option.map(mostDiverseLocation, (function (h) { - return h.location; - })), "N/A"); - var diversity = Core__Option.getOr(Core__Option.map(mostDiverseLocation, (function (h) { - return h.diversity.toString(); - })), "0"); - var avgDiversity = report.uniqueLocations > 0 ? (report.uniqueDomains / report.uniqueLocations).toString() : "0"; - return "\n
\n

💡 Insights

\n
\n
\n Most diverse location:\n " + locationName + " (" + diversity + " domains)\n
\n
\n Interdisciplinary learning:\n " + report.interdisciplinaryExperiences.toString() + " experiences span multiple domains\n
\n
\n Average diversity per location:\n " + avgDiversity + " domains\n
\n
\n
\n "; -} - -function generateJavaScript(hotspots) { - var allDomains = Core__Array.reduce(Core__Array.reduce(hotspots, [], (function (acc, h) { - return acc.concat(h.domains); - })), [], (function (acc, d) { - if (acc.includes(d)) { - return acc; - } else { - return acc.concat([d]); - } - })); - var json = JSON.stringify(allDomains); - var domainsJSON = json !== undefined ? json : "[]"; - return "\n // Search functionality\n const searchBar = document.getElementById('searchHotspots');\n const cards = document.querySelectorAll('.location-card');\n\n searchBar.addEventListener('input', (e) => {\n const query = e.target.value.toLowerCase();\n\n cards.forEach(card => {\n const text = card.textContent.toLowerCase();\n card.style.display = text.includes(query) ? 'block' : 'none';\n });\n });\n\n // Domain filters\n const allDomains = " + domainsJSON + ";\n const filtersContainer = document.getElementById('domainFilters');\n let activeDomains = new Set();\n\n allDomains.forEach(domain => {\n const tag = document.createElement('div');\n tag.className = 'filter-tag';\n tag.textContent = domain;\n tag.addEventListener('click', () => {\n tag.classList.toggle('active');\n\n if (tag.classList.contains('active')) {\n activeDomains.add(domain);\n } else {\n activeDomains.delete(domain);\n }\n\n filterCards();\n });\n\n filtersContainer.appendChild(tag);\n });\n\n function filterCards() {\n if (activeDomains.size === 0) {\n cards.forEach(card => card.style.display = 'block');\n return;\n }\n\n cards.forEach(card => {\n const cardDomains = card.dataset.domains.split(' ');\n const hasActiveDomain = cardDomains.some(d => activeDomains.has(d));\n card.style.display = hasActiveDomain ? 'block' : 'none';\n });\n }\n "; -} - -function generateHTML(report, hotspots, network, locationsWithCoords) { - var currentDate = new Date().toLocaleDateString(); - return "\n\n\n \n \n UbiCity Learning Map\n \n\n\n
\n

🏙️ UbiCity Learning Map

\n

Mapping informal learning across urban space

\n

\n Generated: " + currentDate + "\n

\n
\n\n
\n " + generateStats(report) + "\n " + generateHotspotsSection(hotspots) + "\n " + generateGeographicSection(locationsWithCoords) + "\n " + generateNetworkSection(network) + "\n " + generateInsightsSection(report, hotspots) + "\n
\n\n \n\n \n\n"; -} - -async function generateVisualization(mapper, options) { - var opts = Core__Option.getOr(options, defaultOptions); - var loadResult = await Mapper.loadAll(mapper); - if (loadResult.TAG !== "Ok") { - return Promise.resolve({ - TAG: "Error", - _0: "Failed to load experiences: " + loadResult._0 - }); - } - var reportResult = await Mapper.generateReport(mapper); - if (reportResult.TAG !== "Ok") { - return Promise.resolve({ - TAG: "Error", - _0: "Failed to generate report: " + reportResult._0 - }); - } - var report = reportResult._0; - var locationMap = Mapper.mapByLocation(mapper); - var locationsWithCoords = Object.entries(locationMap).filter(function (param) { - return Core__Option.isSome(param[1].coordinates); - }).length; - var network = Mapper.generateDomainNetwork(mapper); - var html = generateHTML(report.summary, report.learningHotspots, network, locationsWithCoords); - return Core__Promise.$$catch(mapper.storage.saveVisualization(html, opts.outputFile).then(function (_filepath) { - return Promise.resolve({ - TAG: "Ok", - _0: opts.outputFile - }); - }), (function (_err) { - return Promise.resolve({ - TAG: "Error", - _0: "Failed to save visualization" - }); - })); -} - -export { - defaultOptions , - generateCSS , - generateStats , - generateHotspotCard , - generateHotspotsSection , - generateGeographicSection , - generateNetworkSection , - generateInsightsSection , - generateJavaScript , - generateHTML , - generateVisualization , -} -/* Mapper Not a pure module */ diff --git a/src-rescript/storage.js b/src-rescript/storage.js deleted file mode 100644 index 6269f40..0000000 --- a/src-rescript/storage.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Async file-based storage for learning experiences - * Uses promises instead of sync I/O for better performance - */ - -import { promises as fs } from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -/** - * Storage manager for learning experiences - */ -export class ExperienceStorage { - constructor(storageDir = './ubicity-data') { - this.storageDir = path.resolve(storageDir); - this.experiencesDir = path.join(this.storageDir, 'experiences'); - this.mapsDir = path.join(this.storageDir, 'maps'); - this.analysesDir = path.join(this.storageDir, 'analyses'); - } - - /** - * Ensure all storage directories exist - */ - async ensureDirectories() { - await fs.mkdir(this.storageDir, { recursive: true }); - await fs.mkdir(this.experiencesDir, { recursive: true }); - await fs.mkdir(this.mapsDir, { recursive: true }); - await fs.mkdir(this.analysesDir, { recursive: true }); - } - - /** - * Save an experience to disk - * @param {object} experience - Validated experience object - * @returns {Promise} File path where saved - */ - async saveExperience(experience) { - await this.ensureDirectories(); - - const filename = `${experience.id}.json`; - const filepath = path.join(this.experiencesDir, filename); - - await fs.writeFile( - filepath, - JSON.stringify(experience, null, 2), - 'utf8' - ); - - return filepath; - } - - /** - * Load a single experience by ID - * @param {string} id - Experience ID - * @returns {Promise} Experience data or null if not found - */ - async loadExperience(id) { - const filename = `${id}.json`; - const filepath = path.join(this.experiencesDir, filename); - - try { - const content = await fs.readFile(filepath, 'utf8'); - return JSON.parse(content); - } catch (error) { - if (error.code === 'ENOENT') { - return null; - } - throw error; - } - } - - /** - * Load all experiences from storage - * @returns {Promise} Array of all experiences - */ - async loadAllExperiences() { - try { - await this.ensureDirectories(); - const files = await fs.readdir(this.experiencesDir); - - const experiences = await Promise.all( - files - .filter(file => file.endsWith('.json')) - .map(async (file) => { - const filepath = path.join(this.experiencesDir, file); - const content = await fs.readFile(filepath, 'utf8'); - return JSON.parse(content); - }) - ); - - return experiences; - } catch (error) { - if (error.code === 'ENOENT') { - return []; - } - throw error; - } - } - - /** - * Delete an experience - * @param {string} id - Experience ID - * @returns {Promise} True if deleted, false if not found - */ - async deleteExperience(id) { - const filename = `${id}.json`; - const filepath = path.join(this.experiencesDir, filename); - - try { - await fs.unlink(filepath); - return true; - } catch (error) { - if (error.code === 'ENOENT') { - return false; - } - throw error; - } - } - - /** - * Save an analysis report - * @param {object} report - Analysis report - * @param {string} [name] - Optional name, otherwise uses timestamp - * @returns {Promise} File path where saved - */ - async saveReport(report, name = null) { - await this.ensureDirectories(); - - const filename = name || `report-${Date.now()}.json`; - const filepath = path.join(this.analysesDir, filename); - - await fs.writeFile( - filepath, - JSON.stringify(report, null, 2), - 'utf8' - ); - - return filepath; - } - - /** - * Save a visualization/map - * @param {string} html - HTML content - * @param {string} name - File name - * @returns {Promise} File path where saved - */ - async saveVisualization(html, name = 'ubicity-map.html') { - await this.ensureDirectories(); - - const filepath = path.join(this.storageDir, name); - await fs.writeFile(filepath, html, 'utf8'); - - return filepath; - } - - /** - * List all experience IDs - * @returns {Promise} Array of experience IDs - */ - async listExperienceIds() { - try { - const files = await fs.readdir(this.experiencesDir); - return files - .filter(file => file.endsWith('.json')) - .map(file => file.replace('.json', '')); - } catch (error) { - if (error.code === 'ENOENT') { - return []; - } - throw error; - } - } - - /** - * Get storage statistics - * @returns {Promise} Storage stats - */ - async getStats() { - const experiences = await this.listExperienceIds(); - - let totalSize = 0; - for (const id of experiences) { - const filepath = path.join(this.experiencesDir, `${id}.json`); - const stats = await fs.stat(filepath); - totalSize += stats.size; - } - - return { - totalExperiences: experiences.length, - totalSizeBytes: totalSize, - totalSizeKB: (totalSize / 1024).toFixed(2), - storageDir: this.storageDir, - }; - } -} diff --git a/src/rescript-bridge.js b/src/rescript-bridge.js deleted file mode 100644 index 6c18c8f..0000000 --- a/src/rescript-bridge.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Bridge to ReScript-compiled modules - * Imports the optimized JavaScript generated from ReScript - * - * NOTE: de-TypeScripted from rescript-bridge.ts (issue #122 / ubicity#30 - * — no `.ts` in src/). This is ReScript-interop glue; it is deliberately - * NOT ported to AffineScript (that would re-implement a ReScript bridge - * and cross the estate's ReScript-hands-off boundary). Faithful plain - * ESM type-strip; runtime behaviour unchanged. The TS `export type {...}` - * re-export is dropped (type-only; consumers were .ts which src/ no - * longer permits). - */ - -// ReScript compiles to ES6 modules with .res.js extension. -import * as UbiCity from '../src-rescript/UbiCity.res.js'; - -/** - * Create a validated learning experience using ReScript's type system - */ -export function createLearningExperience(data) { - // Use ReScript's make functions with strong typing - const learner = UbiCity.Learner.make( - data.learner.id, - data.learner.name, - data.learner.interests, - ); - - if (learner.TAG === 'Error') { - throw new Error(learner._0); - } - - const location = UbiCity.Location.make( - data.context.location.name, - data.context.location.coordinates, - ); - - if (location.TAG === 'Error') { - throw new Error(location._0); - } - - const context = UbiCity.Context.make( - location._0, - data.context.situation, - data.context.connections, - ); - - const experience = UbiCity.ExperienceData.make( - data.experience.type, - data.experience.description, - data.experience.domains, - ); - - if (experience.TAG === 'Error') { - throw new Error(experience._0); - } - - return UbiCity.LearningExperience.make( - data.id, - data.timestamp, - learner._0, - context, - experience._0, - data.privacy, - data.tags, - ); -} - -/** - * Functional analysis using ReScript - */ -export const Analysis = { - findInterdisciplinary: UbiCity.Analysis.findInterdisciplinary, - groupByLocation: UbiCity.Analysis.groupByLocation, - groupByLearner: UbiCity.Analysis.groupByLearner, - calculateDiversity: UbiCity.Analysis.calculateDiversity, -}; diff --git a/tests/export.test.ts b/tests/export.test.ts deleted file mode 100644 index 41df596..0000000 --- a/tests/export.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Export functionality tests - * Tests CSV, GeoJSON, DOT, and Markdown exports - */ - -import { assert, assertEquals } from '@std/assert'; - -Deno.test('Export - CSV format generation', () => { - const experiences = [ - { - id: 'exp-001', - timestamp: '2025-01-01T10:00:00Z', - learner: { id: 'alice' }, - context: { location: { name: 'Makerspace A' } }, - experience: { - type: 'workshop', - domain: ['electronics'], - description: 'Built LED circuit', - }, - }, - ]; - - const headers = [ - 'id', - 'timestamp', - 'learner_id', - 'location', - 'type', - 'domains', - 'description', - ]; - const rows = experiences.map((e) => [ - e.id, - e.timestamp, - e.learner.id, - e.context.location.name, - e.experience.type, - e.experience.domain.join(';'), - e.experience.description, - ]); - - const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n'); - - assert(csv.includes('id,timestamp,learner_id')); - assert(csv.includes('exp-001,2025-01-01T10:00:00Z')); - assert(csv.includes('electronics')); -}); - -Deno.test('Export - GeoJSON format generation', () => { - const experiences = [ - { - id: 'exp-001', - context: { - location: { - name: 'Makerspace A', - coordinates: { lat: 37.77, lon: -122.42 }, - }, - }, - experience: { type: 'workshop' }, - }, - ]; - - const geojson = { - type: 'FeatureCollection', - features: experiences - .filter((e) => e.context.location.coordinates) - .map((e) => ({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [ - e.context.location.coordinates!.lon, - e.context.location.coordinates!.lat, - ], - }, - properties: { - id: e.id, - name: e.context.location.name, - type: e.experience.type, - }, - })), - }; - - assertEquals(geojson.type, 'FeatureCollection'); - assertEquals(geojson.features.length, 1); - assertEquals(geojson.features[0].geometry.type, 'Point'); - assertEquals(geojson.features[0].geometry.coordinates, [-122.42, 37.77]); -}); - -Deno.test('Export - DOT graph format generation', () => { - const connections = { - 'electronics': ['art', 'robotics'], - 'art': ['sculpture'], - }; - - let dot = 'graph LearningNetwork {\n'; - for (const [from, toList] of Object.entries(connections)) { - for (const to of toList) { - dot += ` "${from}" -- "${to}";\n`; - } - } - dot += '}'; - - assert(dot.includes('graph LearningNetwork')); - assert(dot.includes('"electronics" -- "art"')); - assert(dot.includes('"electronics" -- "robotics"')); - assert(dot.includes('"art" -- "sculpture"')); -}); - -Deno.test('Export - Markdown format generation', () => { - const journey = [ - { - id: '1', - timestamp: '2025-01-01T10:00:00Z', - experience: { - type: 'workshop', - domain: ['electronics'], - description: 'First workshop', - }, - }, - { - id: '2', - timestamp: '2025-01-05T14:00:00Z', - experience: { - type: 'project', - domain: ['electronics', 'art'], - description: 'Built sculpture', - }, - }, - ]; - - let md = '# Learner Journey\n\n'; - journey.forEach((exp) => { - md += `## ${new Date(exp.timestamp).toLocaleDateString()}\n\n`; - md += `**Type**: ${exp.experience.type}\n\n`; - md += `**Domains**: ${exp.experience.domain.join(', ')}\n\n`; - md += `${exp.experience.description}\n\n`; - md += '---\n\n'; - }); - - assert(md.includes('# Learner Journey')); - assert(md.includes('**Type**: workshop')); - assert(md.includes('**Domains**: electronics, art')); - assert(md.includes('Built sculpture')); -}); - -Deno.test('Export - CSV escaping special characters', () => { - const text = 'Description with "quotes" and, commas'; - - // CSV escaping: wrap in quotes and double internal quotes - const escaped = `"${text.replace(/"/g, '""')}"`; - - assertEquals(escaped, '"Description with ""quotes"" and, commas"'); - assert(escaped.startsWith('"')); - assert(escaped.endsWith('"')); -}); diff --git a/tests/mapper.test.ts b/tests/mapper.test.ts deleted file mode 100644 index de91121..0000000 --- a/tests/mapper.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Mapper functionality tests - * Tests pattern detection, hotspots, networks, and journeys - */ - -import { assert, assertEquals } from '@std/assert'; - -Deno.test('Mapper - Hotspot detection', () => { - const experiences = [ - { - id: '1', - context: { location: { name: 'Makerspace A', coordinates: [0, 0] } }, - experience: { domain: ['electronics'] }, - }, - { - id: '2', - context: { location: { name: 'Makerspace A', coordinates: [0, 0] } }, - experience: { domain: ['woodworking'] }, - }, - { - id: '3', - context: { location: { name: 'Garden B', coordinates: [1, 1] } }, - experience: { domain: ['gardening'] }, - }, - ]; - - // Group by location - const byLocation = experiences.reduce((acc, exp) => { - const loc = exp.context.location.name; - if (!acc[loc]) acc[loc] = []; - acc[loc].push(exp); - return acc; - }, {} as Record); - - assertEquals(byLocation['Makerspace A'].length, 2); - assertEquals(byLocation['Garden B'].length, 1); - - // Find hotspots (2+ experiences) - const hotspots = Object.entries(byLocation).filter(([_, exps]) => - exps.length >= 2 - ); - assertEquals(hotspots.length, 1); - assertEquals(hotspots[0][0], 'Makerspace A'); -}); - -Deno.test('Mapper - Domain network generation', () => { - const experiences = [ - { id: '1', experience: { domain: ['electronics', 'art'] } }, - { id: '2', experience: { domain: ['art', 'sculpture'] } }, - { id: '3', experience: { domain: ['electronics', 'robotics'] } }, - ]; - - // Build adjacency map - const connections: Record> = {}; - - experiences.forEach((exp) => { - const domains = exp.experience.domain; - for (let i = 0; i < domains.length; i++) { - for (let j = i + 1; j < domains.length; j++) { - const [a, b] = [domains[i], domains[j]].sort(); - if (!connections[a]) connections[a] = new Set(); - connections[a].add(b); - } - } - }); - - assert(connections['art']?.has('electronics')); - assert(connections['art']?.has('sculpture')); - assert(connections['electronics']?.has('robotics')); - assertEquals(connections['art']?.size, 2); -}); - -Deno.test('Mapper - Learner journey tracking', () => { - const experiences = [ - { - id: '1', - timestamp: '2025-01-01T10:00:00Z', - learner: { id: 'alice' }, - experience: { type: 'workshop' }, - }, - { - id: '2', - timestamp: '2025-01-05T14:00:00Z', - learner: { id: 'alice' }, - experience: { type: 'project' }, - }, - { - id: '3', - timestamp: '2025-01-03T12:00:00Z', - learner: { id: 'alice' }, - experience: { type: 'mentorship' }, - }, - ]; - - // Filter by learner - const aliceExps = experiences.filter((e) => e.learner.id === 'alice'); - assertEquals(aliceExps.length, 3); - - // Sort chronologically - const journey = aliceExps.sort((a, b) => - a.timestamp.localeCompare(b.timestamp) - ); - assertEquals(journey[0].experience.type, 'workshop'); - assertEquals(journey[1].experience.type, 'mentorship'); - assertEquals(journey[2].experience.type, 'project'); -}); - -Deno.test('Mapper - Interdisciplinary connections', () => { - const experiences = [ - { - id: '1', - learner: { id: 'alice' }, - experience: { domain: ['electronics', 'art'] }, - }, - { - id: '2', - learner: { id: 'bob' }, - experience: { domain: ['gardening', 'food-justice'] }, - }, - ]; - - // Find interdisciplinary experiences (2+ domains) - const interdisciplinary = experiences.filter((e) => - e.experience.domain.length >= 2 - ); - assertEquals(interdisciplinary.length, 2); - - // Find unique connections - const allConnections = interdisciplinary.flatMap((e) => { - const domains = e.experience.domain; - const pairs = []; - for (let i = 0; i < domains.length; i++) { - for (let j = i + 1; j < domains.length; j++) { - pairs.push([domains[i], domains[j]].sort().join('→')); - } - } - return pairs; - }); - - assertEquals(allConnections.length, 2); - assert(allConnections.includes('art→electronics')); - assert(allConnections.includes('food-justice→gardening')); -}); - -Deno.test('Mapper - Diversity score calculation', () => { - // Location with high domain diversity - const highDiversity = [ - { domain: ['electronics'] }, - { domain: ['woodworking'] }, - { domain: ['textiles'] }, - { domain: ['sculpture'] }, - ]; - - // Location with low domain diversity - const lowDiversity = [ - { domain: ['electronics'] }, - { domain: ['electronics'] }, - { domain: ['electronics'] }, - ]; - - const uniqueDomainsHigh = new Set(highDiversity.flatMap((e) => e.domain)); - const uniqueDomainsLow = new Set(lowDiversity.flatMap((e) => e.domain)); - - assertEquals(uniqueDomainsHigh.size, 4); - assertEquals(uniqueDomainsLow.size, 1); - - // Diversity ratio - const diversityHigh = uniqueDomainsHigh.size / highDiversity.length; - const diversityLow = uniqueDomainsLow.size / lowDiversity.length; - - assert(diversityHigh > diversityLow); -}); diff --git a/tests/privacy.test.ts b/tests/privacy.test.ts deleted file mode 100644 index e0427e5..0000000 --- a/tests/privacy.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Privacy and anonymization tests - * Tests PII removal, location fuzzing, and data sanitization - */ - -import { assert, assertEquals } from '@std/assert'; -import { crypto } from '@std/crypto'; - -Deno.test('Privacy - Learner ID anonymization', async () => { - const learnerId = 'alice@example.com'; - - // Hash the ID - const encoder = new TextEncoder(); - const data = encoder.encode(learnerId); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join( - '', - ); - - // Verify hash is deterministic - const hashBuffer2 = await crypto.subtle.digest('SHA-256', data); - const hashArray2 = Array.from(new Uint8Array(hashBuffer2)); - const hashHex2 = hashArray2.map((b) => b.toString(16).padStart(2, '0')).join( - '', - ); - - assertEquals(hashHex, hashHex2, 'Hash should be deterministic'); - assert(hashHex.length === 64, 'SHA-256 hash should be 64 chars'); - assert(!hashHex.includes('@'), 'Hash should not contain original PII'); -}); - -Deno.test('Privacy - Location fuzzing', () => { - const preciseCoords = { lat: 37.7749295, lon: -122.4194155 }; - - // Round to ~1km precision (2 decimal places) - const fuzzed = { - lat: Math.round(preciseCoords.lat * 100) / 100, - lon: Math.round(preciseCoords.lon * 100) / 100, - }; - - assertEquals(fuzzed.lat, 37.77); - assertEquals(fuzzed.lon, -122.42); - - // Verify precision loss - const latDiff = Math.abs(preciseCoords.lat - fuzzed.lat); - const lonDiff = Math.abs(preciseCoords.lon - fuzzed.lon); - - assert(latDiff < 0.01, 'Lat fuzzing within 1km'); - assert(lonDiff < 0.01, 'Lon fuzzing within 1km'); -}); - -Deno.test('Privacy - PII removal from text', () => { - const text = - 'Contact alice@example.com or call 555-123-4567 for more info about Jane Doe'; - - // Remove emails - const withoutEmail = text.replace(/[\w.-]+@[\w.-]+\.\w+/g, '[EMAIL]'); - assert(!withoutEmail.includes('@example.com')); - assert(withoutEmail.includes('[EMAIL]')); - - // Remove phone numbers - const withoutPhone = withoutEmail.replace( - /\d{3}-\d{3}-\d{4}/g, - '[PHONE]', - ); - assert(!withoutPhone.includes('555-123-4567')); - assert(withoutPhone.includes('[PHONE]')); - - // Note: Name removal is complex (requires NLP), just verify detection - assert(text.includes('Jane Doe')); -}); - -Deno.test('Privacy - Privacy level enforcement', () => { - const experience = { - id: 'exp-001', - learner: { id: 'alice@example.com', name: 'Alice Johnson' }, - context: { - location: { - name: 'Mission Makerspace', - coordinates: { lat: 37.7749295, lon: -122.4194155 }, - }, - }, - experience: { description: 'Met with mentor Bob Smith' }, - privacy: 'private' as 'private' | 'anonymous' | 'public', - }; - - // Apply privacy level - const sanitized = applyPrivacy(experience); - - if (experience.privacy === 'private') { - // Private: exclude from exports - assertEquals(sanitized, null); - } -}); - -function applyPrivacy(exp: any) { - if (exp.privacy === 'private') return null; - - if (exp.privacy === 'anonymous') { - return { - ...exp, - learner: { id: '[ANONYMOUS]', name: undefined }, - context: { - ...exp.context, - location: { - ...exp.context.location, - coordinates: undefined, - }, - }, - }; - } - - return exp; -} - -Deno.test('Privacy - Data minimization', () => { - const fullData = { - id: 'exp-001', - timestamp: '2025-01-01T10:00:00Z', - learner: { - id: 'alice', - name: 'Alice', - email: 'alice@example.com', - phone: '555-1234', - }, - context: { - location: { name: 'Makerspace' }, - weather: 'sunny', - mood: 'excited', - }, - experience: { type: 'workshop' }, - }; - - // WHO/WHERE/WHAT minimal protocol - const minimal = { - id: fullData.id, - timestamp: fullData.timestamp, - learner: { id: fullData.learner.id }, - context: { location: fullData.context.location }, - experience: fullData.experience, - }; - - // Verify only essential data retained - assertEquals(Object.hasOwn(minimal.learner, 'email'), false); - assertEquals(Object.hasOwn(minimal.learner, 'phone'), false); - assertEquals(Object.hasOwn(minimal.context, 'weather'), false); -}); - -Deno.test('Privacy - Shareable dataset generation', () => { - const experiences = [ - { id: '1', privacy: 'public', data: 'public-data' }, - { id: '2', privacy: 'anonymous', data: 'anon-data' }, - { id: '3', privacy: 'private', data: 'private-data' }, - ]; - - const shareable = experiences.filter((e) => e.privacy !== 'private'); - - assertEquals(shareable.length, 2); - assert(shareable.every((e) => e.privacy !== 'private')); -});