From 226b0b4359b8c63de9b47e28badd9ed1d54ec1d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 10:30:35 +0000 Subject: [PATCH 01/17] =?UTF-8?q?docs(theory):=20add=20correspondence=20mo?= =?UTF-8?q?del=20=E2=80=94=20the=20engine=20that=20classifies,=20not=20tra?= =?UTF-8?q?nslates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canonical design spec for the comprehension/transfer engine: the Concept/Form/Transition object model, the levels-of-objects strata, the Dyadic+Echo formal carrier, and the Rosetta false-friends taxonomy (cognate / false-friend / antonym / alien-realization / novel / vanished) as graded Echo fibres. Also records the engine<->cartridge division of labour, the overlay-protocol / accessibility / PanLL-emit downstream contracts, and the knot-theory honesty caveat. JTV grammar v2 is stubbed pending repo access. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- docs/theory/CORRESPONDENCE-MODEL.adoc | 237 ++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/theory/CORRESPONDENCE-MODEL.adoc diff --git a/docs/theory/CORRESPONDENCE-MODEL.adoc b/docs/theory/CORRESPONDENCE-MODEL.adoc new file mode 100644 index 0000000..b3bd7a0 --- /dev/null +++ b/docs/theory/CORRESPONDENCE-MODEL.adoc @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MPL-2.0 += Correspondence Model: the engine that *classifies*, not translates +:toc: macro +:toclevels: 3 +:icons: font + +[NOTE] +==== +*Status: living design spec.* This is the conceptual foundation for the engine — +authored ahead of the toolchain (AffineScript/Idris2/Zig not yet installable here), so +it is verified by inspection against estate exemplars, not yet by build. One worked +example (JTV grammar v2) is stubbed pending repository access; see <>. +==== + +toc::[] + +== What this engine is (and is not) + +This is *Duolingo / Rosetta Stone for programming languages* — a +language-*comprehension and transfer* engine. It works **one layer above text +editing**, on *syntactic and semantic intention*, so that the effort a person spent +learning language A *transfers* when they move to B (JS → TypeScript, Ruby, Prolog, C, +or exotic targets like QPL, Arrow, JTV). *Transfer learning across languages is the +product.* + +It is *not*: + +* *Not "the next best IDE."* It does not compete on contrast, project-management + widgets, pop-up notepads, shortcuts, or well-trodden attention/memory/workflow + ergonomics. That is **PanLL**'s job (PanLL + eNSAID = the _contact_ between human, + tool, task, and environment). This engine *feeds* PanLL; it is not PanLL. +* *Not a linter.* "Proximal/distal scope-colouring" and "spot the missing `;` / extra + `)`" are precisely what this is *not*. The differentiator is an engine that + **computes and classifies cross-language equivalence**, not one that looks it up. +* *Not a universal translator.* A verified any-language→any-language compiler with full + Curry–Howard fidelity would be nice; it is *not the goal*. We do not need — and will + not attempt — to do this "to the dot" on everything. + +== Division of labour + +[cols="1,3"] +|=== +| *We build* | the general engine + the interface + the classification vocabulary + +the residue model + a reference language pack. +| *The community builds* | the *per-language modules* — which pairs of constructs are +cognate vs. false-friend vs. novel, with their residues. These are authored as +**cartridges** (see `standards/cartridges/`). +|=== + +The engine is language-agnostic. The nextgen-language collection is merely the +substrate we dogfood. + +== The object model: Concept / Form / Transition + +[cols="1,3"] +|=== +| *Concept* | The invariant / equivalence-class — the recurring _trope_ (e.g. +"bind a name to a value", "iterate a collection", "fail recoverably"). +| *Form* | A representative of a Concept *in one language* (e.g. `let` in ReScript, +`=` in Erlang, `def` in Python). A Concept has many Forms across many languages. +| *Transition* | A directed correspondence Form(A) → Form(B): the Echo-refined map +plus its *residue* (what is lost, added, or inverted in crossing). +|=== + +A *lesson* is a Concept presented through the learner's known Form, the target Form, +and the Transition between them — narrated without shame ("you already know this; +here's the catch"). + +== Levels of objects (strata) + +Classification runs *per stratum of meaning*, not on flat text: + +. *Surface* — tokens, lexemes (`=`, `def`, `match`). +. *Structure* — AST shape, scoping, arity. +. *Semantic intention* — what the construct _does_ (bind? mutate? branch?). +. *Abstraction / trope* — the Concept it instantiates. +. *Cross-language invariant* — the preserved quantity certifying "same idea". + +The interface lets a learner move *up and down* these strata and *across* view-modes. +A correspondence can hold at one stratum and break at another — and that divergence is +itself the most valuable signal (see false friends, below). + +== The formal carrier + +The model is grounded in the estate's own formalism, not a bespoke mapping table. + +* *Carrier = Dyadic `Relation`* + (`proven-tests-and-benches/src/ProvenTests/TypeSafe/Dyadic.idr`): + `{ relates; reflexive; symmetric; transitive }`. Equivalence = refl ∧ sym ∧ trans — + the symmetries and transitivities, as a checkable structure. +* *Crossings are lossy-with-residue = Echo fibre* + (`Echo f y := Σ (x : A), f x ≡ y`, `hyperpolymath/echo-types`, Agda): the residue + (a proof term) is precisely _what is lost or added_ going A→B. A non-isomorphic + Transition (e.g. the absence of `return` in ReScript) is a map whose residue we + *surface*, never hide. +* *Typed residue-lenses = the three Echo bridges* (`Bridge.idr`): `EchoChoreo` + (protocol/sequencing), `EchoEpistemic` (visibility/knowledge), `EchoTropical` + (cost, min-plus). Extensible. +* *Composition = transitivity; symmetry distinguishes iso from retraction.* A clean + isomorphism ⇒ no residue (true equivalence); a retraction ⇒ one-way loss + (residue = the delta). +* *`invariant-path` = the governance front-end.* It anchors each equivalence _claim_ to + its two code locations plus a witness (proof/test), human-in-the-loop and editable. + +[IMPORTANT] +==== +*Honesty note on "knot theory."* The owner's framing — *types → carrier · tropes → +recurring equivalence-figures · knot theory → certificate* — is load-bearing for +intuition, but there is *no literal knot-invariant computation* here, and the +knot-theory lens is flagged (by the owner) as "perceived as rhetoric, pending +historical confirmation". We use *invariant* in the precise sense of a _preserved +quantity that certifies "same idea"_, and keep knot theory as an aspirational lens, not +a faked dependency. +==== + +[#taxonomy] +== The Rosetta false-friends taxonomy + +The one judgement that matters is *"is this safe to transfer, or is it a trap?"* +`def` → `define` might be _nothing but a rename_ — or it might look like that and not +be. So the Dyadic relation is not a boolean `relates`; it is a *typed/graded* +`CorrespondenceKind`, realised as grades of the Echo fibre: + +[cols="1,3,2"] +|=== +| Kind | Signature | Pedagogy + +| *Cognate / true friend* +| Intention + behaviour coincide; residue ≈ ∅. _Example:_ `def` → `define`. + But "just a rename" is a hypothesis to *verify*, never assume. +| *Transfer directly.* + +| *False friend / homonym* +| Surface matches, semantics diverge. _Example:_ BASIC `=` (mutable assignment) vs. + Erlang `=` (single-assignment bind/unify). Detected by *per-stratum* classification: + surface-corresponds ∧ semantics-diverge **is** the false-friend signature. +| *Flag the trap.* + +| *Antonym / inverted* +| Related but behaves oppositely. _Examples:_ 0- vs. 1-indexing; truthiness + conventions; stack-growth direction. Residue = the flip. +| *Remap the intuition.* + +| *Alien realization* +| Same intention, foreign mechanism, large residue. _Example:_ subtraction in a + reversible/add-only language (JTV) is `add` run backwards; divide via reversed + repeated-add. Also: recursion-only languages, CPS, monadic IO for "just print". +| *Bridge with effort; explain the machinery.* + +| *Novel / no anchor* +| Forward fibre empty: `∄ x. f x ≡ y`. The target has a concept with nothing to map + _from_. _Examples:_ static types coming from assembly + JS; ownership/borrowing; + affine/linear use-once; Prolog `cut`; JTV totality and information-flow labels. +| *Teach de novo — there is no transfer.* + +| *Vanished* +| Backward fibre empty: `∄ y`. A concept the learner relied on is _gone_. + _Examples:_ `return` in ReScript; `null` in a null-free language; mutable variables + in a pure language. +| *Un-learn / re-route.* +|=== + +The residue therefore ranges from ∅ (true iso) through inverted/large to +empty-in-either-direction (novel / vanished). This taxonomy *is* the Duolingo/Rosetta +pedagogy: transfer cognates, warn on false friends, remap antonyms, bridge the alien, +teach the novel, re-route the vanished. + +== Engine ↔ cartridge boundary + +We cannot do this "to the dot", and we do not try. Responsibilities split cleanly: + +* *Engine (ours):* the `CorrespondenceKind` vocabulary, the residue/fibre model, the + per-stratum classifier, the Dyadic/Echo carrier, and the certifiable Idris2/Echo math + — applied *where it pays* (high-value or dangerous correspondences), not everywhere. +* *Cartridge (per-language, community):* the _facts_ — for this language, which Forms + instantiate which Concepts, and for each Transition the kind + residue + a witness. + +A cartridge fact, sketched: + +[source] +---- +(transition + (concept "name-binding") + (from (lang "basic") (form "X = 5")) + (to (lang "erlang") (form "X = 5")) + (kind false-friend) ; surface-cognate, semantic-divergent + (strata (surface corresponds) (semantic diverges)) + (residue "BASIC rebinds destructively; Erlang binds once and unifies — \ + re-`=` fails unless the value matches") + (witness "proofs/erlang_single_assignment.idr")) ; optional, where it pays +---- + +== Rendering: "levels of objects", not chrome + +The view-modes are the rendered strata, switchable and non-destructive (each conforms +to the estate *overlay-protocol* — additive, non-modifying, idempotent, Idris2-ABI +proved): + +* *focus* — show only what's needed now; +* *glyph* — Makaton-style symbols (accessible, low-cognitive-load); +* *blockly / scratch* — flowchart of structure; +* *raw code* — the text itself; +* *side-by-side* — multi-language diff with step-sync. + +Accessibility is a first-class contract, not a coat of paint: Hyperpolymath +Accessibility Standard, Level *A* minimum → *AA* (keyboard-only operation, ≥4.5:1 +contrast, colourblind-safe palettes, ARIA/screen-reader, reduced-motion, plain-language +mode, high-contrast glyph variants). + +== Downstream: feeding PanLL + +The engine *emits*, it does not own the IDE. Analyses become `octads` written to +VeriSimDB (`:8097`) plus `Groove` signals; PanLL panels subscribe and re-render. +Relevant octad types: `inline-annotation` (per-construct claims), +`ecosystem-link` (intra-program relationships), `proof-status` (where a Transition +carries a witness). Ergonomics are read from `.machine_readable/ENSAID_CONFIG.a2ml`. + +[#open-questions] +== Open questions / placeholders + +* *JTV grammar v2* — the canonical worked example for *alien-realization* and + *novel/no-anchor* kinds. The `julia-the-viper` repo is a not-checked-out submodule + and currently outside session scope; constructs to be tagged against <> + once the grammar is available. From the T1 proof spec, JTV is reversible + total + + effect-typed (I/O, state, crypto) + information-flow-typed (public/private/secret) + + capability-secured — i.e. dense with novel-concept correspondences. +* *Stratum granularity* — whether 5 strata suffice, or "intention" needs splitting + (operational vs. denotational). +* *Where the math pays* — the policy for when a Transition _must_ carry an Idris2/Echo + witness vs. an asserted-but-unproven classification. + +== References + +* `proven-tests-and-benches/src/ProvenTests/TypeSafe/Dyadic.idr`, `…/Bridge.idr` +* `hyperpolymath/echo-types` — loss-with-residue (Agda) +* `standards/overlay-protocol/`, `standards/ensaid-config/`, + `standards/accessibility/`, `standards/cartridges/`, `standards/panll-panels/` From ddd3d4444282b9d669b45b821c996c0f04b68e08 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 11:02:31 +0000 Subject: [PATCH 02/17] chore(standards): complete 6a2 manifest set + panic-attack gate + eNSAID config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add the missing .machine_readable/6a2 manifests — AGENTIC, NEUROSYM, PLAYBOOK — completing the canonical six alongside STATE/META/ECOSYSTEM, tuned to the correspondence-engine identity (no-shame voice, classify-not- translate, toolchain-deferred build honesty, no-relicense guardrail). - Add .github/workflows/static-analysis-gate.yml: panic-attack assail + hypatia scan + patch-bridge triage. Fails only on critical findings and skips gracefully when the binaries are unavailable in the runner. - Add .machine_readable/ENSAID_CONFIG.a2ml: PanLL / eNSAID per-repo config with a correspondence-engine view-layer portfolio. - Justfile: fix stale panic-attacker -> panic-attack references (tool renamed 2026-02-08). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .github/workflows/static-analysis-gate.yml | 400 +++++++++++++++++++++ .machine_readable/6a2/AGENTIC.a2ml | 43 +++ .machine_readable/6a2/NEUROSYM.a2ml | 33 ++ .machine_readable/6a2/PLAYBOOK.a2ml | 31 ++ .machine_readable/ENSAID_CONFIG.a2ml | 65 ++++ Justfile | 4 +- 6 files changed, 574 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/static-analysis-gate.yml create mode 100644 .machine_readable/6a2/AGENTIC.a2ml create mode 100644 .machine_readable/6a2/NEUROSYM.a2ml create mode 100644 .machine_readable/6a2/PLAYBOOK.a2ml create mode 100644 .machine_readable/ENSAID_CONFIG.a2ml diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml new file mode 100644 index 0000000..8bd1606 --- /dev/null +++ b/.github/workflows/static-analysis-gate.yml @@ -0,0 +1,400 @@ +# SPDX-License-Identifier: MPL-2.0 +# Static Analysis Gate — Required by branch protection rules. +# Runs panic-attack and hypatia, deposits findings for gitbot-fleet learning. +name: Static Analysis Gate +on: + pull_request: + branches: ['**'] + push: + branches: [main, master] +permissions: + contents: read +jobs: + # --------------------------------------------------------------------------- + # Job 1: panic-attack assail + # --------------------------------------------------------------------------- + panic-attack-assail: + name: panic-attack assail + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Install panic-attack (if available) + id: install + run: | + # Try to fetch the latest release binary from the org + PA_URL="https://github.com/hyperpolymath/panic-attack/releases/latest/download/panic-attack-linux-x86_64" + if curl -fsSL --head "$PA_URL" >/dev/null 2>&1; then + curl -fsSL -o /usr/local/bin/panic-attack "$PA_URL" + chmod +x /usr/local/bin/panic-attack + echo "installed=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::panic-attack binary not available — skipping assail" + echo "installed=false" >> "$GITHUB_OUTPUT" + fi + - name: Run panic-attack assail + id: assail + if: steps.install.outputs.installed == 'true' + run: | + set +e + panic-attack assail --format json . > panic-attack-findings.json 2>&1 + PA_EXIT=$? + set -e + + if [ ! -s panic-attack-findings.json ]; then + echo "[]" > panic-attack-findings.json + fi + + # Parse finding counts + TOTAL=$(jq '. | length' panic-attack-findings.json 2>/dev/null || echo 0) + CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' panic-attack-findings.json 2>/dev/null || echo 0) + HIGH=$(jq '[.[] | select(.severity == "high")] | length' panic-attack-findings.json 2>/dev/null || echo 0) + MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' panic-attack-findings.json 2>/dev/null || echo 0) + LOW=$(jq '[.[] | select(.severity == "low")] | length' panic-attack-findings.json 2>/dev/null || echo 0) + + echo "total=$TOTAL" >> "$GITHUB_OUTPUT" + echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT" + echo "high=$HIGH" >> "$GITHUB_OUTPUT" + echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" + echo "low=$LOW" >> "$GITHUB_OUTPUT" + echo "exit_code=$PA_EXIT" >> "$GITHUB_OUTPUT" + - name: Emit check annotations + if: steps.install.outputs.installed == 'true' + run: | + # Convert JSON findings into GitHub Actions annotations + jq -r '.[] | select(.file != null) | + if .severity == "critical" then + "::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)" + elif .severity == "high" then + "::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)" + else + "::warning file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)" + end + ' panic-attack-findings.json || true + - name: Write step summary + if: steps.install.outputs.installed == 'true' + run: | + cat <> "$GITHUB_STEP_SUMMARY" + ## panic-attack assail Results + + | Severity | Count | + |----------|-------| + | Critical | ${{ steps.assail.outputs.critical }} | + | High | ${{ steps.assail.outputs.high }} | + | Medium | ${{ steps.assail.outputs.medium }} | + | Low | ${{ steps.assail.outputs.low }} | + | **Total**| ${{ steps.assail.outputs.total }} | + EOF + - name: Create stub findings (when panic-attack unavailable) + if: steps.install.outputs.installed != 'true' + run: | + echo "[]" > panic-attack-findings.json + echo "## panic-attack assail" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" + - name: Upload panic-attack findings + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: panic-attack-findings + path: panic-attack-findings.json + retention-days: 90 + - name: Fail on critical findings + if: steps.install.outputs.installed == 'true' && steps.assail.outputs.critical > 0 + run: | + echo "::error::panic-attack found ${{ steps.assail.outputs.critical }} critical issue(s) — blocking merge" + exit 1 + # --------------------------------------------------------------------------- + # Job 2: hypatia-scan + # --------------------------------------------------------------------------- + hypatia-scan: + name: Hypatia neurosymbolic scan + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Setup Elixir for Hypatia scanner + id: beam + continue-on-error: true + uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.18.2 + with: + elixir-version: '1.19.4' + otp-version: '28.3' + - name: Clone and build Hypatia + id: build + continue-on-error: true + run: | + git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" 2>/dev/null || true + if [ -f "$HOME/hypatia/mix.exs" ]; then + cd "$HOME/hypatia" + # Build escript if neither hypatia nor hypatia-v2 exists + if [ ! -f hypatia ] && [ ! -f hypatia-v2 ]; then + mix deps.get + mix escript.build + fi + echo "ready=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::Hypatia scanner not available — skipping scan" + echo "ready=false" >> "$GITHUB_OUTPUT" + fi + - name: Run Hypatia scan + id: scan + if: steps.build.outputs.ready == 'true' + run: | + set +e + HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1 + HYP_EXIT=$? + set -e + + if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then + echo "[]" > hypatia-findings.json + fi + + TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) + CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json 2>/dev/null || echo 0) + HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json 2>/dev/null || echo 0) + MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' hypatia-findings.json 2>/dev/null || echo 0) + LOW=$(jq '[.[] | select(.severity == "low")] | length' hypatia-findings.json 2>/dev/null || echo 0) + + echo "total=$TOTAL" >> "$GITHUB_OUTPUT" + echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT" + echo "high=$HIGH" >> "$GITHUB_OUTPUT" + echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" + echo "low=$LOW" >> "$GITHUB_OUTPUT" + - name: Emit check annotations + if: steps.build.outputs.ready == 'true' + run: | + jq -r '.[] | select(.file != null) | + if .severity == "critical" then + "::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + elif .severity == "high" then + "::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + else + "::warning file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + end + ' hypatia-findings.json || true + - name: Write step summary + if: steps.build.outputs.ready == 'true' + run: | + cat <> "$GITHUB_STEP_SUMMARY" + ## Hypatia Scan Results + + | Severity | Count | + |----------|-------| + | Critical | ${{ steps.scan.outputs.critical }} | + | High | ${{ steps.scan.outputs.high }} | + | Medium | ${{ steps.scan.outputs.medium }} | + | Low | ${{ steps.scan.outputs.low }} | + | **Total**| ${{ steps.scan.outputs.total }} | + EOF + - name: Create stub findings (when Hypatia unavailable) + if: steps.build.outputs.ready != 'true' + run: | + echo "[]" > hypatia-findings.json + echo "## Hypatia Scan" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Skipped: Hypatia scanner not available in this environment." >> "$GITHUB_STEP_SUMMARY" + - name: Upload hypatia findings + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: hypatia-findings + path: hypatia-findings.json + retention-days: 90 + - name: Fail on critical security findings + if: steps.build.outputs.ready == 'true' && steps.scan.outputs.critical > 0 + run: | + echo "::error::Hypatia found ${{ steps.scan.outputs.critical }} critical security issue(s) — blocking merge" + exit 1 + # --------------------------------------------------------------------------- + # Job 3: patch-bridge triage (CVE contextual assessment) + # --------------------------------------------------------------------------- + patch-bridge-triage: + name: Patch Bridge CVE triage + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Install panic-attack (if available) + id: install + run: | + PA_URL="https://github.com/hyperpolymath/panic-attack/releases/latest/download/panic-attack-linux-x86_64" + if curl -fsSL --head "$PA_URL" >/dev/null 2>&1; then + curl -fsSL -o /usr/local/bin/panic-attack "$PA_URL" + chmod +x /usr/local/bin/panic-attack + echo "installed=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::panic-attack binary not available — skipping Patch Bridge" + echo "installed=false" >> "$GITHUB_OUTPUT" + fi + - name: Run Patch Bridge triage + id: triage + if: steps.install.outputs.installed == 'true' + run: | + set +e + panic-attack bridge triage --format json . > bridge-report.json 2>&1 + PB_EXIT=$? + set -e + + if [ ! -s bridge-report.json ] || ! jq empty bridge-report.json 2>/dev/null; then + echo '{"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > bridge-report.json + fi + + UNMITIGABLE=$(jq '.unmitigable // 0' bridge-report.json) + MITIGATED=$(jq '.mitigated // 0' bridge-report.json) + CONCATENATIVE=$(jq '.concatenative // 0' bridge-report.json) + INFORMATIONAL=$(jq '.informational // 0' bridge-report.json) + + echo "unmitigable=$UNMITIGABLE" >> "$GITHUB_OUTPUT" + echo "mitigated=$MITIGATED" >> "$GITHUB_OUTPUT" + echo "concatenative=$CONCATENATIVE" >> "$GITHUB_OUTPUT" + echo "informational=$INFORMATIONAL" >> "$GITHUB_OUTPUT" + - name: Write step summary + if: steps.install.outputs.installed == 'true' + run: | + cat <> "$GITHUB_STEP_SUMMARY" + ## Patch Bridge CVE Triage + + | Classification | Count | + |----------------|-------| + | Unmitigable | ${{ steps.triage.outputs.unmitigable }} | + | Mitigated | ${{ steps.triage.outputs.mitigated }} | + | Concatenative | ${{ steps.triage.outputs.concatenative }} | + | Informational | ${{ steps.triage.outputs.informational }} | + + Unmitigable CVEs require dependency replacement or rearchitecture. + Mitigated CVEs have active controls with soundness proofs. + Concatenative risks are CVE combinations that multiply severity. + EOF + - name: Create stub report (when unavailable) + if: steps.install.outputs.installed != 'true' + run: | + echo '{"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > bridge-report.json + echo "## Patch Bridge CVE Triage" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" + - name: Upload bridge report + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: bridge-report + path: bridge-report.json + retention-days: 90 + - name: Fail on unmitigable CVEs in critical paths + if: steps.install.outputs.installed == 'true' && steps.triage.outputs.unmitigable > 0 + run: | + echo "::warning::Patch Bridge found ${{ steps.triage.outputs.unmitigable }} unmitigable CVE(s) — review required" + # Warning only, not blocking. Unmitigable means the developer needs + # to make an architectural decision, not that the PR is wrong. + # --------------------------------------------------------------------------- + # Job 4: deposit-findings (combines + archives for gitbot-fleet) + # --------------------------------------------------------------------------- + deposit-findings: + name: Deposit findings for gitbot-fleet + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: [panic-attack-assail, hypatia-scan, patch-bridge-triage] + if: always() + steps: + - name: Download panic-attack findings + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + name: panic-attack-findings + path: findings/ + - name: Download hypatia findings + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + name: hypatia-findings + path: findings/ + - name: Download bridge report + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + name: bridge-report + path: findings/ + - name: Combine findings into unified report + id: combine + run: | + PA_FILE="findings/panic-attack-findings.json" + HYP_FILE="findings/hypatia-findings.json" + + # Ensure both files exist and are valid JSON arrays + for f in "$PA_FILE" "$HYP_FILE"; do + if [ ! -s "$f" ] || ! jq empty "$f" 2>/dev/null; then + echo "[]" > "$f" + fi + done + + # Tag each finding with its source scanner + jq '[.[] | . + {"scanner": "panic-attack"}]' "$PA_FILE" > /tmp/pa-tagged.json + jq '[.[] | . + {"scanner": "hypatia"}]' "$HYP_FILE" > /tmp/hyp-tagged.json + + # Read bridge report (CVE triage, not findings array) + BRIDGE_FILE="findings/bridge-report.json" + if [ ! -s "$BRIDGE_FILE" ] || ! jq empty "$BRIDGE_FILE" 2>/dev/null; then + echo '{"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > "$BRIDGE_FILE" + fi + + # Build unified report envelope + jq -n \ + --arg repo "${{ github.repository }}" \ + --arg sha "${{ github.sha }}" \ + --arg ref "${{ github.ref }}" \ + --arg run_id "${{ github.run_id }}" \ + --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --slurpfile pa /tmp/pa-tagged.json \ + --slurpfile hyp /tmp/hyp-tagged.json \ + --slurpfile bridge "$BRIDGE_FILE" \ + '{ + schema_version: "1.1.0", + repository: $repo, + commit_sha: $sha, + ref: $ref, + run_id: $run_id, + timestamp: $ts, + findings: ($pa[0] + $hyp[0]), + patch_bridge: $bridge[0] + }' > findings/unified-findings.json + + TOTAL=$(jq '.findings | length' findings/unified-findings.json) + CRITICAL=$(jq '[.findings[] | select(.severity == "critical")] | length' findings/unified-findings.json) + HIGH=$(jq '[.findings[] | select(.severity == "high")] | length' findings/unified-findings.json) + MEDIUM=$(jq '[.findings[] | select(.severity == "medium")] | length' findings/unified-findings.json) + LOW=$(jq '[.findings[] | select(.severity == "low")] | length' findings/unified-findings.json) + + echo "total=$TOTAL" >> "$GITHUB_OUTPUT" + echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT" + echo "high=$HIGH" >> "$GITHUB_OUTPUT" + echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" + echo "low=$LOW" >> "$GITHUB_OUTPUT" + - name: Upload unified findings (fleet scanner picks these up) + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: unified-findings + path: findings/unified-findings.json + retention-days: 90 + - name: Write deposit summary + run: | + cat <> "$GITHUB_STEP_SUMMARY" + ## Unified Findings Deposit + + **Repository:** ${{ github.repository }} + **Commit:** \`${{ github.sha }}\` + **Deposited at:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") + + | Severity | Count | + |----------|-------| + | Critical | ${{ steps.combine.outputs.critical }} | + | High | ${{ steps.combine.outputs.high }} | + | Medium | ${{ steps.combine.outputs.medium }} | + | Low | ${{ steps.combine.outputs.low }} | + | **Total**| ${{ steps.combine.outputs.total }} | + + Findings saved as \`unified-findings\` artifact. + The gitbot-fleet scanner will ingest these on its next pass. + EOF diff --git a/.machine_readable/6a2/AGENTIC.a2ml b/.machine_readable/6a2/AGENTIC.a2ml new file mode 100644 index 0000000..433bdc4 --- /dev/null +++ b/.machine_readable/6a2/AGENTIC.a2ml @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# AGENTIC.a2ml — AI agent constraints and capabilities +[metadata] +version = "0.1.0" +last-updated = "2026-06-18" +project = "nextgen-languages-evangeliser" + +[agent-permissions] +can-edit-source = true +can-edit-tests = true +can-edit-docs = true +can-edit-config = true +can-create-files = true + +[agent-constraints] +# What AI agents must NOT do in this repo: +# - Never relicense. This is a sole-owner repo; keep SPDX = MPL-2.0 on every +# file. No bulk SPDX sweeps (estate licence guardrail). +# - Never use banned languages (TypeScript, Python, Go, V) or Makefiles (use +# the Justfile). +# - Never reframe this as a universal translator or a Curry-Howard-fidelity +# compiler. It *classifies* cross-language correspondences and teaches the +# transfer; see docs/theory/CORRESPONDENCE-MODEL.adoc. +# - Never shame the source language. Narratives keep the +# celebrate / minimise / better / safety / example voice. +# - Never claim a green build from an untooled environment. AffineScript, +# Idris2, Zig and Deno may be absent here — author now, verify in CI. +# - Never place state files in the repository root (must live in +# .machine_readable/). + +[maintenance-integrity] +fail-closed = true +require-evidence-per-step = true +allow-silent-skip = false +require-rerun-after-fix = true +release-claim-requires-hard-pass = true + +[automation-hooks] +# on-enter: Read 0-AI-MANIFEST.a2ml, then .machine_readable/6a2/STATE.a2ml +# on-exit: Update 6a2/STATE.a2ml with session outcomes +# on-commit: Run `just validate-rsr` diff --git a/.machine_readable/6a2/NEUROSYM.a2ml b/.machine_readable/6a2/NEUROSYM.a2ml new file mode 100644 index 0000000..d33f7f5 --- /dev/null +++ b/.machine_readable/6a2/NEUROSYM.a2ml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# NEUROSYM.a2ml — Neurosymbolic integration metadata +[metadata] +version = "0.1.0" +last-updated = "2026-06-18" +project = "nextgen-languages-evangeliser" + +[hypatia-config] +scan-enabled = true +scan-depth = "standard" # quick | standard | deep +report-format = "logtalk" + +[static-analysis-gate] +# panic-attack assail runs in .github/workflows/static-analysis-gate.yml. +# It fails the build only on critical findings and skips gracefully when the +# binary is unavailable in the runner. +panic-attack-enabled = true +fail-on = "critical" + +[symbolic-rules] +# Project-specific symbolic rules (illustrative): +# - { name = "no-shame-narrative", pattern = "stupid|wrong code|bad code", severity = "high" } +# - { name = "no-universal-translator-claim", pattern = "universal translator|curry-?howard fidelity", severity = "medium" } + +[neural-config] +# The engine's own correspondence classifier IS this repo's domain +# neurosymbolic surface: a symbolic Dyadic relation + a graded Echo +# loss-with-residue fibre -> a typed CorrespondenceKind +# (cognate / false-friend / antonym / alien-realization / novel / vanished). +# See docs/theory/CORRESPONDENCE-MODEL.adoc. +# confidence-threshold = 0.85 diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/6a2/PLAYBOOK.a2ml new file mode 100644 index 0000000..f1ee58a --- /dev/null +++ b/.machine_readable/6a2/PLAYBOOK.a2ml @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# PLAYBOOK.a2ml — Operational playbook +[metadata] +version = "0.1.0" +last-updated = "2026-06-18" +project = "nextgen-languages-evangeliser" + +[deployment] +# Host: ReScript today (compiles to JS, run under Deno); AffineScript after the +# Phase-3 host port. Surfaces: Deno CLI (bin/evangeliser.js) and the browser +# GUI (gui/server.js + gui/app.js). +# method = "ci-triggered" +# target = "library + cli + browser-gui" + +[incident-response] +# 1. Read .machine_readable/6a2/STATE.a2ml for current status. +# 2. Review recent commits and CI (Static Analysis Gate, RSR Compliance). +# 3. Run `just validate-rsr` to check compliance. +# 4. Run `just assail` for the panic-attack static-analysis scan. + +[release-process] +# 1. Update version in 6a2/STATE.a2ml and 6a2/META.a2ml. +# 2. Run `just validate-rsr` and `just crg-grade`. +# 3. Tag and push. + +[maintenance-operations] +# RSR validation: just validate-rsr +# Static-analysis scan: just assail +# Component readiness: just crg-grade diff --git a/.machine_readable/ENSAID_CONFIG.a2ml b/.machine_readable/ENSAID_CONFIG.a2ml new file mode 100644 index 0000000..3bfbd3f --- /dev/null +++ b/.machine_readable/ENSAID_CONFIG.a2ml @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# ENSAID_CONFIG.a2ml — eNSAID Environment Configuration +# Per-repo configuration for PanLL and eNSAID-compatible tools. +# Spec: https://github.com/hyperpolymath/standards/tree/main/ensaid-config +# +# This engine *emits into* PanLL (octads -> VeriSimDB -> panels) and renders its +# "levels of objects" through overlay-protocol view-layers. See +# docs/theory/CORRESPONDENCE-MODEL.adoc, "Rendering" and "Downstream: feeding PanLL". + +# ───────────────────────────────────────────────────────────────── +# [ensaid] — Core eNSAID identity and version +# ───────────────────────────────────────────────────────────────── +[ensaid] +version = "1.0.0" +tool = "panll" + +# ───────────────────────────────────────────────────────────────── +# [workspace] — Workspace mode, protection, and execution policy +# ───────────────────────────────────────────────────────────────── +[workspace] +mode = "rhodium" # rhodium | gold | silver | bronze +protection = "open" # open | guarded | locked +execution = "live" # live | dry-run | approval-required + +# ───────────────────────────────────────────────────────────────── +# [preferences] — Display and behaviour preferences +# ───────────────────────────────────────────────────────────────── +[preferences] +humidity = "medium" # high | medium | low (drift aura intensity) +default-arrangement = "correspondence-3-panel" +auto-connect = true + +# ───────────────────────────────────────────────────────────────── +# [panels] — View-layers render the engine's "levels of objects": +# focus | glyph | blockly | raw-code | side-by-side (overlay-protocol). +# ───────────────────────────────────────────────────────────────── +[panels] +version = "1.0.0" + +# ───────────────────────────────────────────────────────────────── +# [workflows] — Automation Router event-driven cross-panel rules +# ───────────────────────────────────────────────────────────────── +[workflows] +version = "1.0.0" + +# ───────────────────────────────────────────────────────────────── +# [clades] — Panel clade trait/capability overrides +# ───────────────────────────────────────────────────────────────── +[clades] +version = "1.0.0" + +# ───────────────────────────────────────────────────────────────── +# [portfolios] — Custom panel bundles for this repo's workflow +# ───────────────────────────────────────────────────────────────── +[portfolios] +version = "1.0.0" + +[[portfolios.custom]] +id = "correspondence-dev" +name = "Correspondence Engine Development" +description = "Cross-language correspondence panes: side-by-side + residue + glyph + focus" +panels = ["raw-code", "side-by-side", "glyph", "focus"] +default-isolation = "native" diff --git a/Justfile b/Justfile index a7caa0e..ae32a1f 100644 --- a/Justfile +++ b/Justfile @@ -237,9 +237,9 @@ help-me: @echo "" @echo "Include the output of 'just doctor' in your report." -# Run panic-attacker pre-commit scan +# Run panic-attack pre-commit scan assail: - @command -v panic-attack >/dev/null 2>&1 && panic-attack assail . || echo "panic-attack not found — install from https://github.com/hyperpolymath/panic-attacker" + @command -v panic-attack >/dev/null 2>&1 && panic-attack assail . || echo "panic-attack not found — install from https://github.com/hyperpolymath/panic-attack" # Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) From 734c4b058448c9d7d0d88986c5522c1da26fd132 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 11:45:23 +0000 Subject: [PATCH 03/17] docs(identity): re-point repo to the correspondence (classify-not-translate) engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring the public + machine-readable identity into line with the merged correspondence-model spec (docs/theory/CORRESPONDENCE-MODEL.adoc): - README.adoc, EXPLAINME.adoc, ROADMAP.adoc — Duolingo/Rosetta-Stone-for-PLs framing: a comprehension+transfer engine that *classifies* cross-language correspondences (Concept/Form/Transition + six CorrespondenceKinds), not an IDE / linter / universal translator. Engine-vs-cartridge split; feeds PanLL; no-shame transfer-first pedagogy; HAS accessibility; toolchain honesty. - CLAUDE.md, .claude/CLAUDE.md — corrected overview + notes; policy tables preserved (Deno/Justfile/Zig/Idris2; ReScript banned in new code). - .machine_readable/{STATE,META,ECOSYSTEM}.a2ml (+ 6a2/ copies) — corrected identity/purpose/ADRs/related-projects; filled empty + corrupted fields. Licence labels left untouched (no relicensing): the pre-existing PMPL badge, "Palimpsest License (MPL-2.0)" text, and ROADMAP "MPL-2.0-or-later" are flagged to the owner, not edited. No SPDX identifiers changed. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .claude/CLAUDE.md | 11 +- .machine_readable/6a2/ECOSYSTEM.a2ml | 16 +- .machine_readable/6a2/META.a2ml | 12 +- .machine_readable/6a2/STATE.a2ml | 32 +-- .machine_readable/ECOSYSTEM.a2ml | 21 +- .machine_readable/META.a2ml | 18 +- .machine_readable/STATE.a2ml | 49 +++-- CLAUDE.md | 239 ++++++--------------- EXPLAINME.adoc | 259 ++++++++++++++++++++-- README.adoc | 308 ++++++++++++++------------- ROADMAP.adoc | 123 ++++++----- 11 files changed, 645 insertions(+), 443 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 22c75e6..a6b7e7b 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,8 +2,15 @@ ## Project: Nextgen Languages Evangeliser -Flagship target: **AffineScript**. Legacy target: **ReScript**. More targets planned (Rust, Gleam, Zig). -Current host application language: ReScript (migration to AffineScript planned in Phase 3). +**Duolingo / Rosetta Stone for programming languages** — a language-*comprehension and transfer* engine that **classifies** cross-language correspondences (it does not translate, lint, or build IDEs) so learning transfers from a known language to a new one. Canonical design: `docs/theory/CORRESPONDENCE-MODEL.adoc`. + +- **Not** an IDE (that is **PanLL + eNSAID**; this engine *feeds* PanLL), **not** a linter, **not** a universal translator (no Curry–Howard-fidelity goal), **not** a shame-the-JS pattern matcher. +- **Model:** Concept / Form / Transition; six `CorrespondenceKind`s (cognate / false-friend / antonym / alien-realization / novel / vanished) as graded Echo fibres; classification runs per stratum. Carrier = Dyadic relation + Echo loss-with-residue (`proven-tests-and-benches` + `echo-types`). +- **Division of labour:** we build the engine + interface + vocabulary; the community authors per-language **cartridges** (`standards/cartridges/`). + +Host: **ReScript today** (legacy host, being migrated; banned in *new* code per estate policy → AffineScript `.affine`). **AffineScript** is the future host and a first-class target (Zig FFI + Idris2 ABI seams). + +When working here: classify-don't-translate; keep the no-shame voice; lead with the correspondence model (AffineScript is one first-class target/host, not the sole pitch); author-now / verify-in-CI when the toolchain is absent; **MPL-2.0 SPDX on new files, never relicense or sweep SPDX (licence-label drift is FLAG-ONLY to the owner)**. ## Language Policy (Hyperpolymath Standard) diff --git a/.machine_readable/6a2/ECOSYSTEM.a2ml b/.machine_readable/6a2/ECOSYSTEM.a2ml index b11c904..846d1a2 100644 --- a/.machine_readable/6a2/ECOSYSTEM.a2ml +++ b/.machine_readable/6a2/ECOSYSTEM.a2ml @@ -3,18 +3,24 @@ # # ECOSYSTEM.a2ml — Nextgen Languages Evangeliser ecosystem position [metadata] -version = "1.0" -last-updated = "2026-04-11" +version = "1.2" +last-updated = "2026-06-18" [project] name = "Nextgen Languages Evangeliser" -purpose = "image:https://img.shields.io/badge/license-Palimpsest--MPL--1.0-purple.svg[Palim" +purpose = "Duolingo/Rosetta-Stone for programming languages — a comprehension+transfer engine that classifies cross-language correspondences; emits into PanLL; extended by community language cartridges." role = "project" [position-in-ecosystem] -category = "" +category = "application" +description = "Cross-language correspondence engine; classify-not-translate; feeds PanLL" [related-projects] projects = [ - # No related projects recorded + "hyperpolymath/panll — downstream IDE / contact layer this engine feeds (+ eNSAID)", + "hyperpolymath/proven-tests-and-benches — Dyadic relation carrier", + "hyperpolymath/echo-types — loss-with-residue (Agda)", + "hyperpolymath/standards — cartridges, overlay-protocol, ensaid-config, accessibility", + "hyperpolymath/affinescript — first-class target + future host", + "hyperpolymath/typed-wasm — AffineScript codegen layer", ] diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/6a2/META.a2ml index 36ae5e3..5883426 100644 --- a/.machine_readable/6a2/META.a2ml +++ b/.machine_readable/6a2/META.a2ml @@ -2,10 +2,9 @@ # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # META.a2ml — Nextgen Languages Evangeliser meta-level information -# Renamed from Rescript Evangeliser on 2026-04-16 [metadata] -version = "1.0.0" -last-updated = "2026-04-11" +version = "1.1.0" +last-updated = "2026-06-18" [project-info] license = "MPL-2.0" @@ -13,7 +12,12 @@ author = "Jonathan D.A. Jewell (hyperpolymath)" [architecture-decisions] decisions = [ - # No ADRs recorded + "ADR-1 Classify, don't translate — name the CorrespondenceKind + residue; no universal translator, no Curry-Howard-fidelity goal", + "ADR-2 Carrier = Dyadic relation + Echo loss-with-residue (proven-tests-and-benches + echo-types); invariant-path is the governance front-end", + "ADR-3 Engine vs cartridge — we build the engine/interface/vocabulary; per-language facts are cartridges (standards/cartridges)", + "ADR-4 Host ReScript -> AffineScript (Zig FFI + Idris2 ABI seams); ReScript banned in new code", + "ADR-5 Emit into PanLL (octads -> VeriSimDB -> panels); not itself the IDE", + "ADR-6 Knot-theory framing is an aspirational lens, not a literal computation", ] [development-practices] diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/6a2/STATE.a2ml index c04e05d..e0ab148 100644 --- a/.machine_readable/6a2/STATE.a2ml +++ b/.machine_readable/6a2/STATE.a2ml @@ -4,35 +4,43 @@ # STATE.a2ml — Nextgen Languages Evangeliser project state [metadata] project = "nextgen-languages-evangeliser" -version = "1.0.0" -last-updated = "2026-01-10T13:50:33+00:00" +version = "1.2.0" +last-updated = "2026-06-18" status = "active" -session = "converted from scheme — 2026-04-11" [project-context] name = "Nextgen Languages Evangeliser" -purpose = """""" -completion-percentage = 50 +purpose = "Duolingo/Rosetta-Stone for programming languages — a comprehension+transfer engine that classifies cross-language correspondences so learning transfers from a known language to a new one." +completion-percentage = 58 [position] -phase = "Active Development" # design | implementation | testing | maintenance | archived -maturity = "experimental" # experimental | alpha | beta | production | lts +phase = "Re-point: correspondence engine + abstraction-model pivot" # design | implementation | testing | maintenance | archived +maturity = "alpha" # experimental | alpha | beta | production | lts [route-to-mvp] milestones = [ - # No milestones recorded + "M0 correspondence-model spec — complete", + "M1 standards/repo hygiene — complete", + "M2 identity re-point + abstraction-model pivot — in progress", + "M3 browser multi-pane GUI + overlay view-layers — pending", + "M4 AffineScript host port (Zig FFI + Idris2 ABI) — gated", + "M5 cartridge contract + 2nd language pack — pending", + "M6 proofs/benches + PanLL octad emission — pending", ] [blockers-and-issues] issues = [ - # No blockers recorded + "AffineScript/Idris2/Zig/Deno toolchain absent in some environments — author now, verify in CI", + "JTV grammar-v2 worked example stubbed pending julia-the-viper repo access", + "Licence-label drift to flag to owner vs MPL-2.0 SPDX — FLAG-ONLY, manual", ] [critical-next-actions] actions = [ - # No actions recorded + "Abstraction-model pivot: Concept/Form/Transition + CorrespondenceKind in src", + "Browser multi-pane GUI surface", ] [maintenance-status] -last-run-utc = "2026-01-10T13:50:33+00:00T00:00:00Z" -last-result = "unknown" # unknown | pass | warn | fail +last-run-utc = "2026-06-18T00:00:00Z" +last-result = "pass" # unknown | pass | warn | fail diff --git a/.machine_readable/ECOSYSTEM.a2ml b/.machine_readable/ECOSYSTEM.a2ml index d368e8d..badfe41 100644 --- a/.machine_readable/ECOSYSTEM.a2ml +++ b/.machine_readable/ECOSYSTEM.a2ml @@ -2,26 +2,33 @@ ;; ECOSYSTEM.a2ml — nextgen-languages-evangeliser ecosystem position ;; Migrated from root ECOSYSTEM.scm on 2026-04-12 ;; Renamed from rescript-evangeliser on 2026-04-16 +;; Updated for the correspondence re-point on 2026-06-18 (ecosystem - (version "1.1") + (version "1.2") (name "nextgen-languages-evangeliser") (predecessor "rescript-evangeliser") (type "project") - (purpose "Nextgen typed-language evangelism — educational materials, examples, and tools for adopting AffineScript (flagship), ReScript (legacy), and future targets") + (purpose "Duolingo/Rosetta-Stone for programming languages — a comprehension+transfer engine that classifies cross-language correspondences so learning transfers from a known language to a new one") (position-in-ecosystem (role "component") (layer "application") - (description "Multi-target advocacy and resource hub for next-generation typed languages within the hyperpolymath estate")) + (description "Cross-language correspondence engine; classify-not-translate; emits into PanLL; extended by community language cartridges")) (related-projects + (downstream-ide "hyperpolymath/panll (+ eNSAID contact layer this engine feeds)") + (carrier-relation "hyperpolymath/proven-tests-and-benches (Dyadic.idr)") + (carrier-residue "hyperpolymath/echo-types (loss-with-residue, Agda)") + (cartridge-home "hyperpolymath/standards (cartridges, overlay-protocol, ensaid-config, accessibility)") (flagship-target "hyperpolymath/affinescript") (codegen-layer "hyperpolymath/typed-wasm") (legacy-target "rescript-lang/rescript")) - (what-this-is "Educational materials, worked examples, and advocacy tools for affine/linear-typed and other next-generation languages (flagship: AffineScript)") + (what-this-is "A language-comprehension and transfer engine: it classifies cross-language correspondences (cognate/false-friend/antonym/alien-realization/novel/vanished) and teaches the transfer without shame") (what-this-is-not - "A compiler" - "A type-checker" - "A language-server (see affinescript repo for LSP)")) + "An IDE (that is PanLL)" + "A linter" + "A universal translator (no Curry-Howard-fidelity goal)" + "A compiler or type-checker" + "A shame-the-JavaScript pattern matcher")) diff --git a/.machine_readable/META.a2ml b/.machine_readable/META.a2ml index 406ddf1..94a53cb 100644 --- a/.machine_readable/META.a2ml +++ b/.machine_readable/META.a2ml @@ -1,15 +1,25 @@ ;; SPDX-License-Identifier: MPL-2.0 ;; META.a2ml — nextgen-languages-evangeliser meta-information ;; Migrated from root META.scm on 2026-04-12 +;; Updated for the correspondence re-point on 2026-06-18 (meta - (version "1.0.0") + (version "1.1.0") (project "nextgen-languages-evangeliser") - (architecture-decisions ()) + (architecture-decisions + (adr-1 "Classify, don't translate — the engine names the kind of cross-language correspondence (CorrespondenceKind) and its residue; it is not a universal translator and claims no Curry-Howard fidelity") + (adr-2 "Formal carrier = Dyadic relation + Echo loss-with-residue (proven-tests-and-benches Dyadic.idr + hyperpolymath/echo-types); invariant-path is the governance front-end") + (adr-3 "Engine vs cartridge — we build the general engine, interface and vocabulary; per-language facts are authored as cartridges (standards/cartridges)") + (adr-4 "Host moves ReScript -> AffineScript with Zig FFI + Idris2 ABI seams; ReScript banned in new code per estate policy") + (adr-5 "The engine emits into PanLL (octads -> VeriSimDB -> panels); it is not itself the IDE") + (adr-6 "Knot-theory framing is an aspirational lens, not a literal computation — documented honestly")) (development-practices (code-style "standard") - (security "openssf-scorecard") + (security "openssf-scorecard + panic-attack static-analysis-gate") (versioning "semver") (documentation "asciidoc") (branching "trunk-based")) - (design-rationale ())) + (design-rationale + (no-shame "celebrate / minimise / better / safety / example — transfer-first pedagogy") + (levels-of-objects "classification runs per stratum: surface -> structure -> intention -> trope -> invariant") + (accessibility "Hyperpolymath Accessibility Standard, Level A -> AA"))) diff --git a/.machine_readable/STATE.a2ml b/.machine_readable/STATE.a2ml index 2a3c6ab..c252f0e 100644 --- a/.machine_readable/STATE.a2ml +++ b/.machine_readable/STATE.a2ml @@ -2,41 +2,52 @@ ;; STATE.a2ml — nextgen-languages-evangeliser project state ;; Migrated from root STATE.scm on 2026-04-12 ;; Renamed from rescript-evangeliser on 2026-04-16 +;; Re-pointed to the correspondence (classify-not-translate) model on 2026-06-18 (state (metadata - (version "1.1.0") + (version "1.2.0") (project "nextgen-languages-evangeliser") (predecessor "rescript-evangeliser") - (last-updated "2026-04-16")) + (last-updated "2026-06-18")) + + (identity + (is "Duolingo/Rosetta-Stone for programming languages — a comprehension+transfer engine that classifies cross-language correspondences so learning transfers from a known language to a new one") + (is-not "an IDE (that is PanLL), a linter, a universal translator, or a shame-the-JS pattern matcher") + (model "Concept/Form/Transition + six CorrespondenceKinds (cognate/false-friend/antonym/alien-realization/novel/vanished) as graded Echo fibres") + (division "we build the engine + interface + vocabulary; the community authors per-language cartridges")) (current-position - (phase "Migration: Phase 1 — rebrand + engine generalisation") - (overall-completion 55) + (phase "Re-point: cross-language correspondence engine + abstraction-model pivot") + (overall-completion 58) (working-features - "ReScript target catalogue (52 patterns, 21 categories)" - "Scanner + Analyser detection engine" - "Narrative generation with 'You were close!' voice" - "Makaton-inspired glyph system" + "Correspondence-model design spec (docs/theory/CORRESPONDENCE-MODEL.adoc) — merged" + "Standards/machine-readable hygiene (6a2 manifests, panic-attack gate, eNSAID config) — merged" + "Scanner + Analyser detection engine (ReScript host)" + "Narrative generation (no-shame voice)" + "Makaton-inspired glyph view-layer" "CLI: scan / patterns / legend / stats" "RAW/FOLDED/GLYPHED view layers; plain/markdown/html outputs")) (route-to-mvp (milestones - (phase-0 (status "complete") (items "Decide pivot strategy" "Verify AffineScript toolchain")) - (phase-1 (status "in-progress") (items "Rebrand docs+metadata" "Generalise Types.res for multi-target")) - (phase-2 (status "pending") (items "Author AffineScript pattern catalogue (affine/linear safety)")) - (phase-3 (status "gated") (items "Host port ReScript -> AffineScript" "Gate: AffineScript->typed-wasm->WASM proof-of-life")) - (phase-4 (status "pending") (items "Policy perimeter flip" "ts-blocker + .res exception for legacy")) - (phase-5 (status "pending") (items "Additional targets: Rust, Gleam, Zig")))) + (m0 (status "complete") (items "Correspondence-model spec: Concept/Form/Transition + six kinds + Dyadic/Echo carrier")) + (m1 (status "complete") (items "Standards/repo hygiene: 6a2 manifests, panic-attack gate, eNSAID config")) + (m2 (status "in-progress") (items "Identity re-point" "Abstraction-model pivot: Concept/Form/Transition + CorrespondenceKind in src")) + (m3 (status "pending") (items "Browser multi-pane GUI + overlay view-layers + HAS accessibility")) + (m4 (status "gated") (items "AffineScript host port (.res -> .affine) + Zig FFI + Idris2 ABI seams")) + (m5 (status "pending") (items "Cartridge contract + 2nd language pack")) + (m6 (status "pending") (items "Idris2/Echo witnesses + benches + PanLL octad emission")))) (blockers-and-issues - "AffineScript WASM codegen backend parity incomplete (Phase 3 gate)" - "GitHub repo rename still needs manual action by maintainer") + "AffineScript/Idris2/Zig/Deno toolchain absent in some environments — author now, verify in CI" + "JTV grammar-v2 worked example stubbed pending julia-the-viper repo access" + "Licence-label drift to flag to owner (PMPL badge / 'Palimpsest License (MPL-2.0)' / ROADMAP 'MPL-2.0-or-later') vs MPL-2.0 SPDX — FLAG-ONLY, manual") (critical-next-actions - "Complete Phase 1a rebrand commit" - "Complete Phase 1b engine generalisation (Types.res -> multi-target)") + "Abstraction-model pivot: recast Types/Patterns/Narrative/Analyser to Concept/Form/Transition + CorrespondenceKind" + "Browser multi-pane GUI surface") (session-history - (2026-04-16 "Phase 0 decisions locked; Phase 1 started; branch claude/migrate-to-affinescript-zig-U0iYX"))) + (2026-04-16 "Phase 0 decisions locked; Phase 1 started") + (2026-06-18 "Re-point: correspondence-model spec merged (#19); standards hygiene merged (#20); identity re-point"))) diff --git a/CLAUDE.md b/CLAUDE.md index a488c17..2367d39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,209 +2,108 @@ ## Project Overview -**Nextgen Languages Evangeliser** is a pattern detection engine, CLI, and educational toolkit that teaches JavaScript developers next-generation type-safe languages through progressive code transformation — without shame. +**Nextgen Languages Evangeliser** is **Duolingo / Rosetta Stone for programming languages** — a language-*comprehension and transfer* engine. It works one layer above text editing, on *syntactic and semantic intention*, and **classifies** cross-language correspondences so the effort spent learning one language **transfers** to the next. *Transfer learning across languages is the product.* -**Flagship target:** AffineScript (affine/linear type system, borrow checking, quantity checking, WASM backend). +Canonical design: `docs/theory/CORRESPONDENCE-MODEL.adoc` — this file mirrors it. The repo began as *ReScript Evangeliser*; ReScript is now a legacy host (being migrated) and a legacy target. -**Supported targets:** AffineScript (flagship), ReScript (legacy), with more to follow (Rust, Gleam, Zig being the likely next additions). +### What it is NOT +- **Not an IDE** — that is **PanLL + eNSAID** (the *contact* between human, tool, task, environment). This engine *feeds* PanLL; it is not PanLL. +- **Not a linter** — not scope-colouring or spotting a missing `;` / extra `)`. +- **Not a universal translator** — no Curry–Howard-fidelity goal ("nice, not the point"). +- **Not a shame-the-JavaScript pattern matcher.** -This project is the successor to *ReScript Evangeliser*. The ReScript pattern library is preserved as a legacy target; active pattern authorship now centres on affine/linear safety concerns teachable from idiomatic JavaScript. +## The Model: Concept / Form / Transition -## Project Purpose +- **Concept** — the invariant / equivalence-class (the recurring *trope*). +- **Form** — a representative of a Concept in one language (`let` in ReScript, `=` in Erlang, `def` in Python). +- **Transition** — a directed Form(A)→Form(B) correspondence plus its *residue* (what is lost, added, or inverted). -This repository serves as a resource hub for: -- Educational materials about next-generation type-safe languages (AffineScript first) -- Pattern catalogues showing equivalent transformations from JavaScript to target languages -- Tools that detect amenable patterns in existing JS and encourage adoption -- Community content that evangelises type-safe coding without shaming incumbent code +Classification is a **graded `CorrespondenceKind`**, not a boolean — six grades of the Echo fibre, each with a pedagogy: +**cognate** (transfer) · **false-friend** (warn) · **antonym** (remap) · **alien-realization** (bridge) · **novel / no-anchor** (teach de novo) · **vanished** (re-route). Classification runs *per stratum* (surface → structure → intention → trope → invariant). -## Technology Stack +Carrier = **Dyadic relation + Echo loss-with-residue** (`proven-tests-and-benches` `Dyadic.idr` + `hyperpolymath/echo-types`); `invariant-path` is the governance front-end. "Knot theory" is an aspirational lens, **not** a literal computation. -### Core Technologies -- **AffineScript** — flagship target language (teaching subject) -- **typed-wasm** — separate repo; AffineScript's downstream codegen layer (AffineScript → typed-wasm → WASM) -- **ReScript** — current host application language (during Phase 1/2 of migration), legacy target -- **Deno** — runtime & package management (replaces Node/npm/bun) -- **Nickel** — configuration language -- **Zig** — FFI bridge (canonical, per `0-AI-MANIFEST.a2ml`) +## Engine vs. Cartridge -### Language Policy (Hyperpolymath Standard) +- **We build** the general engine + interface + classification vocabulary + residue model + a reference language pack. +- **The community builds** per-language modules as **cartridges** (`standards/cartridges/`). + +The engine is language-agnostic; the nextgen-language collection is merely the substrate we dogfood. + +## Downstream -**ALLOWED:** -- AffineScript (application code, once Phase 3 port completes) -- ReScript (current application code; demoted to legacy target after Phase 3) -- Deno (runtime & package management) -- Zig (FFI, systems code) -- Bash/POSIX Shell (scripts, automation) -- JavaScript (only where ReScript/AffineScript cannot — minimal glue code) -- Nickel (configuration) +Emits `octads` → VeriSimDB (`:8097`) + Groove signals → **PanLL** panels; view-layers conform to the estate **overlay-protocol**; ergonomics read `.machine_readable/ENSAID_CONFIG.a2ml`; accessibility meets the **Hyperpolymath Accessibility Standard** (Level A → AA). -**BANNED:** -- TypeScript (use AffineScript or ReScript) -- Node.js (use Deno) -- npm/bun (use Deno) -- Makefile (use Justfile) -- V (use Zig, except inside V-ecosystem-specific projects) +## Technology Stack + +- **ReScript** — current / legacy host (being ported; banned in *new* code per estate policy: `.res` → `.affine`) +- **AffineScript** — future host + first-class target (Zig FFI + Idris2 ABI seams; OCaml 5.1+; emits typed-wasm IR → WebAssembly) +- **Deno** — runtime & package management (not npm/bun) +- **Zig** — FFI layer; **Idris2** — ABI / proofs +- **Nickel** — configuration -### Key Features to Evangelise +### Language Policy (Hyperpolymath Standard) -Per target, the pitch differs. The engine is target-aware: +**ALLOWED:** AffineScript (future host), ReScript (legacy host/target), Deno, Zig (FFI), Idris2 (ABI/proofs), Bash/POSIX shell, JavaScript (glue only — see exemptions in `.claude/CLAUDE.md`), Nickel. -- **AffineScript (flagship):** affine/linear types, use-at-most-once guarantees, borrow checking, quantity type theory, compile-time resource safety, WASM deployment. -- **ReScript (legacy):** sound type inference, Option/Result, pattern matching, pipe operator, zero-cost JS interop. +**BANNED:** TypeScript, Node.js, npm/bun, Makefile (use Justfile), V (outside the V ecosystem), Python, Go. **ReScript is banned in _new_ code** — migrate `.res` → `.affine` directly. ## Project Structure ``` nextgen-languages-evangeliser/ -├── src/ # Application source (ReScript today, AffineScript post-Phase 3) -│ ├── Types.res # Core type model — multi-target pattern representation -│ ├── Glyphs.res # Makaton-inspired glyph system (target-agnostic) -│ ├── Narrative.res # "You were close!" narrative generation (target-aware) -│ ├── Patterns.res # Pattern library (multi-target) -│ ├── Scanner.res # Regex pattern detection engine -│ ├── Analyser.res # Result aggregation and reporting -│ ├── Output.res # RAW/FOLDED/GLYPHED rendering (multi-target) -│ └── Cli.res # CLI entry point +├── src/ # Host source (ReScript today; AffineScript target) +│ ├── Types.res # Core type model (→ Concept / Form / Transition) +│ ├── Glyphs.res # Makaton-inspired glyph view-layer +│ ├── Narrative.res # Shame-free narrative (per CorrespondenceKind) +│ ├── Patterns.res # Correspondence catalogue (→ reference language pack) +│ ├── Scanner.res # Detection engine +│ ├── Analyser.res # Classification + aggregation +│ ├── Output.res # focus/glyph/blockly/raw/side-by-side view-layers +│ └── Cli.res # CLI entry point (offline fallback) +├── gui/ # Browser multi-pane workspace (primary surface) +├── docs/theory/CORRESPONDENCE-MODEL.adoc # Canonical design spec ├── test/ # Test suites -├── docs/ # Documentation -├── rescript.json # Host build config (current) -├── deno.json # Deno configuration ├── Justfile # Task orchestration (NOT Makefile) -├── Mustfile.epx # Deployment contract -└── config.ncl # Nickel configuration +└── deno.json # Deno configuration ``` -## Development Guidelines - -### Code Style -- Follow the host language's official style guide (ReScript today; AffineScript post-Phase 3) -- Use meaningful names; prefer pattern matching over if/else -- Leverage the type system to make invalid states unrepresentable -- Write interface files (.resi) for public APIs -- Add SPDX license headers to all source files - -### Best Practices -1. **Type-First Design** — Design types before implementation -2. **Immutability** — Prefer immutable data structures -3. **Pure Functions** — Write pure functions when possible -4. **Documentation** — Document complex type definitions and public APIs -5. **Error Handling** — Use Result and Option types (not exceptions) - ## Common Commands ```bash -# Install dependencies -just install - -# Build the project -just build - -# Watch mode for development -just watch - -# Clean build artifacts -just clean - -# Run tests -just test - -# Validate project structure and policy -just validate - -# Format code -just fmt - -# Full validation -just validate-rsr +just install # Install dependencies (Deno) +just build # Build host sources +just watch # Watch mode +just test # Run tests +just assail # panic-attack static-analysis scan +just validate-rsr # RSR compliance +just fmt # Format ``` -## Build System - -This project uses: -- **Deno**: For build scripts and task running -- **justfile**: For task orchestration (NOT Makefile) -- **ReScript**: Current host compiler (bsb) -- **AffineScript**: Planned host compiler post-Phase 3 (OCaml 5.1+, Dune 3.14+; emits typed-wasm IR, which compiles to WASM) - -## Dependencies - -### Runtime -- Deno (latest stable) -- ReScript 12.2+ (current host) -- @rescript/core -- AffineScript compiler (planned, Phase 3+) - -### Package Management -- **Primary**: Guix (guix.scm) -- **Fallback**: Nix (flake.nix) -- **JS deps**: Deno (deno.json imports) - -## Evangelism Goals - -1. **Demonstrate Value** — Show concrete benefits of each supported target -2. **Lower Barriers** — Make adoption as easy as possible -3. **Share Success Stories** — Document real-world wins -4. **Build Community** — Foster a welcoming environment -5. **Create Resources** — Provide learning materials and tools - -## Philosophy: "Celebrate Good, Minimize Bad, Show Better" - -We **never** shame developers. Instead: - -1. **Celebrate** — Recognize what their JavaScript does well -2. **Minimize** — Gently acknowledge minor limitations -3. **Better** — Show how the target language enhances the pattern -4. **Safety** — Explain type-level (and affine/linear) guarantees -5. **Example** — Provide concrete, encouraging examples - -## Migration Status (ReScript → AffineScript) - -This repo is mid-migration from ReScript Evangeliser to Nextgen Languages Evangeliser. Phases: - -- **Phase 0** ✅ — Decide (option A content rewrite, multilang future, rename repo, toolchain verified) -- **Phase 1** 🚧 — Rebrand + generalise engine for multi-target (host stays ReScript) -- **Phase 2** — Pattern catalogue pivot: affine/linear-safety-focused AffineScript patterns -- **Phase 3** — Host language port ReScript → AffineScript (gated on toolchain WASM maturity) -- **Phase 4** — Policy perimeter flip (ts-blocker extended, affinescript linter added) -- **Phase 5** — Zig/V policy text formalisation +## Philosophy: no shame, transfer-first -## Resources +We **never** shame developers. The voice stays **celebrate / minimise / better / safety / example**. The six kinds map to: transfer cognates, warn on false friends, remap antonyms, bridge the alien, teach the novel, re-route the vanished. -### AffineScript -- Compiler: https://github.com/hyperpolymath/affinescript -- Toolchain: OCaml 5.1+, Dune 3.14+ -- Downstream codegen: typed-wasm (separate repo) → WebAssembly +## Re-point Status (was the ReScript → AffineScript "migration") -### ReScript (legacy target) -- [ReScript Documentation](https://rescript-lang.org/docs) -- [ReScript Forum](https://forum.rescript-lang.org) -- [ReScript GitHub](https://github.com/rescript-lang) +- ✅ Correspondence-model spec merged (Concept/Form/Transition + six kinds + Dyadic/Echo carrier) +- ✅ Standards / repo hygiene merged (6a2 manifests, panic-attack gate, eNSAID config) +- 🚧 Identity re-point + abstraction-model pivot (classify, not translate) +- → Browser multi-pane GUI (overlay view-layers) +- → AffineScript host port (Zig FFI + Idris2 ABI) +- → Cartridge contract + second language pack +- → Proofs/benches + PanLL octad emission ## Notes for Claude -When working on this project: -- Prioritize type safety and correctness -- Suggest idiomatic patterns for whichever target is being discussed -- Treat AffineScript as the flagship target; ReScript as a well-supported legacy target -- When adding patterns, lead with the AffineScript transformation and keep ReScript as secondary -- Help maintain clear documentation -- Encourage best practices in evangelism materials -- **Use Deno, not npm/bun** -- **Use Justfile, not Makefile** -- **Use Zig for FFI, not V (except inside V-ecosystem projects)** -- **Source host language: ReScript during Phases 1-2, AffineScript from Phase 3 onward** -- Add SPDX license headers to new source files - -## Project Philosophy - -**Make next-generation languages approachable**: The goal is to help developers discover and adopt affine/linear type systems and other nextgen features by: -- Showing practical examples from code they already write -- Addressing common concerns -- Demonstrating real benefits -- Providing migration paths -- Building confidence through education +- Lead with the **correspondence model**; AffineScript is one first-class target/host, not the sole pitch. +- **Classify, don't translate.** No universal-translator or Curry–Howard-fidelity claims. +- **No shame.** Keep the celebrate / minimise / better / safety / example voice. +- **Use Deno, not npm/bun. Use Justfile, not Makefile. Use Zig for FFI, Idris2 for ABI/proofs.** +- **ReScript is banned in new code** → AffineScript (`.affine`). +- Toolchain may be absent in this environment — **author now, verify in CI**; never claim a green build you did not run. +- **Licence: MPL-2.0 (sole-owner repo). Add SPDX `MPL-2.0` to new files. NEVER relicense or bulk-sweep SPDX; licence-label drift is FLAG-ONLY to the owner.** --- -*This document should be updated as the project evolves and new patterns emerge.* +*Keep this in sync with `docs/theory/CORRESPONDENCE-MODEL.adoc` as the engine evolves.* diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index c22e74b..fb6fc0e 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -2,48 +2,269 @@ = Nextgen Languages Evangeliser — EXPLAINME -== Celebrate good, minimize bad, show better +== Learn — to love it! -[quote, README.adoc] +[quote, docs/theory/CORRESPONDENCE-MODEL.adoc] ____ -A pattern detection engine, CLI, and educational toolkit that teaches JavaScript developers next-generation type-safe languages through progressive code transformation, without shame. +Duolingo / Rosetta Stone for programming languages — a language-comprehension and transfer engine that works one layer above text editing, on syntactic and semantic intention, so that the effort a person spent learning language A transfers when they move to B. ____ -The evangeliser detects patterns in existing JavaScript amenable to transformation, then shows equivalent code in one or more target languages — with encouragement rather than criticism. -It uses progressive disclosure (RAW, FOLDED, GLYPHED views) and Makaton-inspired glyphs to make concepts visual. -See link:README.adoc[] for the full pattern catalogue and quick start. +This is the long-form "why". For the precise, canonical version, read +link:docs/theory/CORRESPONDENCE-MODEL.adoc[docs/theory/CORRESPONDENCE-MODEL.adoc] — +this document mirrors it and may not lead it. -**Current state:** this repo is the successor to *ReScript Evangeliser* and is mid-migration. -The ReScript catalogue (originally 52 patterns across 21 categories) is preserved as a legacy target. -Active pattern authorship has pivoted to *AffineScript*, focusing on affine/linear-safety concerns — use-after-free analogs, double-cleanup, aliasing of owned state, missing disposal. -See link:ROADMAP.adoc[] for migration phases. +=== The one-sentence pitch -**Caveat:** During Phases 1–2 the host language of this toolkit is still ReScript; Phase 3 ports it to AffineScript once the AffineScript → typed-wasm → WASM toolchain is stable enough to carry 2,400+ LOC of production code. +You already learned to program. When you pick up your next language, most of what +you know is still true — but some of it is a *trap*, and a little of it has *no analog +at all*. This engine tells you which is which. *Transfer learning across languages is +the product.* + +=== What it is + +A *language-comprehension and transfer* engine. It works *one layer above text +editing*, on *syntactic and semantic intention* — not characters, not tokens-as-text. +Given a Form you know in language A and a Form in language B, it *computes and +classifies* the correspondence between them, and surfaces the *residue*: what is lost, +added, or inverted in the crossing. That classification is the lesson. + +The substrate we dogfood is the nextgen-language collection (AffineScript and friends), +but the engine is *language-agnostic*: JS → TypeScript, Ruby, Prolog, C, or exotic +targets like QPL, Arrow, JTV. + +=== What it is *not* (stated plainly) + +* *Not "the next best IDE."* Contrast, project widgets, pop-up notepads, shortcuts, + attention/memory/workflow ergonomics — that is the job of *PanLL* (PanLL + eNSAID = + the _contact_ between human, tool, task, and environment). This engine *feeds* PanLL. + It is not PanLL. +* *Not a linter.* It is not about proximal/distal scope-colouring or spotting a missing + `;` / extra `)`. A linter *looks up* rules; this engine *computes and classifies* + cross-language equivalence. +* *Not a universal translator.* A verified any-language→any-language compiler with full + Curry–Howard fidelity would be nice — it is *not the goal*, and we will not attempt it + "to the dot". +* *Not a shame-the-JavaScript pattern matcher.* The voice is celebrate / minimise / + better / safety / example, always. + +=== The model: Concept / Form / Transition + +* *Concept* — the invariant / equivalence-class, the recurring _trope_: "bind a name to + a value", "iterate a collection", "fail recoverably". +* *Form* — a representative of a Concept _in one language_: `let` in ReScript, `=` in + Erlang, `def` in Python. One Concept, many Forms. +* *Transition* — a directed correspondence Form(A) → Form(B): the Echo-refined map plus + its *residue*. + +A *lesson* presents a Concept through the learner's known Form, the target Form, and the +Transition between them — narrated without shame: "you already know this; here's the +catch". + +=== Levels of objects (strata) + +Classification runs *per stratum of meaning*, not on flat text: + +. *Surface* — tokens, lexemes (`=`, `def`, `match`). +. *Structure* — AST shape, scoping, arity. +. *Semantic intention* — what the construct _does_ (bind? mutate? branch?). +. *Abstraction / trope* — the Concept it instantiates. +. *Cross-language invariant* — the preserved quantity certifying "same idea". + +The interface lets a learner move *up and down* the strata and *across* view-modes. A +correspondence can hold at one stratum and break at another — and *that divergence is the +most valuable signal*. It is, literally, the false-friend detector. + +=== The false-friends pedagogy (where it earns its keep) + +The one judgement that matters is *"is this safe to transfer, or is it a trap?"* So the +relation is not a boolean — it is a *graded* `CorrespondenceKind`, one of six, each with +its own teaching move: + +[cols="2,3,2", options="header"] +|=== +| Kind | What it means | Teaching move + +| *Cognate / true friend* +| Intention + behaviour coincide; residue ≈ ∅. _e.g._ `def` → `define`. But "just a + rename" is a hypothesis to *verify*, never assume. +| *Transfer directly.* + +| *False friend / homonym* +| Surface matches, semantics diverge. _e.g._ BASIC `=` (destructive assignment) vs. + Erlang `=` (bind-once / unify). Detected as: surface-corresponds ∧ semantics-diverge. +| *Flag the trap.* + +| *Antonym / inverted* +| Related but behaves oppositely. _e.g._ 0- vs. 1-indexing; truthiness conventions; + stack-growth direction. Residue = the flip. +| *Remap the intuition.* + +| *Alien realization* +| Same intention, foreign mechanism, large residue. _e.g._ in JTV (reversible / add-only) + subtraction is `add` run backwards; divide via reversed repeated-add. Also: recursion- + only languages, CPS, monadic IO just to print. +| *Bridge with effort; explain the machinery.* + +| *Novel / no anchor* +| Forward fibre empty (`∄ x. f x ≡ y`) — the target has a concept with nothing to map + _from_. _e.g._ static types coming from assembly + JS; ownership/borrowing; affine/ + linear use-once; Prolog `cut`; JTV totality and information-flow labels. +| *Teach de novo — there is no transfer.* + +| *Vanished* +| Backward fibre empty (`∄ y`) — a concept the learner relied on is _gone_. _e.g._ + `return` in ReScript; `null` in a null-free language; mutable variables in a pure one. +| *Un-learn / re-route.* +|=== + +The residue ranges from ∅ (true iso) through inverted/large to empty-in-either-direction +(novel / vanished). This taxonomy *is* the Duolingo/Rosetta pedagogy. + +==== A walk through the false friend + +BASIC and Erlang both write `X = 5`. Surface: identical. Structure: both an +infix `=` between a name and a literal. A naïve tool calls them cognate. They are not. + +* In BASIC, `=` *destructively assigns* — `X = 5` then `X = 6` is fine; `X` is now 6. +* In Erlang, `=` *binds once and unifies* — `X = 5` binds `X`; a later `X = 6` is not an + overwrite, it is a *match that fails*. + +The engine sees `surface corresponds` ∧ `semantic diverges` across strata and emits +*false friend*. The lesson does not say "you wrote it wrong". It says: "you already know +`=`; here is the one thing that will bite you." That is the whole product in miniature. + +=== The formal carrier (and an honesty caveat) + +The model is grounded in the estate's own formalism, not a bespoke lookup table: + +* *Carrier = Dyadic `Relation`* (`proven-tests-and-benches` `Dyadic.idr`): + `{ relates; reflexive; symmetric; transitive }`. Equivalence = refl ∧ sym ∧ trans. +* *Crossings are lossy-with-residue = Echo fibre* (`hyperpolymath/echo-types`, Agda): + `Echo f y := Σ (x : A), f x ≡ y`. The residue (a proof term) is exactly _what is lost + or added_ A→B. A retraction (one-way loss) is distinguished from an isomorphism (no + residue) by symmetry. +* *Typed residue-lenses = the three Echo bridges* (`Bridge.idr`): `EchoChoreo` + (protocol/sequencing), `EchoEpistemic` (visibility/knowledge), `EchoTropical` + (cost, min-plus). Extensible. +* *`invariant-path`* is the governance front-end: it anchors each equivalence _claim_ to + its two code locations plus a witness (proof/test), human-in-the-loop and editable. + +[IMPORTANT] +==== +*"Knot theory" is an aspirational lens, not a computation.* The framing _types → carrier +· tropes → recurring equivalence-figures · knot theory → certificate_ is load-bearing for +intuition, but there is *no literal knot-invariant computation here*. We use *invariant* +in the precise sense of a _preserved quantity that certifies "same idea"_, and we make no +Curry–Howard-fidelity claim. +==== + +=== Engine vs. cartridge — the division of labour + +We cannot do this "to the dot", and we do not try. Responsibilities split cleanly: + +* *We build* — the general engine, the interface, the classification vocabulary + (`CorrespondenceKind`), the residue/fibre model, the Dyadic/Echo carrier, and a + *reference language pack*. The certifiable Idris2/Echo math is applied *where it pays* + (high-value or dangerous correspondences), not everywhere. +* *The community builds* — the *per-language modules as cartridges* + (`standards/cartridges/`): the _facts_, i.e. which Forms instantiate which Concepts, + and for each Transition the kind + residue + an optional witness. + +A cartridge fact, sketched: + +[source] +---- +(transition + (concept "name-binding") + (from (lang "basic") (form "X = 5")) + (to (lang "erlang") (form "X = 5")) + (kind false-friend) + (strata (surface corresponds) (semantic diverges)) + (residue "BASIC rebinds destructively; Erlang binds once and unifies") + (witness "proofs/erlang_single_assignment.idr")) ; optional, where it pays +---- + +=== The interface: overlay view-layers + +The "levels of objects" are rendered as *overlay view-layers* — switchable and +non-destructive, each conforming to the estate *overlay-protocol* (additive, +non-modifying, idempotent, Idris2-ABI proved): + +* *focus* — show only what's needed now +* *glyph* — Makaton-style symbols (accessible, low cognitive load) +* *blockly / scratch* — flowchart of structure +* *raw code* — the text itself +* *side-by-side* — multi-language diff with step-sync + +The primary surface is a *browser multi-pane workspace*; the CLI/TUI is the offline +fallback. Accessibility is a contract, not a coat of paint: *Hyperpolymath Accessibility +Standard, Level A minimum → AA* (keyboard-only operation, ≥4.5:1 contrast, colourblind- +safe palettes, ARIA/screen-reader, reduced-motion, plain-language mode, high-contrast +glyph variants). + +=== Downstream: it emits, it does not own the IDE + +Analyses become `octads` written to VeriSimDB (`:8097`) plus `Groove` signals; PanLL +panels subscribe and re-render. Relevant octad types: `inline-annotation` (per-construct +claims), `ecosystem-link` (intra-program relationships), `proof-status` (where a +Transition carries a witness). Ergonomics are read from +`.machine_readable/ENSAID_CONFIG.a2ml`. + +=== Where the host language is going + +The host *today* is ReScript — now a *legacy host being migrated*, and a legacy +*target*. Per estate policy ReScript is banned in *new* code (`.res` → `.affine`). The +host is moving to *AffineScript*, with *Zig FFI + Idris2 ABI* seams. AffineScript stays a +first-class teaching target and the future host — but the project's frame is no longer +"evangelise AffineScript specifically". It is "teach *any* language by transfer, with +AffineScript among the first-class targets". + +=== Toolchain honesty + +AffineScript / Idris2 / Zig / Deno may be absent in some environments. The design and the +proofs are *authored now and verified in CI* — we do not claim local green builds we +cannot run. The correspondence-model spec itself is verified by inspection against estate +exemplars, with one worked example (JTV grammar v2) stubbed pending repository access. + +=== Status + +* Correspondence-model spec: *merged* (canonical design). +* Standards / repo hygiene: *merged*. +* Now: identity re-point + abstraction-model pivot (Concept/Form/Transition + + CorrespondenceKind in the engine). +* Next: browser multi-pane GUI; AffineScript host port; cartridge contract + a second + language pack; proofs/benches + PanLL octad emission. + +See link:ROADMAP.adoc[ROADMAP.adoc] for the sequenced plan, and +link:README.adoc[README.adoc] for the front-door summary. === Evidence [cols="2,3", options="header"] |=== -| Path | Proves +| Path | Role + +| `docs/theory/CORRESPONDENCE-MODEL.adoc` +| Canonical design spec — Concept/Form/Transition, six CorrespondenceKinds, strata, Dyadic+Echo carrier | `src/Patterns.res` -| Multi-target pattern library — ReScript (legacy) and AffineScript (flagship) transformations, plus slots for future targets +| Correspondence / pattern catalogue (target source for cartridge facts) | `src/Analyser.res` / `src/Analyser.resi` -| Pattern analysis engine with public interface — takes JS, returns matches (target-aware) +| Classification + aggregation engine with public interface (per-stratum, kind-aware) | `src/Narrative.res` -| "You were close!" encouragement narratives, per target — the shame-free philosophy in code +| Shame-free narratives keyed by CorrespondenceKind (transfer / flag / remap / bridge / teach / re-route) | `src/Glyphs.res` -| Makaton-inspired glyph system — visual concept representation (target-agnostic) +| Makaton-inspired glyph view-layer — accessible, low-cognitive-load symbol mode | `src/Scanner.res` / `src/Scanner.resi` -| File/project scanner with public interface — batch analysis entry point +| File/project scanner with public interface — offline analysis entry point | `src/Output.res` -| Three output formats (plain, markdown, html) and three view layers (RAW, FOLDED, GLYPHED), per target +| Overlay view-layers (focus / glyph / blockly / raw / side-by-side) | `src/Cli.res` -| CLI entry point: `scan`, `patterns`, `legend`, `stats` with `--target` selection +| CLI entry point (offline fallback): `scan`, `patterns`, `legend`, `stats` |=== diff --git a/README.adoc b/README.adoc index 8115637..83778e4 100644 --- a/README.adoc +++ b/README.adoc @@ -6,44 +6,113 @@ image:https://img.shields.io/badge/License-PMPL--1.0--or--later-blue.svg[License // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell image:[TPCF: Perimeter 3,link=docs/TPCF.md] -image:[AffineScript First] +image:[Learn — to love it!] image:https://img.shields.io/badge/Deno-Runtime-000000.svg[Deno Runtime] -> *Celebrate good, minimize bad, show better* -- A pattern detection engine, CLI, and educational toolkit that teaches JavaScript developers next-generation type-safe languages through progressive code transformation, without shame. +> *Learn — to love it!* -- Duolingo / Rosetta Stone for programming languages. A language-comprehension and transfer engine that *classifies* cross-language correspondences so what you learned in one language carries to the next — without shame. -NOTE: This project is the successor to *ReScript Evangeliser*. The ReScript catalogue is preserved as a legacy target while we pivot the flagship to *AffineScript*. See link:ROADMAP.adoc[ROADMAP.adoc] for migration phases. +NOTE: The canonical design is link:docs/theory/CORRESPONDENCE-MODEL.adoc[docs/theory/CORRESPONDENCE-MODEL.adoc]. This README mirrors it. The repo began as *ReScript Evangeliser*; ReScript is now a legacy host (being migrated) and a legacy target. See link:ROADMAP.adoc[ROADMAP.adoc] for the re-point. == What is This? -Nextgen Languages Evangeliser is a library and toolkit that helps JavaScript developers learn affine/linear-typed and other next-generation languages by: +Nextgen Languages Evangeliser is *Duolingo / Rosetta Stone for programming languages* — a language-*comprehension and transfer* engine. It works *one layer above text editing*, on *syntactic and semantic intention*, so the effort you spent learning language A *transfers* when you move to B (JS → TypeScript, Ruby, Prolog, C, or exotic targets like QPL, Arrow, JTV). -* *Detecting patterns* in existing JS code amenable to transformation -* *Showing transformations* to equivalent code in each supported target -* *Explaining with encouragement* using the "You were close!" philosophy -* *Visualizing concepts* with Makaton-inspired glyphs -* *Progressive disclosure* through 3 view layers (RAW -> FOLDED -> GLYPHED) and 3 output formats (plain, markdown, html) +*Transfer learning across languages is the product.* The engine *computes and classifies* cross-language correspondences and surfaces what carries cleanly, what is a trap, and what has no analog at all. -*We never shame developers.* Your JavaScript is good! Next-generation type systems just make some things even safer. +*We never shame developers.* Your JavaScript is good — the engine tells you which of your existing intuitions transfer, and warns you before one bites. -=== Supported targets +=== What it is NOT -[cols="2,2,3"] +* *Not "the next best IDE."* It does not compete on contrast, project widgets, pop-up notepads, shortcuts, or attention/memory/workflow ergonomics. That is *PanLL* (PanLL + eNSAID = the _contact_ between human, tool, task, and environment). This engine *feeds* PanLL; it is not PanLL. +* *Not a linter.* It is not about colouring scopes or spotting a missing `;` / extra `)`. The differentiator is an engine that *computes and classifies* cross-language equivalence, not one that looks it up. +* *Not a universal translator.* A verified any-language→any-language compiler with full Curry–Howard fidelity would be nice; it is *not the goal*. +* *Not a shame-the-JavaScript pattern matcher.* + +== The Model: Concept / Form / Transition + +[cols="1,3"] +|=== +| *Concept* | The invariant / equivalence-class — the recurring _trope_ ("bind a name to a value", "iterate a collection", "fail recoverably"). +| *Form* | A representative of a Concept _in one language_ (`let` in ReScript, `=` in Erlang, `def` in Python). A Concept has many Forms. +| *Transition* | A directed correspondence Form(A) → Form(B): the Echo-refined map plus its *residue* — what is lost, added, or inverted in crossing. |=== -|*Target* |*Status* |*Pitch* -|*AffineScript* |Flagship |Affine/linear types, borrow checking, quantity type theory; emits typed-wasm (separate repo) -> WebAssembly -|*ReScript* |Legacy (maintained) |Sound inference, Option/Result, pattern matching, JS interop -|*Rust, Gleam, Zig, …* |Planned |Roadmap — see link:ROADMAP.adoc[ROADMAP.adoc] +A *lesson* is a Concept shown through the learner's known Form, the target Form, and the Transition between — narrated without shame: "you already know this; here's the catch". + +=== The six CorrespondenceKinds + +Classification is not a boolean "relates"; it is a *graded* `CorrespondenceKind`, realised as grades of the Echo fibre. Each maps to a pedagogy: + +[cols="2,3,2"] |=== +| Kind | Signature | Pedagogy + +| *Cognate / true friend* | Intention + behaviour coincide; residue ≈ ∅. _e.g._ `def` → `define`. | *Transfer directly.* +| *False friend / homonym* | Surface matches, semantics diverge. _e.g._ BASIC `=` (assignment) vs. Erlang `=` (single-assignment bind/unify). | *Flag the trap.* +| *Antonym / inverted* | Related but opposite. _e.g._ 0- vs. 1-indexing; truthiness; stack-growth direction. | *Remap the intuition.* +| *Alien realization* | Same intention, foreign mechanism, large residue. _e.g._ subtraction in JTV (reversible/add-only) is `add` run backwards. | *Bridge with effort.* +| *Novel / no anchor* | Forward fibre empty. _e.g._ static types coming from assembly + JS; ownership/borrowing; Prolog `cut`. | *Teach de novo.* +| *Vanished* | Backward fibre empty. _e.g._ `return` in ReScript; `null` in a null-free language. | *Un-learn / re-route.* +|=== + +Classification runs *per stratum* — surface → structure → semantic intention → abstraction/trope → cross-language invariant. A correspondence can hold at one stratum and break at another, and that divergence is itself the most valuable signal (it _is_ the false-friend signature). + +=== The formal carrier (honest) + +* *Carrier = Dyadic `Relation`* (`proven-tests-and-benches` `Dyadic.idr`): `{ relates; reflexive; symmetric; transitive }`. Equivalence = refl ∧ sym ∧ trans. +* *Crossings are lossy-with-residue = Echo fibre* (`hyperpolymath/echo-types`, Agda): `Echo f y := Σ (x : A), f x ≡ y`. The residue is precisely what is lost or added going A→B. +* *`invariant-path`* is the governance front-end: it anchors each equivalence _claim_ to two code locations plus a witness, human-in-the-loop. -For those on TypeScript or CoffeeScript: we want to help you reach type-safety guarantees the incumbent tooling cannot give you — without making you throw your existing code away. For the adventurous who want to explore AffineScript's affine/linear guarantees from scratch, this repo is aimed at you. +NOTE: "Knot theory" is an aspirational lens for intuition, not a literal computation. "Invariant" here means a _preserved quantity that certifies "same idea"_ — no knot-invariant is computed, and we do not claim Curry–Howard fidelity. + +== Engine vs. Cartridge + +[cols="1,3"] +|=== +| *We build* | the general engine + the interface + the classification vocabulary (`CorrespondenceKind`) + the residue/fibre model + a reference language pack. +| *The community builds* | the *per-language modules as cartridges* (estate `standards/cartridges/`) — which Forms instantiate which Concepts, and for each Transition the kind + residue + an optional witness. +|=== + +The engine is *language-agnostic*. The nextgen-language collection is merely the substrate we dogfood. + +== Interface: levels of objects, not chrome + +The "levels of objects" are rendered as *overlay view-layers* — switchable, non-destructive, each conforming to the estate *overlay-protocol* (additive, idempotent, Idris2-ABI proved): + +* *focus* — show only what's needed now +* *glyph* — Makaton-style symbols (accessible, low cognitive load) +* *blockly / flowchart* — structure as blocks +* *raw code* — the text itself +* *side-by-side* — multi-language diff with step-sync + +The primary surface is a *browser multi-pane workspace*; a CLI/TUI is the offline fallback. Accessibility is first-class: *Hyperpolymath Accessibility Standard, Level A minimum → AA* (keyboard-only, ≥4.5:1 contrast, colourblind-safe palettes, ARIA/screen-reader, reduced-motion, plain-language mode). + +== Downstream: feeding PanLL + +The engine *emits*; it does not own the IDE. Analyses become `octads` written to VeriSimDB (`:8097`) plus `Groove` signals; PanLL panels subscribe and re-render. Ergonomics are read from `.machine_readable/ENSAID_CONFIG.a2ml`. View-layers conform to the overlay-protocol. + +== Worked examples + +[cols="2,2,3"] +|=== +| Example | Kind | Note + +| `def` → `define` | cognate | "just a rename" — a hypothesis to verify, not assume +| BASIC `=` vs. Erlang `=` | false friend / homonym | surface-corresponds ∧ semantics-diverge +| 0- vs. 1-indexing | antonym | residue = the flip +| JTV reversible/add-only (subtraction = reversed add) | alien realization | JTV is a security-focused, reversible, total, effect/info-flow/capability-typed language +| static types (coming from assembly + JS) | novel / no anchor | nothing to transfer _from_ +| `return` in ReScript | vanished | a concept the learner relied on is gone +|=== == Quick Start === Prerequisites * https://deno.land[Deno] (latest stable) -* https://rescript-lang.org[ReScript] 12.2+ (current host language; see link:ROADMAP.adoc[ROADMAP.adoc] for AffineScript port) +* https://rescript-lang.org[ReScript] 12.2+ (current/legacy host; the host is being ported to AffineScript — see link:ROADMAP.adoc[ROADMAP.adoc]) + +NOTE: AffineScript / Idris2 / Zig may be absent in some environments. Material is authored now and verified in CI — we do not claim local green builds we cannot run. === Installation @@ -64,16 +133,16 @@ just build [source,bash] ---- -# Scan a JavaScript file for patterns -deno run --allow-read bin/evangeliser.js scan +# Scan a source file (offline CLI fallback) +deno run --allow-read bin/evangeliser.js scan -# List all available patterns (with their target languages) +# List available correspondences / patterns deno run --allow-read bin/evangeliser.js patterns # Show the glyph legend deno run --allow-read bin/evangeliser.js legend -# Display detection statistics +# Display statistics deno run --allow-read bin/evangeliser.js stats ---- @@ -81,77 +150,31 @@ deno run --allow-read bin/evangeliser.js stats [source,bash] ---- -# Run tests -just test - -# Watch mode for development -just watch - -# Format code -just fmt - -# Full CI simulation -just ci +just test # Run tests +just watch # Watch mode +just fmt # Format code +just ci # Full CI simulation ---- -== Features - -=== Multi-target Pattern Catalogue - -Each pattern carries example transformations for multiple target languages. Categories include: - -* Null safety -> Option types (ReScript) / total functions (AffineScript) -* Resource management -> affine ownership (AffineScript) / explicit disposal -* Aliasing of mutable state -> borrow checking (AffineScript) -* Async/await -> Promise types / effect systems -* Try/catch -> Result types / typed error channels -* Array operations -> pipe operator / iterator patterns -* Conditionals -> pattern matching / total case analysis -* OOP -> functional programming / records over classes -* ...and more - -The AffineScript catalogue prioritises affine/linear-safety patterns: use-after-free analogs, double-cleanup, detached references, accidental aliasing of owned state, missing `using`/disposal, and similar concerns that the type system can make unrepresentable. - -=== Three View Layers - -1. *RAW*: Side-by-side JavaScript/target comparison with narrative -2. *FOLDED*: Organized sections with collapsible regions -3. *GLYPHED*: Symbol-annotated code showing semantic meaning - -=== Three Output Formats - -* *Plain text*: Terminal-friendly output -* *Markdown*: For documentation and READMEs -* *HTML*: For web integration and reports - -=== Makaton-Inspired Glyphs - -Visual symbols that transcend syntax across 21 glyph categories: +== Targets -* Shield (null safety) -* Transform (data transformation) -* Flow (pipe operator) -* Branch (pattern matching) -* Crystal (immutability) -* Seal (affine ownership) -* ...and more +The engine is language-agnostic; targets are the languages a learner moves *to*. AffineScript is a first-class teaching target and the future host, but the frame is no longer "evangelise AffineScript specifically" — it is "teach _any_ language by transfer, with AffineScript among the first-class targets." -=== Scanner + Analyser Detection Engine - -Regex-based pattern matching engine that scans JavaScript source files and identifies patterns amenable to transformation. The Analyser aggregates Scanner results into categorised reports with narrative explanations per-target. - -=== CLI Interface +[cols="2,2,3"] +|=== +|*Target* |*Status* |*Why interesting for transfer* -Four commands: `scan`, `patterns`, `legend`, `stats`. Supports RAW/FOLDED/GLYPHED view layers, plain/markdown/html output formats, and `--target` selection. +|*AffineScript* |First-class; future host |Affine/linear use-once, borrow checking, QTT — dense with _novel/no-anchor_ correspondences; emits typed-wasm → WebAssembly +|*ReScript* |Legacy target + legacy host |Sound inference, Option/Result, pattern matching; source of the _vanished_ `return` example +|*TypeScript, Ruby, Prolog, C, JTV, …* |Reference / planned |Cartridge-authored; JTV is the canonical _alien-realization_ + _novel_ worked example +|=== == Technology Stack -=== Primary Technologies - -* *ReScript 12.2* (current host): All application logic (Types, Glyphs, Narrative, Patterns, Scanner, Analyser, Output, CLI) -* *AffineScript* (planned host, Phase 3+): OCaml 5.1+ toolchain; emits typed-wasm IR (separate repo) -> WebAssembly -* *Deno*: Runtime and package management -* *Zig*: FFI layer (canonical per manifest) +* *ReScript 12.2* — current/legacy host (being migrated; banned in _new_ code per estate policy: `.res` → `.affine`) +* *AffineScript* — future host (Zig FFI + Idris2 ABI seams); OCaml 5.1+ toolchain; emits typed-wasm IR → WebAssembly +* *Deno* — runtime and package management +* *Zig* — FFI layer (canonical per manifest); *Idris2* — ABI / proofs === Language Policy @@ -161,34 +184,33 @@ Per Hyperpolymath Standard: |=== |*Allowed* |*Banned* -|AffineScript, ReScript |TypeScript +|AffineScript (future host), ReScript (legacy) |TypeScript |Deno |Node.js/npm/bun |justfile |Makefile -|Zig (FFI) |V (outside V ecosystem) +|Zig (FFI), Idris2 (ABI/proofs) |V (outside V ecosystem) |=== -See link:CLAUDE.md[CLAUDE.md] and link:.claude/CLAUDE.md[.claude/CLAUDE.md] for the full policy. +ReScript is banned in _new_ code; existing `.res` migrates to `.affine`. See link:CLAUDE.md[CLAUDE.md] and link:.claude/CLAUDE.md[.claude/CLAUDE.md] for the full policy. == Project Structure [source] ---- nextgen-languages-evangeliser/ -+-- src/ # Host source (ReScript today, AffineScript post-Phase 3) -| +-- Types.res # Core type model (multi-target) -| +-- Glyphs.res # Makaton-inspired symbol system -| +-- Narrative.res # Encouraging message generation (target-aware) -| +-- Patterns.res # Multi-target transformation patterns -| +-- Scanner.res # Pattern detection engine -| +-- Scanner.resi # Scanner interface -| +-- Analyser.res # Result aggregation and reporting -| +-- Analyser.resi # Analyser interface -| +-- Output.res # RAW/FOLDED/GLYPHED formatting (multi-target) -| +-- Cli.res # CLI entry point (scan/patterns/legend/stats) ++-- src/ # Host source (ReScript today; AffineScript target) +| +-- Types.res # Core type model +| +-- Glyphs.res # Makaton-inspired glyph view-layer +| +-- Narrative.res # Shame-free narrative (per CorrespondenceKind) +| +-- Patterns.res # Correspondence / pattern catalogue +| +-- Scanner.res(i) # Detection engine + interface +| +-- Analyser.res(i) # Classification + aggregation + interface +| +-- Output.res # focus/glyph/blockly/raw/side-by-side view-layers +| +-- Cli.res # CLI entry point (offline fallback) ++-- gui/ # Browser multi-pane workspace (primary surface) +-- test/ # Test suites -+-- bin/ -| +-- evangeliser.js # CLI entry point ++-- bin/evangeliser.js # Deno CLI shim +-- docs/ # Documentation +| +-- theory/CORRESPONDENCE-MODEL.adoc # Canonical design spec +-- rescript.json # Host (ReScript) configuration +-- deno.json # Deno configuration +-- Justfile # Task orchestration @@ -197,44 +219,28 @@ nextgen-languages-evangeliser/ +-- CLAUDE.md # AI context ---- -== Philosophy: "Celebrate Good, Minimize Bad, Show Better" - -We *never* shame developers. Instead: +== Philosophy: no shame, transfer-first -1. *Celebrate*: Recognize what their JavaScript does well -2. *Minimize*: Gently acknowledge minor limitations -3. *Better*: Show how the target language enhances the pattern -4. *Safety*: Explain type-level (and affine/linear) guarantees -5. *Example*: Provide concrete, encouraging examples - -Example narrative (ReScript target): - -____ -*You were close!* You're already thinking about null and undefined - that's great defensive programming! - -The only small thing is that it's easy to forget one of these checks somewhere... - -*Even better:* ReScript's Option type makes null safety automatic - you literally can't forget a check! -____ +We *never* shame developers. The six kinds map to: transfer cognates, warn on false friends, remap antonyms, bridge the alien, teach the novel, re-route the vanished. The narrative voice stays *celebrate / minimise / better / safety / example*. -Example narrative (AffineScript target): +Example narrative (a _false friend_): ____ -*You were close!* You're already disposing of the resource in a `finally` block - that's careful code! +*You already know this!* You've used `=` for assignment for years — that intuition is real. -The only small thing is that if another closure still holds a reference, the dispose runs but the alias lives on... +*Here's the catch:* in Erlang `=` doesn't assign, it binds-once and unifies. Re-`=` with a different value _fails_ a match rather than overwriting. -*Even better:* AffineScript's affine types make the resource vanish from the type environment at disposal - no alias can survive, no double-free can compile. +*Re-route:* read `X = 5` as "assert X equals 5 (binding it if unbound)", not "store 5 in X". ____ == RSR Compliance This project follows the *Rhodium Standard Repository (RSR)* framework: -* *Type Safety*: ReScript 12.2 host today; AffineScript (affine/linear types, borrow checking) post-Phase 3 -* *Offline First*: Zero network dependencies -* *Complete Documentation*: See link:docs/[docs/] -* *Security First*: See link:SECURITY.md[SECURITY.md] +* *Type Safety*: ReScript 12.2 host today; AffineScript (affine/linear types, borrow checking) as the host target. Correspondence carrier is Idris2/Agda-grounded (Dyadic + Echo). +* *Offline First*: CLI/TUI fallback; zero network dependency to scan +* *Complete Documentation*: see link:docs/[docs/] and link:docs/theory/CORRESPONDENCE-MODEL.adoc[the design spec] +* *Security First*: see link:SECURITY.md[SECURITY.md] * *Open Governance*: TPCF Perimeter 3 (Community Sandbox) * *Licensed*: MPL-2.0 * *Build Reproducibility*: Deno + Justfile @@ -243,13 +249,14 @@ See link:docs/RSR_COMPLIANCE.md[RSR_COMPLIANCE.md] for full details. == Documentation -* link:CONTRIBUTING.md[CONTRIBUTING.md] - How to contribute +* link:docs/theory/CORRESPONDENCE-MODEL.adoc[CORRESPONDENCE-MODEL.adoc] - Canonical design spec +* link:CONTRIBUTING.md[CONTRIBUTING.md] - How to contribute (incl. cartridge authoring) * link:CODE_OF_CONDUCT.md[CODE_OF_CONDUCT.md] - Community guidelines * link:SECURITY.md[SECURITY.md] - Security policies * link:CHANGELOG.md[CHANGELOG.md] - Version history * link:docs/RSR_COMPLIANCE.md[RSR_COMPLIANCE.md] - RSR framework compliance * link:docs/TPCF.md[TPCF.md] - Contribution framework -* link:ROADMAP.adoc[ROADMAP.adoc] - Migration phases & future targets +* link:ROADMAP.adoc[ROADMAP.adoc] - Re-point sequence & milestones == Contributing @@ -257,7 +264,7 @@ We welcome contributions! See link:CONTRIBUTING.md[CONTRIBUTING.md] for: * Code of Conduct * Development setup -* Pattern authoring guide (multi-target) +* Cartridge authoring guide (per-language correspondence modules) * Testing requirements * Pull request process @@ -273,12 +280,13 @@ See link:LICENSE[LICENSE] for the full license text. == Roadmap -* [x] Phase 0: Decide migration path (option A content rewrite, multilanguage future) -* [-] Phase 1: Rebrand + generalise engine for multi-target (host stays ReScript) -* [ ] Phase 2: Pattern catalogue pivot — affine/linear-safety-focused AffineScript patterns -* [ ] Phase 3: Host language port (ReScript -> AffineScript), gated on toolchain WASM maturity -* [ ] Phase 4: Policy perimeter flip (ts-blocker extended, affinescript linter added) -* [ ] Phase 5: Zig/V policy formalisation; additional targets (Rust, Gleam) +* [x] Correspondence-model spec merged (Concept/Form/Transition + six CorrespondenceKinds) +* [x] Standards / repo hygiene merged +* [-] Identity re-point + abstraction-model pivot (classify, not translate) +* [ ] Browser multi-pane GUI (overlay view-layers) +* [ ] AffineScript host port (Zig FFI + Idris2 ABI) +* [ ] Cartridge contract + 2nd language pack +* [ ] Proofs/benches + PanLL octad emission See link:ROADMAP.adoc[ROADMAP.adoc] for detailed milestones. @@ -289,11 +297,11 @@ If you use this project in academic research, please cite: [source,bibtex] ---- @software{nextgen_languages_evangeliser_2026, - title = {Nextgen Languages Evangeliser: Progressive Code Transformation for Affine/Linear-Typed Languages}, + title = {Nextgen Languages Evangeliser: Cross-Language Correspondence Classification for Transfer Learning}, author = {Jonathan D.A. Jewell}, year = {2026}, url = {https://github.com/hyperpolymath/nextgen-languages-evangeliser}, - note = {Successor to rescript-evangeliser; RSR Bronze-compliant, TPCF Perimeter 3} + note = {Duolingo/Rosetta Stone for programming languages; successor to rescript-evangeliser; RSR Bronze-compliant, TPCF Perimeter 3} } ---- @@ -305,24 +313,22 @@ If you use this project in academic research, please cite: == Acknowledgments -* *AffineScript Team*: For the flagship target — affine/linear types, borrow checker, QTT -* *ReScript Team*: For seeding this evangeliser's original catalogue and philosophy -* *Makaton*: Inspiration for the glyph system -* *Deno Team*: For the excellent runtime -* *Contributors*: See link:.well-known/humans.txt[humans.txt] +* *Dyadic / Echo*: `proven-tests-and-benches` and `hyperpolymath/echo-types` for the formal carrier (relation + loss-with-residue) +* *PanLL / eNSAID*: the downstream contact layer this engine feeds +* *Makaton*: inspiration for the glyph view-layer +* *Deno Team*: for the excellent runtime +* *Contributors*: see link:.well-known/humans.txt[humans.txt] == Status -* *Version*: 0.6.0-alpha (migrating from rescript-evangeliser 0.5.x) +* *Version*: 0.6.0-alpha (re-pointed from rescript-evangeliser 0.5.x) * *RSR Level*: Bronze -* *Targets*: 2 active (AffineScript flagship, ReScript legacy), more planned -* *Tests*: 38 across 6 suites (being updated for multi-target) -* *CI Workflows*: 16, all passing -* *Build Status*: Passing -* *Last Updated*: 2026-04-16 +* *Design*: correspondence-model spec merged; hygiene merged; abstraction-model pivot next +* *Toolchain*: AffineScript/Idris2/Zig deferred — authored now, verified in CI +* *Last Updated*: 2026-06-18 --- -Made with care for JavaScript developers curious about next-generation languages. +Made with care for developers learning a new language. -*Remember*: You were close! +*Remember*: Learn — to love it! diff --git a/ROADMAP.adoc b/ROADMAP.adoc index 8522978..d290e82 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -3,60 +3,83 @@ == Current Status -Migration from *ReScript Evangeliser* to *Nextgen Languages Evangeliser* — Phase 1 in progress. - -See link:CLAUDE.md[CLAUDE.md] and link:README.adoc[README.adoc] for project overview. - -== Migration Phases - -=== Phase 0 — Decide -* [x] Pick pivot strategy: Option A (content rewrite, not mechanical translation) -* [x] Commit to multilanguage future (flagship AffineScript, legacy ReScript, more to follow) -* [x] Rename repo: `rescript-evangeliser` -> `nextgen-languages-evangeliser` -* [x] Verify AffineScript toolchain buildable today (OCaml 5.1+, Dune 3.14+) -* [x] Note: typed-wasm is a separate repo (downstream of AffineScript) - -=== Phase 1 — Rebrand + generalise engine -* [-] Rebrand docs, metadata, contractiles, config files -* [ ] Generalise `src/Types.res` for multi-target pattern representation -* [ ] Thread multi-target through Narrative / Output / Analyser / CLI -* [ ] Convert `src/Patterns.res` to multi-target shape (ReScript + AffineScript stub) -* [ ] Update tests -* [ ] Host language stays ReScript throughout - -=== Phase 2 — Pattern catalogue pivot -* [ ] Design AffineScript-first catalogue around affine/linear safety concerns: - ** Use-after-free analogs (stale closures, detached refs) - ** Double-cleanup / double-free analogs - ** Accidental aliasing of owned state - ** Missing `using`/disposal, leaky listeners/timers - ** Resource sharing across async boundaries -* [ ] Target ~12 curated patterns initially (depth over breadth) -* [ ] Demote ReScript catalogue to legacy support -* [ ] Preserve "You were close!" narrative voice - -=== Phase 3 — Host language port (gated on toolchain) -* [ ] End-to-end "hello world -> typed-wasm -> WASM" proof-of-life -* [ ] Port `src/` ReScript -> AffineScript module-by-module: - Types, Glyphs, Narrative, Scanner, Analyser, Output, Cli, Patterns (last) +*Identity re-point in progress.* The project is now *Duolingo / Rosetta Stone for +programming languages* — a language-comprehension and transfer engine that *classifies* +cross-language correspondences (it does not translate, lint, or build IDEs). The +correspondence-model spec and standards hygiene have landed; the abstraction-model pivot +is next. + +See link:docs/theory/CORRESPONDENCE-MODEL.adoc[CORRESPONDENCE-MODEL.adoc] (canonical +design), link:CLAUDE.md[CLAUDE.md], and link:README.adoc[README.adoc] for overview. + +== Milestones + +=== M0 — Correspondence-model spec ✅ +* [x] Establish the engine identity: *classify, not translate* (and not an IDE, not a linter) +* [x] Concept / Form / Transition object model +* [x] Six `CorrespondenceKind`s (cognate / false-friend / antonym / alien-realization / novel / vanished) as graded Echo fibres +* [x] Strata ("levels of objects": surface → structure → intention → trope → invariant) +* [x] Formal carrier: Dyadic `Relation` + Echo loss-with-residue; `invariant-path` as governance front-end +* [x] Honesty caveat recorded: "knot theory" is an aspirational lens, not a computation + +=== M1 — Standards / repo hygiene ✅ +* [x] Repo renamed `rescript-evangeliser` → `nextgen-languages-evangeliser` +* [x] Estate machine-readable artefacts + SPDX present +* [x] Language-policy perimeter aligned (Deno, Justfile, Zig FFI; TS/Python/Go/V banned; ReScript legacy) + +=== M2 — Identity re-point + abstraction-model pivot → +* [-] Re-point identity documentation to the corrected purpose (this pass) +* [ ] Implement Concept / Form / Transition in `src/Types.res` (engine model, not pattern-list) +* [ ] Implement `CorrespondenceKind` + per-stratum classification in Analyser +* [ ] Thread kind-aware, shame-free narratives through `src/Narrative.res` +* [ ] Reframe `src/Patterns.res` as a *reference language pack* (Forms + Transitions, not "JS→AffineScript patterns") +* [ ] Update tests for the classify-not-translate model + +=== M3 — Browser multi-pane GUI → +* [ ] Browser multi-pane workspace as the primary surface +* [ ] Overlay view-layers: focus / glyph / blockly-flowchart / raw-code / side-by-side +* [ ] View-layers conform to the estate overlay-protocol (additive, idempotent) +* [ ] Accessibility: Hyperpolymath Accessibility Standard Level A → AA (keyboard-only, ≥4.5:1 contrast, ARIA, reduced-motion, plain-language mode) +* [ ] CLI/TUI retained as the offline fallback + +=== M4 — AffineScript host port (Zig FFI + Idris2 ABI) → +* [ ] End-to-end "hello world → typed-wasm → WASM" proof-of-life +* [ ] Port `src/` ReScript → AffineScript (`.res` → `.affine`), module-by-module +* [ ] Zig FFI seam + Idris2 ABI seam for the certifiable carrier * [ ] Port `test/` in lockstep +* [ ] Note: ReScript banned in _new_ code; AffineScript remains a first-class teaching target and the future host + +=== M5 — Cartridge contract + 2nd language pack → +* [ ] Specify the cartridge fact format (concept / from / to / kind / strata / residue / witness) +* [ ] Wire `standards/cartridges/` as the community authoring surface +* [ ] Ship the reference language pack as the worked exemplar +* [ ] Author a second language pack to validate the engine is language-agnostic (JTV is the canonical alien-realization / novel exemplar) + +=== M6 — Proofs/benches + PanLL octad emission → +* [ ] Carry Idris2/Echo witnesses where the math pays (high-value / dangerous Transitions) +* [ ] Benches via `proven-tests-and-benches` +* [ ] Emit `octads` → VeriSimDB (`:8097`) + `Groove` signals; PanLL panels subscribe +* [ ] Read ergonomics from `.machine_readable/ENSAID_CONFIG.a2ml` + +== Division of labour + +* *We build* — the general engine + interface + classification vocabulary + residue model + a reference language pack. +* *The community builds* — the per-language modules as *cartridges* (`standards/cartridges/`). + +The engine is language-agnostic; the nextgen-language collection is merely the substrate we dogfood. -=== Phase 4 — Policy perimeter flip -* [ ] Extend `ts-blocker.yml` to also reject new `.res` files outside `legacy/` -* [ ] Add `affinescript-linter.yml` workflow -* [ ] `justfile` recipe names stable, invocations swapped -* [ ] Retire `rescript.json` (or demote to legacy) +== Toolchain honesty -=== Phase 5 — Policy text + additional targets -* [ ] `.claude/CLAUDE.md`: formalise V-in-ecosystem-only carveout -* [ ] Zig as canonical FFI (no code change — already true per manifest) -* [ ] Additional target backends: Rust, Gleam, Zig +AffineScript / Idris2 / Zig / Deno may be absent in some environments. Material is +*authored now and verified in CI* — we do not claim local green builds we cannot run. The +JTV grammar-v2 worked example remains stubbed pending repository access. == Future Directions -After Phase 5, the project becomes a general-purpose evangelisation platform for nextgen typed languages, with: +Beyond M6, the project is a general transfer-learning platform across programming +languages: -* A shared engine (Scanner, Analyser, Output, Narrative) target-agnostic -* Per-target catalogues authored by language advocates -* Glyph system treated as a lingua franca across targets -* Citation-stable URL via GitHub redirect from legacy `rescript-evangeliser` slug +* A shared, language-agnostic engine (scan → classify → narrate → emit) feeding PanLL +* Per-language correspondence cartridges authored by language advocates +* The glyph view-layer as an accessible lingua franca across targets +* Citation-stable URL via GitHub redirect from the legacy `rescript-evangeliser` slug From c8b2859f77eca82193da35a709f3a6f74d5d653d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 11:59:50 +0000 Subject: [PATCH 04/17] =?UTF-8?q?feat(abi):=20Idris2=20ABI=20core=20?= =?UTF-8?q?=E2=80=94=20Concept/Form/Transition=20+=20six=20CorrespondenceK?= =?UTF-8?q?inds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First slice of the abstraction-model pivot (track 2), authored as the formally- typed ABI seam per the chosen "Idris2 ABI core first" approach: - abi.ipkg — Idris2 package (sourcedir src/interface, depends base only) - src/interface/Abi/Carrier.idr — Relation (refl/sym/trans + isEquivalence) and Residue (the Echo-fibre grades: None/Inverted/Lossy/AbsentSource/AbsentTarget) - src/interface/Abi/Correspondence.idr — Concept/Form/Transition, the six CorrespondenceKinds, the Stratum ladder, total pedagogy + residueShape maps, and the precise false-friend signature (surface-corresponds AND intention-diverges) - src/interface/Abi/README.adoc — seam overview + the cite-don't-import licence note Licence: original MPL-2.0. Grounded in but NOT importing the Dyadic Relation (proven-tests-and-benches, AGPL-3.0-or-later / son-shared) or the echo-types Echo fibre — importing AGPL would relicense this MPL-2.0 repo by linkage, so the shapes are re-expressed as fresh source. abi.ipkg depends on base only. Verification deferred: no Idris2 in this environment; typecheck via `idris2 --typecheck abi.ipkg` in a tooled session / CI. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- abi.ipkg | 26 +++++ src/interface/Abi/Carrier.idr | 59 +++++++++++ src/interface/Abi/Correspondence.idr | 149 +++++++++++++++++++++++++++ src/interface/Abi/README.adoc | 41 ++++++++ 4 files changed, 275 insertions(+) create mode 100644 abi.ipkg create mode 100644 src/interface/Abi/Carrier.idr create mode 100644 src/interface/Abi/Correspondence.idr create mode 100644 src/interface/Abi/README.adoc diff --git a/abi.ipkg b/abi.ipkg new file mode 100644 index 0000000..53a4a79 --- /dev/null +++ b/abi.ipkg @@ -0,0 +1,26 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- Copyright (c) 2026 Jonathan D.A. Jewell +-- +-- Idris2 package for the formally-typed ABI seam of the correspondence engine. +-- +-- The seam lives in a single, case-consistent directory src/interface/Abi/ +-- (uppercase) so the physical path matches the `module Abi.*` namespace on +-- EVERY filesystem (Idris2 requires capitalised namespace components). +-- +-- Typecheck with the package (not a bare --check, which warns on module/path): +-- idris2 --typecheck abi.ipkg (or --build) + +package abi + +version = 0.1.0 + +authors = "Jonathan D.A. Jewell" + +brief = "Formally-typed ABI seam (Idris2): Concept/Form/Transition + the six CorrespondenceKinds, grounded in Dyadic + Echo" + +sourcedir = "src/interface" + +depends = base + +modules = Abi.Carrier + , Abi.Correspondence diff --git a/src/interface/Abi/Carrier.idr b/src/interface/Abi/Carrier.idr new file mode 100644 index 0000000..95f5727 --- /dev/null +++ b/src/interface/Abi/Carrier.idr @@ -0,0 +1,59 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- Copyright (c) 2026 Jonathan D.A. Jewell +||| +||| Abi.Carrier — the formal carrier of a cross-language equivalence *claim*. +||| +||| This is ORIGINAL MPL-2.0 code. It is conceptually grounded in — but does +||| not import — two estate libraries, to keep the licence clean: +||| +||| * the Dyadic `Relation` (reflexive/symmetric/transitive) from +||| hyperpolymath/proven-tests-and-benches (AGPL-3.0-or-later, son-shared) +||| * the Echo loss-with-residue fibre `Echo f y := Sigma (x : A), f x = y` +||| from hyperpolymath/echo-types (Agda) +||| +||| Importing the AGPL library into this MPL-2.0 repo would relicense it by +||| linkage; the shapes are re-expressed here as fresh MPL-2.0 source instead. + +module Abi.Carrier + +%default total + +||| A binary relation over `a` together with its (asserted) algebraic +||| properties. The carrier of an equivalence *claim*: a Concept is "the same +||| idea" across Forms exactly when this relation is an equivalence. +public export +record Relation (a : Type) where + constructor MkRelation + name : String + relates : a -> a -> Bool + reflexive : Bool + symmetric : Bool + transitive : Bool + +||| Equivalence = reflexive AND symmetric AND transitive. +public export +isEquivalence : Relation a -> Bool +isEquivalence (MkRelation _ _ r s t) = r && s && t + +||| The residue of a crossing A -> B: *what is lost, added, or inverted*. +||| These are the grades of the Echo fibre, from empty (true iso) through +||| inverted / lossy to empty-in-either-direction. +public export +data Residue : Type where + ||| True isomorphism — nothing lost (the fibre is inhabited and unique). + None : Residue + ||| The crossing is a flip (e.g. 0- vs 1-indexing). Carries a note. + Inverted : String -> Residue + ||| One-way loss / added machinery (a retraction): same intention, foreign + ||| mechanism. Carries a note of the machinery. + Lossy : String -> Residue + ||| Forward fibre empty: the target concept has no source anchor. (Novel.) + AbsentSource : String -> Residue + ||| Backward fibre empty: the source concept has vanished in the target. + AbsentTarget : String -> Residue + +||| Is this crossing a true isomorphism (no residue)? +public export +isIso : Residue -> Bool +isIso None = True +isIso _ = False diff --git a/src/interface/Abi/Correspondence.idr b/src/interface/Abi/Correspondence.idr new file mode 100644 index 0000000..a0bba3a --- /dev/null +++ b/src/interface/Abi/Correspondence.idr @@ -0,0 +1,149 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- Copyright (c) 2026 Jonathan D.A. Jewell +||| +||| Abi.Correspondence — the abstraction model: Concept / Form / Transition and +||| the six graded CorrespondenceKinds. The formal heart of an engine that +||| *classifies* cross-language correspondences (it does not translate, and +||| claims no Curry-Howard fidelity). See docs/theory/CORRESPONDENCE-MODEL.adoc. + +module Abi.Correspondence + +import Abi.Carrier + +%default total + +-------------------------------------------------------------------------------- +-- Strata ("levels of objects") +-------------------------------------------------------------------------------- + +||| The strata of meaning. Classification runs *per stratum*: a correspondence +||| can hold at one and break at another — and that divergence is the signal. +public export +data Stratum = Surface | Structure | Intention | Trope | Invariant + +public export +Eq Stratum where + (==) Surface Surface = True + (==) Structure Structure = True + (==) Intention Intention = True + (==) Trope Trope = True + (==) Invariant Invariant = True + (==) _ _ = False + +-------------------------------------------------------------------------------- +-- The six CorrespondenceKinds (graded Echo fibres) +-------------------------------------------------------------------------------- + +||| The kind of a cross-language correspondence — a typed/graded verdict, not a +||| boolean "relates". Each kind is a grade of the Echo fibre with a pedagogy. +public export +data CorrespondenceKind : Type where + ||| Intention + behaviour coincide; residue ~ empty. e.g. `def` -> `define`. + Cognate : CorrespondenceKind + ||| Surface matches, semantics diverge. e.g. BASIC `=` vs Erlang `=`. + FalseFriend : CorrespondenceKind + ||| Related but inverted. e.g. 0- vs 1-indexing. + Antonym : CorrespondenceKind + ||| Same intention, foreign mechanism, large residue. e.g. subtraction in JTV + ||| (reversible / add-only) is `add` run backwards. + AlienRealization : CorrespondenceKind + ||| Forward fibre empty: nothing to map from. e.g. static types from asm+JS. + Novel : CorrespondenceKind + ||| Backward fibre empty: a relied-on concept is gone. e.g. `return` in ReScript. + Vanished : CorrespondenceKind + +public export +Eq CorrespondenceKind where + (==) Cognate Cognate = True + (==) FalseFriend FalseFriend = True + (==) Antonym Antonym = True + (==) AlienRealization AlienRealization = True + (==) Novel Novel = True + (==) Vanished Vanished = True + (==) _ _ = False + +||| The pedagogy prescribed by each kind. +public export +data Pedagogy = Transfer | Warn | Remap | Bridge | TeachDeNovo | ReRoute + +||| Total map: every kind has exactly one pedagogy. No shame — even the traps +||| are taught, not scolded. +public export +pedagogy : CorrespondenceKind -> Pedagogy +pedagogy Cognate = Transfer +pedagogy FalseFriend = Warn +pedagogy Antonym = Remap +pedagogy AlienRealization = Bridge +pedagogy Novel = TeachDeNovo +pedagogy Vanished = ReRoute + +||| The residue shape characteristic of each kind (its Echo-fibre grade). +public export +residueShape : CorrespondenceKind -> Residue +residueShape Cognate = None +residueShape FalseFriend = Lossy "surface-corresponds; semantics diverge" +residueShape Antonym = Inverted "related but inverted" +residueShape AlienRealization = Lossy "same intention; foreign mechanism" +residueShape Novel = AbsentSource "no source anchor" +residueShape Vanished = AbsentTarget "concept gone in the target" + +-------------------------------------------------------------------------------- +-- Form / Concept / Transition +-------------------------------------------------------------------------------- + +||| A representative of a Concept *in one language*. +public export +record Form where + constructor MkForm + language : String -- e.g. "erlang", "rescript", "jtv" + surface : String -- the token / snippet, e.g. "X = 5" + +||| A Concept — the invariant / equivalence-class (the recurring trope) — +||| carried by an (asserted) equivalence Relation over its Forms. +public export +record Concept where + constructor MkConcept + name : String -- e.g. "name-binding" + carrier : Relation Form -- certifies "same idea" across Forms + +||| A per-stratum verdict: does the correspondence hold at this stratum? +public export +record StratumVerdict where + constructor MkVerdict + stratum : Stratum + holds : Bool + +||| A directed correspondence Form(from) -> Form(to): the classified crossing, +||| its residue, the per-stratum verdicts, and an optional `witness` anchoring +||| the claim to a proof/test — the invariant-path governance front-end, used +||| where the certifiable math pays. +public export +record Transition where + constructor MkTransition + concept : String + from : Form + to : Form + kind : CorrespondenceKind + residue : Residue + strata : List StratumVerdict + witness : Maybe String + +-------------------------------------------------------------------------------- +-- The false-friend signature +-------------------------------------------------------------------------------- + +||| Look up the verdict at a given stratum. +public export +verdictAt : Stratum -> List StratumVerdict -> Maybe Bool +verdictAt _ [] = Nothing +verdictAt s (v :: vs) = if v.stratum == s then Just v.holds else verdictAt s vs + +||| The false-friend signature, made precise: *corresponds at the Surface +||| stratum yet diverges at the Intention stratum*. This is the whole reason +||| classification runs per stratum rather than as a single boolean. +public export +isFalseFriendShape : List StratumVerdict -> Bool +isFalseFriendShape vs = + case (verdictAt Surface vs, verdictAt Intention vs) of + (Just True, Just False) => True + _ => False diff --git a/src/interface/Abi/README.adoc b/src/interface/Abi/README.adoc new file mode 100644 index 0000000..a2f05f7 --- /dev/null +++ b/src/interface/Abi/README.adoc @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MPL-2.0 += ABI seam — the correspondence model (Idris2) + +The formally-typed core of the engine that *classifies* cross-language +correspondences. Typecheck with `idris2 --typecheck abi.ipkg` from the repo root. + +== Modules + +* `Abi.Carrier` — the carrier of an equivalence *claim*: a `Relation` (with its + reflexive / symmetric / transitive properties and `isEquivalence`) and the + `Residue` of a crossing — the grades of the Echo fibre, from `None` (true iso) + through `Inverted` / `Lossy` to `AbsentSource` / `AbsentTarget` (empty fibre). +* `Abi.Correspondence` — `Concept` / `Form` / `Transition`, the six + `CorrespondenceKind`s (cognate / false-friend / antonym / alien-realization / + novel / vanished), the `Stratum` ladder, each kind's `pedagogy` and + `residueShape`, and the precise *false-friend signature* (`isFalseFriendShape`): + corresponds at the `Surface` stratum yet diverges at the `Intention` stratum. + +See `docs/theory/CORRESPONDENCE-MODEL.adoc` for the full design. + +== Licence note (cite, don't import) + +This seam is original *MPL-2.0* code. It is conceptually grounded in two estate +libraries but deliberately does **not** import them, to keep the licence clean: + +* the Dyadic `Relation` from `hyperpolymath/proven-tests-and-benches` + (AGPL-3.0-or-later, son-shared) — importing it would relicense this repo by + linkage; +* the Echo loss-with-residue fibre `Echo f y := Sigma (x : A), f x = y` from + `hyperpolymath/echo-types` (Agda). + +The shapes are re-expressed here as fresh MPL-2.0 source rather than depended on. +`abi.ipkg` therefore lists `depends = base` only. + +== Status + +Authored ahead of the toolchain: Idris2 is not present in every environment, so +this is verified by `idris2 --typecheck abi.ipkg` in a tooled session / CI, not +yet in the authoring environment. The Zig FFI side of the seam +(`src/interface/ffi/`) and the AffineScript host binding that consumes this ABI +follow as later slices of the abstraction-model pivot. From dd13b846d50677fd569488ba7f496ec99a4cfcb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 12:41:57 +0000 Subject: [PATCH 05/17] =?UTF-8?q?feat(ffi):=20Zig=20FFI=20seam=20=E2=80=94?= =?UTF-8?q?=20C-ABI=20mirror=20of=20the=20correspondence=20ABI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second slice of the abstraction-model pivot (track 2): the Zig FFI side of the seam, mirroring the Idris2 ABI (src/interface/Abi/) over a C boundary. Per the estate pattern Idris2 owns the ABI/proofs and Zig owns the FFI; the two describe one model and must agree. - src/interface/ffi/src/main.zig — CorrespondenceKind / Pedagogy / Stratum C enums + nle_pedagogy_of (total map), nle_residue_is_iso, nle_kind_name, nle_is_false_friend (the false-friend signature) and nle_version; unit tests mirror the Idris2 semantics. - src/interface/ffi/test/integration_test.zig — cross-checks against the model. - src/interface/ffi/build.zig — minimal Zig 0.15.2+ scaffolding. - src/interface/ffi/README.adoc — seam overview + cite-don't-import licence note. Licence: original MPL-2.0, no AGPL/echo-types linkage. Verification deferred: no Zig in this environment; run `zig test src/main.zig`. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- src/interface/ffi/README.adoc | 30 +++++ src/interface/ffi/build.zig | 16 +++ src/interface/ffi/src/main.zig | 131 ++++++++++++++++++++ src/interface/ffi/test/integration_test.zig | 20 +++ 4 files changed, 197 insertions(+) create mode 100644 src/interface/ffi/README.adoc create mode 100644 src/interface/ffi/build.zig create mode 100644 src/interface/ffi/src/main.zig create mode 100644 src/interface/ffi/test/integration_test.zig diff --git a/src/interface/ffi/README.adoc b/src/interface/ffi/README.adoc new file mode 100644 index 0000000..39af8c3 --- /dev/null +++ b/src/interface/ffi/README.adoc @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 += FFI seam — the correspondence model over C-ABI (Zig) + +The C-ABI mirror of the Idris2 ABI in `../Abi/` (`Abi.Carrier` + +`Abi.Correspondence`). Per the estate pattern, *Idris2 owns the ABI/proofs* and +*Zig owns the FFI/C boundary*; the two describe the same model and must agree. + +== Surface + +* `CorrespondenceKind` / `Pedagogy` / `Stratum` — C enums matching the Idris2 types. +* `nle_pedagogy_of(kind) -> Pedagogy` — the total pedagogy map. +* `nle_residue_is_iso(kind) -> u32` — true only for `cognate` (residue `None`). +* `nle_kind_name(kind)` — stable lowercase names. +* `nle_is_false_friend(surface, intention) -> u32` — the false-friend signature + (corresponds at `Surface` AND diverges at `Intention`). +* `nle_version()`. + +== Build / test + +Authored ahead of the toolchain (Zig may be absent in this environment). Run: + +[source,sh] +---- +zig test src/main.zig +zig test test/integration_test.zig +---- + +== Licence + +Original *MPL-2.0*. No AGPL / echo-types linkage — see `../Abi/README.adoc`. diff --git a/src/interface/ffi/build.zig b/src/interface/ffi/build.zig new file mode 100644 index 0000000..c54be06 --- /dev/null +++ b/src/interface/ffi/build.zig @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell +// +// FFI build configuration (Zig 0.15.2+). Minimal scaffolding: the FFI seam +// mirrors the Idris2 ABI in src/interface/Abi/. Run the unit tests directly: +// zig test src/main.zig +// zig test test/integration_test.zig + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + _ = b.standardTargetOptions(.{}); + _ = b.standardOptimizeOption(.{}); + // Tests run via `zig test src/main.zig`. Expand to addStaticLibrary / + // addTest steps when the AffineScript host binding consumes this seam. +} diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig new file mode 100644 index 0000000..560589d --- /dev/null +++ b/src/interface/ffi/src/main.zig @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell +// +// Nextgen Languages Evangeliser — FFI seam (Zig). +// +// The C-ABI mirror of the Idris2 ABI in src/interface/Abi/ (Abi.Carrier + +// Abi.Correspondence). Idris2 owns the proofs; Zig owns the C boundary. The two +// MUST agree: the enums and the pedagogy / false-friend semantics here mirror +// Abi.Correspondence exactly. Verify with: zig test src/main.zig +// +// Original MPL-2.0 — no AGPL/echo-types linkage (see ../Abi/README.adoc). + +const std = @import("std"); + +//============================================================================== +// Enums — must match Abi.Correspondence +//============================================================================== + +/// The six graded kinds of cross-language correspondence. +pub const CorrespondenceKind = enum(c_int) { + cognate = 0, + false_friend = 1, + antonym = 2, + alien_realization = 3, + novel = 4, + vanished = 5, +}; + +/// The pedagogy prescribed by each kind. +pub const Pedagogy = enum(c_int) { + transfer = 0, + warn = 1, + remap = 2, + bridge = 3, + teach_de_novo = 4, + re_route = 5, +}; + +/// Strata of meaning ("levels of objects"). +pub const Stratum = enum(c_int) { + surface = 0, + structure = 1, + intention = 2, + trope = 3, + invariant = 4, +}; + +//============================================================================== +// Total maps — mirror Idris2 `pedagogy` and `residueShape` +//============================================================================== + +/// pedagogy : CorrespondenceKind -> Pedagogy (total) +pub export fn nle_pedagogy_of(kind: CorrespondenceKind) Pedagogy { + return switch (kind) { + .cognate => .transfer, + .false_friend => .warn, + .antonym => .remap, + .alien_realization => .bridge, + .novel => .teach_de_novo, + .vanished => .re_route, + }; +} + +/// Is the kind's residue a true isomorphism (None)? Only Cognate. Returns 1/0. +pub export fn nle_residue_is_iso(kind: CorrespondenceKind) u32 { + return if (kind == .cognate) 1 else 0; +} + +/// A stable, lowercase name for a kind (static storage; do not free). +pub export fn nle_kind_name(kind: CorrespondenceKind) [*:0]const u8 { + return switch (kind) { + .cognate => "cognate", + .false_friend => "false-friend", + .antonym => "antonym", + .alien_realization => "alien-realization", + .novel => "novel", + .vanished => "vanished", + }; +} + +//============================================================================== +// The false-friend signature — mirrors Abi.Correspondence.isFalseFriendShape +//============================================================================== + +/// Corresponds at Surface AND diverges at Intention. Inputs are bool-as-u32 +/// (non-zero = holds). Returns 1 if the crossing is a false friend. +pub export fn nle_is_false_friend(surface_holds: u32, intention_holds: u32) u32 { + return if (surface_holds != 0 and intention_holds == 0) 1 else 0; +} + +//============================================================================== +// Version +//============================================================================== + +pub export fn nle_version() [*:0]const u8 { + return "0.1.0"; +} + +//============================================================================== +// Tests — mirror the Idris2 semantics +//============================================================================== + +test "pedagogy mapping is total and matches the ABI" { + try std.testing.expectEqual(Pedagogy.transfer, nle_pedagogy_of(.cognate)); + try std.testing.expectEqual(Pedagogy.warn, nle_pedagogy_of(.false_friend)); + try std.testing.expectEqual(Pedagogy.remap, nle_pedagogy_of(.antonym)); + try std.testing.expectEqual(Pedagogy.bridge, nle_pedagogy_of(.alien_realization)); + try std.testing.expectEqual(Pedagogy.teach_de_novo, nle_pedagogy_of(.novel)); + try std.testing.expectEqual(Pedagogy.re_route, nle_pedagogy_of(.vanished)); +} + +test "only cognate is a true isomorphism" { + try std.testing.expect(nle_residue_is_iso(.cognate) == 1); + try std.testing.expect(nle_residue_is_iso(.false_friend) == 0); + try std.testing.expect(nle_residue_is_iso(.vanished) == 0); +} + +test "false-friend signature: surface holds AND intention diverges" { + try std.testing.expect(nle_is_false_friend(1, 0) == 1); // BASIC = vs Erlang = + try std.testing.expect(nle_is_false_friend(1, 1) == 0); + try std.testing.expect(nle_is_false_friend(0, 0) == 0); + try std.testing.expect(nle_is_false_friend(0, 1) == 0); +} + +test "kind names are stable" { + try std.testing.expectEqualStrings("false-friend", std.mem.span(nle_kind_name(.false_friend))); +} + +test "version is non-empty" { + try std.testing.expect(std.mem.span(nle_version()).len > 0); +} diff --git a/src/interface/ffi/test/integration_test.zig b/src/interface/ffi/test/integration_test.zig new file mode 100644 index 0000000..51125ef --- /dev/null +++ b/src/interface/ffi/test/integration_test.zig @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell +// +// FFI integration tests — verify the Zig C-ABI mirror agrees with the Idris2 ABI +// (src/interface/Abi/Correspondence.idr). Run: zig test test/integration_test.zig + +const std = @import("std"); +const ffi = @import("../src/main.zig"); + +test "pedagogy mapping matches the ABI (spot checks)" { + try std.testing.expectEqual(ffi.Pedagogy.transfer, ffi.nle_pedagogy_of(.cognate)); + try std.testing.expectEqual(ffi.Pedagogy.warn, ffi.nle_pedagogy_of(.false_friend)); + try std.testing.expectEqual(ffi.Pedagogy.re_route, ffi.nle_pedagogy_of(.vanished)); +} + +test "false-friend signature matches the per-stratum rule" { + // surface-corresponds AND intention-diverges (e.g. BASIC = vs Erlang =) + try std.testing.expect(ffi.nle_is_false_friend(1, 0) == 1); + try std.testing.expect(ffi.nle_is_false_friend(1, 1) == 0); +} From 021b2272f8aa787c49f1184ba55237b483b9b8c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 12:58:17 +0000 Subject: [PATCH 06/17] feat(cartridges): correspondence cartridge contract + validator (verified) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-format half of the abstraction model: how community language packs declare the correspondence FACTS the engine classifies. The kind / residue / stratum vocabulary mirrors the Idris2 ABI (src/interface/Abi/Correspondence.idr) and the Zig FFI. - cartridges/correspondence-cartridge.schema.json — JSON Schema (draft 2020-12): transitions of concept / from / to / kind / residue / strata / narrative / witness, with enums mirroring the ABI. - cartridges/reference/worked-examples.cartridge.json — reference pack: one transition per CorrespondenceKind (the canonical worked examples). - cartridges/README.adoc — cartridge-authoring guide + engine/cartridge boundary. - scripts/validate-cartridges.js — Deno + ajv validator (walks cartridges/). - Justfile — `just validate-cartridges` recipe. Verified locally: `deno run -A scripts/validate-cartridges.js` => 1/1 valid (6 transitions, all six kinds). Fixed a schema bug found while verifying ($schema vs additionalProperties:false). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- Justfile | 5 + cartridges/README.adoc | 56 ++++++++++ .../correspondence-cartridge.schema.json | 105 ++++++++++++++++++ .../reference/worked-examples.cartridge.json | 98 ++++++++++++++++ scripts/validate-cartridges.js | 45 ++++++++ 5 files changed, 309 insertions(+) create mode 100644 cartridges/README.adoc create mode 100644 cartridges/correspondence-cartridge.schema.json create mode 100644 cartridges/reference/worked-examples.cartridge.json create mode 100644 scripts/validate-cartridges.js diff --git a/Justfile b/Justfile index ae32a1f..0012c1d 100644 --- a/Justfile +++ b/Justfile @@ -76,6 +76,11 @@ validate: @echo "🔍 Validating project..." deno task validate +# Validate cartridges against the correspondence schema (Deno + ajv) +validate-cartridges: + @echo "🧩 Validating cartridges..." + @deno run -A scripts/validate-cartridges.js + # Validate RSR compliance validate-rsr: @echo "🔍 Validating RSR Bronze-level compliance..." diff --git a/cartridges/README.adoc b/cartridges/README.adoc new file mode 100644 index 0000000..2c35887 --- /dev/null +++ b/cartridges/README.adoc @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MPL-2.0 += Cartridges — per-language correspondence packs + +*We build the engine + this contract; the community authors cartridges.* A +**cartridge** is a per-language pack of correspondence *facts* the engine +classifies and teaches. The engine is language-agnostic; a cartridge supplies the +language-specific knowledge. + +== What a cartridge declares + +For each cross-language `transition` (a directed Form(from) -> Form(to)): + +* `concept` — the invariant / trope it instantiates (e.g. `name-binding`); +* `from` / `to` — the `Form`s (`language` slug + `surface` snippet); +* `kind` — the `CorrespondenceKind`: `cognate` / `false-friend` / `antonym` / + `alien-realization` / `novel` / `vanished`; +* `residue` — the Echo-fibre grade (`none` / `inverted` / `lossy` / + `absent-source` / `absent-target`) + a note of what is lost, added, or inverted; +* `strata` (optional) — per-stratum verdicts; the *false-friend signature* is + `surface: true` AND `intention: false`; +* `narrative` (optional) — shame-free guidance (celebrate / minimise / better / + safety / example); +* `witness` (optional) — a path to a proof/test anchoring the claim, used where + the certifiable math pays. + +This vocabulary mirrors the Idris2 ABI at `src/interface/Abi/Correspondence.idr` +and the Zig FFI at `src/interface/ffi/`, so cartridges, proofs, and the C +boundary all describe one model. + +== Schema and reference + +* Schema: `correspondence-cartridge.schema.json` (JSON Schema draft 2020-12). +* Reference: `reference/worked-examples.cartridge.json` — one transition per kind, + using the canonical examples from `docs/theory/CORRESPONDENCE-MODEL.adoc`. + +== Authoring a cartridge + +. Copy the reference pack and replace its transitions with your language pair(s). +. Validate against the schema, e.g. with any JSON Schema validator: ++ +[source,sh] +---- +# example (Deno): +deno run --allow-read npm:ajv-cli validate \ + -s cartridges/correspondence-cartridge.schema.json \ + -d cartridges/reference/worked-examples.cartridge.json +---- +. Choose your own `spdx` for your cartridge — the engine does not dictate it. + +Estate-wide cartridges live in `hyperpolymath/standards` (`standards/cartridges/`); +this directory holds the schema this engine defines plus its reference pack. + +== Licence + +The schema and reference pack here are *MPL-2.0*. Cartridge authors license their +own packs as they choose (the `spdx` field). diff --git a/cartridges/correspondence-cartridge.schema.json b/cartridges/correspondence-cartridge.schema.json new file mode 100644 index 0000000..1e00662 --- /dev/null +++ b/cartridges/correspondence-cartridge.schema.json @@ -0,0 +1,105 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hyperpolymath.dev/nextgen-languages-evangeliser/cartridges/correspondence-cartridge.schema.json", + "title": "Correspondence Cartridge", + "description": "SPDX-License-Identifier: MPL-2.0. A per-language correspondence pack for the Nextgen Languages Evangeliser engine. A cartridge declares the FACTS — which Forms instantiate which Concepts, and for each Transition its kind + residue + per-stratum verdicts — that the engine classifies. The vocabulary (kinds, strata, residue shapes) mirrors the Idris2 ABI at src/interface/Abi/Correspondence.idr. We build the engine + this contract; the community authors cartridges.", + "type": "object", + "required": ["spdx", "name", "version", "transitions"], + "additionalProperties": false, + "properties": { + "$schema": { "type": "string", "description": "Optional pointer to this schema (for editor / CI validation)." }, + "spdx": { + "type": "string", + "description": "SPDX licence of THIS cartridge (the cartridge author's own choice; not dictated by the engine)." + }, + "name": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Cartridge slug, e.g. 'js-to-erlang' or 'worked-examples'." + }, + "version": { "type": "string", "description": "SemVer." }, + "description": { "type": "string" }, + "languages": { + "type": "array", + "items": { "type": "string" }, + "description": "Language slugs this cartridge relates, e.g. ['javascript', 'erlang', 'jtv']." + }, + "transitions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/transition" } + } + }, + "$defs": { + "form": { + "type": "object", + "required": ["language", "surface"], + "additionalProperties": false, + "properties": { + "language": { "type": "string", "description": "Language slug, e.g. 'erlang'." }, + "surface": { "type": "string", "description": "The token / snippet, e.g. 'X = 5'." } + } + }, + "stratumVerdict": { + "type": "object", + "required": ["stratum", "holds"], + "additionalProperties": false, + "properties": { + "stratum": { + "enum": ["surface", "structure", "intention", "trope", "invariant"], + "description": "Mirrors Abi.Correspondence.Stratum." + }, + "holds": { "type": "boolean", "description": "Does the correspondence hold at this stratum?" } + } + }, + "residue": { + "type": "object", + "required": ["shape"], + "additionalProperties": false, + "properties": { + "shape": { + "enum": ["none", "inverted", "lossy", "absent-source", "absent-target"], + "description": "The Echo-fibre grade; mirrors Abi.Carrier.Residue. 'none' = true iso; 'absent-source' = novel; 'absent-target' = vanished." + }, + "note": { "type": "string", "description": "What is lost, added, or inverted." } + } + }, + "narrative": { + "type": "object", + "additionalProperties": false, + "description": "Shame-free narrative. The voice stays celebrate / minimise / better / safety / example.", + "properties": { + "celebrate": { "type": "string" }, + "minimise": { "type": "string" }, + "better": { "type": "string" }, + "safety": { "type": "string" }, + "example": { "type": "string" } + } + }, + "transition": { + "type": "object", + "required": ["concept", "from", "to", "kind", "residue"], + "additionalProperties": false, + "properties": { + "concept": { "type": "string", "description": "The Concept (invariant / trope), e.g. 'name-binding'." }, + "from": { "$ref": "#/$defs/form" }, + "to": { "$ref": "#/$defs/form" }, + "kind": { + "enum": ["cognate", "false-friend", "antonym", "alien-realization", "novel", "vanished"], + "description": "The CorrespondenceKind; mirrors Abi.Correspondence.CorrespondenceKind." + }, + "residue": { "$ref": "#/$defs/residue" }, + "strata": { + "type": "array", + "items": { "$ref": "#/$defs/stratumVerdict" }, + "description": "Per-stratum verdicts. The false-friend signature is surface:true AND intention:false." + }, + "narrative": { "$ref": "#/$defs/narrative" }, + "witness": { + "type": "string", + "description": "Optional path to a proof/test anchoring this claim (the invariant-path governance front-end) — used where the certifiable math pays." + } + } + } + } +} diff --git a/cartridges/reference/worked-examples.cartridge.json b/cartridges/reference/worked-examples.cartridge.json new file mode 100644 index 0000000..d2585eb --- /dev/null +++ b/cartridges/reference/worked-examples.cartridge.json @@ -0,0 +1,98 @@ +{ + "$schema": "../correspondence-cartridge.schema.json", + "spdx": "MPL-2.0", + "name": "worked-examples", + "version": "0.1.0", + "description": "Reference cartridge: one correspondence per CorrespondenceKind, using the canonical worked examples from docs/theory/CORRESPONDENCE-MODEL.adoc. Proves the schema covers all six grades of the Echo fibre.", + "languages": ["python", "scheme", "basic", "erlang", "c", "julia", "jtv", "assembly", "rescript", "javascript"], + "transitions": [ + { + "concept": "function-definition", + "from": { "language": "python", "surface": "def f(): ..." }, + "to": { "language": "scheme", "surface": "(define (f) ...)" }, + "kind": "cognate", + "residue": { "shape": "none" }, + "strata": [ + { "stratum": "surface", "holds": false }, + { "stratum": "intention", "holds": true } + ], + "narrative": { + "celebrate": "You already define functions — this is the very same idea.", + "better": "`define` is just the Scheme spelling of `def`.", + "safety": "Same binding intent; verify before assuming it is only a rename.", + "example": "def f(): return 1 <-> (define (f) 1)" + } + }, + { + "concept": "name-binding", + "from": { "language": "basic", "surface": "X = 5" }, + "to": { "language": "erlang", "surface": "X = 5" }, + "kind": "false-friend", + "residue": { + "shape": "lossy", + "note": "BASIC rebinds destructively; Erlang binds once and unifies — re-`=` fails a match rather than overwriting." + }, + "strata": [ + { "stratum": "surface", "holds": true }, + { "stratum": "intention", "holds": false } + ], + "narrative": { + "celebrate": "You have used `=` for assignment for years — that intuition is real.", + "safety": "In Erlang `=` is single-assignment bind / unify, not store.", + "example": "X = 5, X = 6 %% the second match FAILS" + } + }, + { + "concept": "array-indexing", + "from": { "language": "c", "surface": "a[0]" }, + "to": { "language": "julia", "surface": "a[1]" }, + "kind": "antonym", + "residue": { "shape": "inverted", "note": "0-based vs 1-based: the first element flips index." }, + "strata": [ + { "stratum": "surface", "holds": true }, + { "stratum": "intention", "holds": true }, + { "stratum": "structure", "holds": false } + ] + }, + { + "concept": "subtraction", + "from": { "language": "c", "surface": "a - b" }, + "to": { "language": "jtv", "surface": "uncall add a b" }, + "kind": "alien-realization", + "residue": { + "shape": "lossy", + "note": "JTV is reversible / add-only: subtraction is `add` run backwards; divide via reversed repeated-add." + }, + "strata": [ + { "stratum": "intention", "holds": true }, + { "stratum": "structure", "holds": false } + ] + }, + { + "concept": "static-types", + "from": { "language": "assembly", "surface": "(no analog)" }, + "to": { "language": "rescript", "surface": "let x: int = 5" }, + "kind": "novel", + "residue": { + "shape": "absent-source", + "note": "Coming from assembly + JS there is no prior anchor for a static type system — teach de novo." + }, + "strata": [ + { "stratum": "intention", "holds": false } + ] + }, + { + "concept": "early-return", + "from": { "language": "javascript", "surface": "return x" }, + "to": { "language": "rescript", "surface": "x /* last expression is the value */" }, + "kind": "vanished", + "residue": { + "shape": "absent-target", + "note": "ReScript has no `return`; a block evaluates to its last expression — re-route the habit." + }, + "strata": [ + { "stratum": "intention", "holds": false } + ] + } + ] +} diff --git a/scripts/validate-cartridges.js b/scripts/validate-cartridges.js new file mode 100644 index 0000000..aa1b6fc --- /dev/null +++ b/scripts/validate-cartridges.js @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MPL-2.0 +// Validate every *.cartridge.json under cartridges/ against the correspondence +// schema (JSON Schema draft 2020-12). Deno + ajv. +// +// deno run -A scripts/validate-cartridges.js +// just validate-cartridges +// +// Exits non-zero if any cartridge fails — suitable for CI. + +import Ajv2020 from "npm:ajv@8/dist/2020.js"; + +const dir = new URL("../cartridges/", import.meta.url); +const schema = JSON.parse( + await Deno.readTextFile(new URL("correspondence-cartridge.schema.json", dir)), +); + +const AjvCtor = Ajv2020.default ?? Ajv2020; +const validate = new AjvCtor({ allErrors: true, strict: false }).compile(schema); + +let count = 0; +let bad = 0; + +async function walk(d) { + for await (const entry of Deno.readDir(d)) { + const child = new URL(entry.name + (entry.isDirectory ? "/" : ""), d); + if (entry.isDirectory) { + await walk(child); + continue; + } + if (!entry.name.endsWith(".cartridge.json")) continue; + count++; + const data = JSON.parse(await Deno.readTextFile(child)); + if (validate(data)) { + console.log(`VALID ${entry.name} (${data.transitions.length} transitions)`); + } else { + bad++; + console.error(`INVALID ${entry.name}`); + console.error(JSON.stringify(validate.errors, null, 2)); + } + } +} + +await walk(dir); +console.log(`\n${count - bad}/${count} cartridges valid.`); +Deno.exit(bad ? 1 : 0); From 47e19a6a6fa7e2e3c5e9082d298d0d7fb193265a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 13:02:43 +0000 Subject: [PATCH 07/17] docs(verify): record ABI typecheck; make cartridge validator lockfile-safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/interface/Abi/README.adoc: the ABI typechecks clean under Idris2 0.8.0 (idris2 --typecheck abi.ipkg; %default total, no escape hatches), verified locally — replacing the "not yet typechecked here" caveat. - Justfile + scripts/validate-cartridges.js: run the validator with --no-lock so it no longer perturbs deno.lock. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- Justfile | 2 +- scripts/validate-cartridges.js | 2 +- src/interface/Abi/README.adoc | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Justfile b/Justfile index 0012c1d..951820c 100644 --- a/Justfile +++ b/Justfile @@ -79,7 +79,7 @@ validate: # Validate cartridges against the correspondence schema (Deno + ajv) validate-cartridges: @echo "🧩 Validating cartridges..." - @deno run -A scripts/validate-cartridges.js + @deno run -A --no-lock scripts/validate-cartridges.js # Validate RSR compliance validate-rsr: diff --git a/scripts/validate-cartridges.js b/scripts/validate-cartridges.js index aa1b6fc..70a6c0d 100644 --- a/scripts/validate-cartridges.js +++ b/scripts/validate-cartridges.js @@ -2,7 +2,7 @@ // Validate every *.cartridge.json under cartridges/ against the correspondence // schema (JSON Schema draft 2020-12). Deno + ajv. // -// deno run -A scripts/validate-cartridges.js +// deno run -A --no-lock scripts/validate-cartridges.js // just validate-cartridges // // Exits non-zero if any cartridge fails — suitable for CI. diff --git a/src/interface/Abi/README.adoc b/src/interface/Abi/README.adoc index a2f05f7..07cda21 100644 --- a/src/interface/Abi/README.adoc +++ b/src/interface/Abi/README.adoc @@ -34,8 +34,7 @@ The shapes are re-expressed here as fresh MPL-2.0 source rather than depended on == Status -Authored ahead of the toolchain: Idris2 is not present in every environment, so -this is verified by `idris2 --typecheck abi.ipkg` in a tooled session / CI, not -yet in the authoring environment. The Zig FFI side of the seam +*Typechecks clean* under Idris2 0.8.0 (`idris2 --typecheck abi.ipkg`; `%default +total`, no escape hatches — verified 2026-06-18). The Zig FFI side of the seam (`src/interface/ffi/`) and the AffineScript host binding that consumes this ABI follow as later slices of the abstraction-model pivot. From ec1f5ddb70dd0a5a4e87386760d65451b63e6acd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 13:18:36 +0000 Subject: [PATCH 08/17] feat(ci+host): Idris2 ABI typecheck CI + AffineScript host binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .github/workflows/idris2-abi.yml: builds Idris2 v0.7.0 from source (Chez) and runs `idris2 --typecheck abi.ipkg` on ABI changes — turns the local typecheck into a CI gate (mirrors the verified-clean local run). - src/interface/host/Correspondence.affine: the AffineScript host binding — it mirrors the ABI types (CorrespondenceKind/Pedagogy/Stratum/Form/Transition) and binds the Zig nle_* FFI symbols via `extern fn`. Authored ahead of the AffineScript compiler (compiler-gated; not yet compiled); syntax follows the estate .affine conventions. - src/interface/host/README.adoc. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .github/workflows/idris2-abi.yml | 44 +++++++++ src/interface/host/Correspondence.affine | 117 +++++++++++++++++++++++ src/interface/host/README.adoc | 23 +++++ 3 files changed, 184 insertions(+) create mode 100644 .github/workflows/idris2-abi.yml create mode 100644 src/interface/host/Correspondence.affine create mode 100644 src/interface/host/README.adoc diff --git a/.github/workflows/idris2-abi.yml b/.github/workflows/idris2-abi.yml new file mode 100644 index 0000000..3759026 --- /dev/null +++ b/.github/workflows/idris2-abi.yml @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: MPL-2.0 +# Idris2 ABI typecheck — verifies the formally-typed seam (abi.ipkg) actually +# typechecks. Builds Idris2 v0.7.0 from source via Chez Scheme (the same path +# verified locally; the ABI typechecks clean under Idris2 0.7/0.8). +name: Idris2 ABI + +on: + pull_request: + paths: ['abi.ipkg', 'src/interface/Abi/**', '.github/workflows/idris2-abi.yml'] + push: + branches: [main, master] + paths: ['abi.ipkg', 'src/interface/Abi/**', '.github/workflows/idris2-abi.yml'] + +permissions: + contents: read + +jobs: + typecheck: + name: idris2 --typecheck abi.ipkg + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install Chez Scheme + build tools + run: | + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + chezscheme make gcc libgmp-dev + + - name: Build Idris2 v0.7.0 from source + run: | + git clone --branch v0.7.0 --depth 1 \ + https://github.com/idris-lang/Idris2.git "$HOME/Idris2" + cd "$HOME/Idris2" + make bootstrap SCHEME=chezscheme + make install + echo "$HOME/.idris2/bin" >> "$GITHUB_PATH" + + - name: Typecheck the ABI + run: | + idris2 --version + idris2 --typecheck abi.ipkg diff --git a/src/interface/host/Correspondence.affine b/src/interface/host/Correspondence.affine new file mode 100644 index 0000000..94d763c --- /dev/null +++ b/src/interface/host/Correspondence.affine @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MPL-2.0 +// Correspondence — the AffineScript host binding to the cross-language +// correspondence model. +// +// This mirrors the Idris2 ABI (src/interface/Abi/, which owns the proofs) and +// binds the Zig FFI symbols (src/interface/ffi/, prefix `nle_`, which owns the +// C boundary). Idris2 + Zig + this host module all describe ONE model. +// +// AUTHORED AHEAD OF THE TOOLCHAIN: the AffineScript compiler is not yet +// released, so this module is NOT YET COMPILED. The syntax follows the estate's +// .affine conventions (cf. the 7-tentacles agents); the exact C-FFI `extern` +// annotation and `if`-expression form are confirmed against the AffineScript +// FFI spec when the host toolchain ships. The model types + marshalling are the +// load-bearing part. + +module Correspondence; + +// The six graded kinds (mirrors Abi.Correspondence.CorrespondenceKind and the +// Zig `CorrespondenceKind` enum(c_int), discriminants 0..5). +pub type CorrespondenceKind = + | Cognate + | FalseFriend + | Antonym + | AlienRealization + | Novel + | Vanished + +// The pedagogy prescribed by each kind (mirrors Abi.Correspondence.Pedagogy). +pub type Pedagogy = + | Transfer + | Warn + | Remap + | Bridge + | TeachDeNovo + | ReRoute + +// Strata of meaning ("levels of objects"). +pub type Stratum = + | Surface + | Structure + | Intention + | Trope + | Invariant + +// A representative of a Concept in one language. +pub type Form = { + language: String, + surface: String, +} + +// A per-stratum verdict. +pub type StratumVerdict = { + stratum: Stratum, + holds: Bool, +} + +// A directed correspondence Form(from) -> Form(to): the classified crossing, +// its residue note, the per-stratum verdicts, and an optional witness path. +pub type Transition = { + concept: String, + from: Form, + to: Form, + kind: CorrespondenceKind, + residue: String, + strata: [StratumVerdict], + witness: Option, +} + +// --------------------------------------------------------------------------- +// FFI bindings to the Zig C-ABI seam (src/interface/ffi/, symbols nle_*). +// Integers crossing the boundary are the enum discriminants declared in +// src/interface/ffi/src/main.zig (CorrespondenceKind / Pedagogy as enum(c_int)). +// --------------------------------------------------------------------------- + +extern fn ffi_pedagogy_of(kind: Int) -> Int = "ffi" "nle_pedagogy_of"; +extern fn ffi_residue_is_iso(kind: Int) -> Int = "ffi" "nle_residue_is_iso"; +extern fn ffi_is_false_friend(surface: Int, intention: Int) -> Int = "ffi" "nle_is_false_friend"; + +// Marshalling between the AffineScript sum types and the C discriminants. +pub fn kind_to_int(k: CorrespondenceKind) -> Int { + match k { + Cognate => 0, + FalseFriend => 1, + Antonym => 2, + AlienRealization => 3, + Novel => 4, + Vanished => 5, + } +} + +pub fn int_to_pedagogy(n: Int) -> Pedagogy { + match n { + 0 => Transfer, + 1 => Warn, + 2 => Remap, + 3 => Bridge, + 4 => TeachDeNovo, + _ => ReRoute, + } +} + +// The pedagogy for a kind, via the verified FFI seam. +pub fn pedagogy_of(k: CorrespondenceKind) -> Pedagogy { + int_to_pedagogy(ffi_pedagogy_of(kind_to_int(k))) +} + +// Is the crossing a true isomorphism (residue None)? Only Cognate. +pub fn is_iso(k: CorrespondenceKind) -> Bool { + ffi_residue_is_iso(kind_to_int(k)) == 1 +} + +// The false-friend signature: corresponds at Surface AND diverges at Intention. +pub fn is_false_friend(surface_holds: Bool, intention_holds: Bool) -> Bool { + let s = if surface_holds { 1 } else { 0 }; + let i = if intention_holds { 1 } else { 0 }; + ffi_is_false_friend(s, i) == 1 +} diff --git a/src/interface/host/README.adoc b/src/interface/host/README.adoc new file mode 100644 index 0000000..8b26f1b --- /dev/null +++ b/src/interface/host/README.adoc @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MPL-2.0 += Host binding — the correspondence model in AffineScript + +The host-language side of the seam. `src/interface/Abi/` (Idris2) owns the +proofs, `src/interface/ffi/` (Zig) owns the C boundary, and this module is the +**AffineScript host binding** that consumes them: it mirrors the model types and +binds the Zig `nle_*` symbols via `extern fn`, so the host engine works in terms +of `CorrespondenceKind` / `Pedagogy` / `Transition` directly. + +== Status — compiler-gated + +The AffineScript compiler is **not yet released**, so `Correspondence.affine` is +**authored but not compiled**. Its syntax follows the estate's `.affine` +conventions (cf. the 7-tentacles agents); the precise C-FFI `extern` annotation +is confirmed against the AffineScript FFI spec when the host toolchain ships. +Until then the proofs live on the verified Idris2 side and the runnable logic on +the verified-in-CI Zig side; this module is the typed surface they converge on. + +== See also + +* `src/interface/Abi/` — Idris2 ABI (typechecks clean under Idris2 0.7/0.8). +* `src/interface/ffi/` — Zig FFI (C-ABI mirror; `zig test`). +* `docs/theory/CORRESPONDENCE-MODEL.adoc` — the design. From 1aa75bd71c6b265cf4add84b4108addd87a0b2e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 04:20:07 +0000 Subject: [PATCH 09/17] feat(gui): multi-pane correspondence workspace (cartridge-driven) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the single-pane JS-analyser GUI with the browser multi-pane correspondence workspace — the project's primary surface. - gui/server.js: Deno server, cartridge-driven (reads cartridges/**/*.cartridge.json), no ReScript dependency. Serves the workspace + /api/cartridges. - gui/app.html: 3-pane shell (index · forms · classification); light/dark; accessible (ARIA roles, keyboard nav, >=4.5:1 contrast, reduced-motion). - gui/app.js: workspace logic + five overlay view-layers (side-by-side / focus / glyph / blocks / raw); kind + pedagogy + residue + per-stratum verdicts + no-shame narrative; false-friend-signature callout mirroring the ABI. - gui/README.adoc + `just gui` recipe. Verified locally (Deno): `deno lint` clean; server serves / and /app.js; /api/cartridges returns 6 correspondences across all six kinds. Browser rendering reviewed + lint-clean (headless-DOM test is a follow-up). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- Justfile | 5 + gui/README.adoc | 44 ++++ gui/app.html | 637 ++++++++++++++++-------------------------------- gui/app.js | 470 +++++++++++++++++------------------ gui/server.js | 337 +++++++------------------ 5 files changed, 571 insertions(+), 922 deletions(-) create mode 100644 gui/README.adoc diff --git a/Justfile b/Justfile index 37c6d65..21322bc 100644 --- a/Justfile +++ b/Justfile @@ -62,6 +62,11 @@ lint: @echo "🔍 Linting..." deno task lint +# Serve the browser correspondence workspace (multi-pane, cartridge-driven) +gui: + @echo "🖥️ Correspondence workspace → http://127.0.0.1:8765" + @deno run --allow-read --allow-net gui/server.js + # === TESTING === # Run all tests diff --git a/gui/README.adoc b/gui/README.adoc new file mode 100644 index 0000000..3cb4ab5 --- /dev/null +++ b/gui/README.adoc @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += Correspondence workspace (browser, multi-pane) + +The project's primary surface: a cartridge-driven, multi-pane correspondence +explorer. It reads the validated correspondence facts +(`cartridges/**/*.cartridge.json`) and presents them across three panes — + +* *Index* — the correspondences (concept · from→to · kind badge), filterable by + kind and free-text; +* *Forms* — the two `Form`s, rendered through the active overlay view-layer; +* *Classification* — the `CorrespondenceKind` + its pedagogy, the residue, the + per-stratum verdicts (with the *false-friend signature* called out), and the + no-shame narrative. + +== Run + +[source,sh] +---- +just gui +# or, directly: +deno run --allow-read --allow-net gui/server.js # http://127.0.0.1:8765 +---- + +`--port N` chooses a port; `--open` (with `--allow-run`) launches a browser. + +== View-layers (the "levels of objects") + +* *side-by-side* — the two Forms with the kind between them (default) +* *focus* — only the target Form, minimal chrome +* *glyph* — Makaton-style, symbol-forward, low text +* *blocks* — flowchart-style blocks +* *raw* — the bare surfaces + +== Notes + +No ReScript/AffineScript/Zig at runtime — the workspace is *Deno-only* and +presents authored correspondence facts (the "Duolingo / Rosetta Stone" lessons). +Accessibility targets the Hyperpolymath Accessibility Standard: keyboard +navigation (arrow keys in the index), ARIA roles, >= 4.5:1 contrast, +`prefers-reduced-motion`, and light/dark via `prefers-color-scheme`. + +The Deno server + cartridge API are verified locally (`deno lint`; live endpoint +probe). The browser rendering is lint-clean and reviewed; a headless-DOM test is +a follow-up. diff --git a/gui/app.html b/gui/app.html index d11a1f3..a335022 100644 --- a/gui/app.html +++ b/gui/app.html @@ -1,402 +1,181 @@ + - Nextgen Languages Evangeliser + Nextgen Languages Evangeliser — Correspondence Workspace @@ -404,75 +183,73 @@
- +

Nextgen Languages Evangeliser

-
AffineScript migration workbench
+
Learn — to love it!
-
- - - - +
+
+ + +
+
+ + +
+
+ View layer + + + + + +
-
+
-
JavaScript Source
-
- - -
+
Correspondences
+
+
+
+
    -
    -
    +
    -
    Analysis
    -
    Idle
    +
    Forms
    +
    side-by-side
    -
    -
    -
    0matches
    -
    0patterns
    -
    0%coverage
    -
    -difficulty
    -
    -
    - - - - -
    -
    +
    +
    + +
    +
    +
    Classification
    +
    kind · residue · strata · narrative
    +
    -
    +
    + Loading… + Classify, don't translate · the six CorrespondenceKinds +
    + diff --git a/gui/app.js b/gui/app.js index 2dec019..0ddd9cb 100644 --- a/gui/app.js +++ b/gui/app.js @@ -1,284 +1,258 @@ // SPDX-License-Identifier: MPL-2.0 +// Correspondence multi-pane workspace — browser logic. +// +// Loads cartridge facts from /api/cartridges and presents them as a 3-pane +// correspondence explorer (index · forms · classification) with five overlay +// view-layers (side-by-side / focus / glyph / blocks / raw). Classify, don't +// translate: each correspondence carries its CorrespondenceKind, residue, +// per-stratum verdicts, and a no-shame narrative. -const sampleSource = `async function loadUser(id) { - try { - const response = await fetch(\`/api/users/\${id}\`); - const user = await response.json(); - const name = user ? user.name : "Guest"; - return user?.settings?.theme || name; - } catch (error) { - console.error(error); - return null; - } -}` +const KIND = { + "cognate": { glyph: "🤝", label: "Cognate", pedagogy: "Transfer directly", cls: "k-cognate", blurb: "Same idea — your intuition carries over." }, + "false-friend": { glyph: "🎭", label: "False friend", pedagogy: "Flag the trap", cls: "k-false", blurb: "Looks familiar; behaves differently." }, + "antonym": { glyph: "🔄", label: "Antonym", pedagogy: "Remap the intuition", cls: "k-antonym", blurb: "Related but inverted — flip your expectation." }, + "alien-realization": { glyph: "🛸", label: "Alien realization", pedagogy: "Bridge with effort", cls: "k-alien", blurb: "Same goal, unfamiliar machinery." }, + "novel": { glyph: "✨", label: "Novel", pedagogy: "Teach de novo", cls: "k-novel", blurb: "No prior anchor — learn it fresh." }, + "vanished": { glyph: "👻", label: "Vanished", pedagogy: "Un-learn / re-route", cls: "k-vanished", blurb: "A habit to drop — it's gone here." }, +}; +const RESIDUE = { + "none": { glyph: "∅", label: "none (true isomorphism)" }, + "inverted": { glyph: "🔃", label: "inverted" }, + "lossy": { glyph: "〜", label: "lossy" }, + "absent-source": { glyph: "⌀→", label: "absent source (novel)" }, + "absent-target": { glyph: "→⌀", label: "absent target (vanished)" }, +}; +const STRATA = ["surface", "structure", "intention", "trope", "invariant"]; +const NARR = [ + ["celebrate", "Celebrate"], ["minimise", "Minimise"], ["better", "Better"], + ["safety", "Safety"], ["example", "Example"], +]; -const state = { - activeTab: "findings", - analysis: null, - patterns: [], - legend: "", -} +const state = { items: [], view: "side-by-side", kind: "all", query: "", selected: 0 }; -const source = document.querySelector("#source") -const target = document.querySelector("#target") -const view = document.querySelector("#view") -const difficulty = document.querySelector("#difficulty") -const analyseButton = document.querySelector("#analyse") -const sampleButton = document.querySelector("#sample") -const clearButton = document.querySelector("#clear") -const statusPill = document.querySelector("#status") -const tabPanel = document.querySelector("#tabPanel") +const $ = (id) => document.getElementById(id); -source.value = sampleSource - -function setStatus(text, kind = "") { - statusPill.textContent = text - statusPill.className = `pill ${kind}`.trim() +function node(tag, opts = {}, kids = []) { + const el = document.createElement(tag); + for (const [k, v] of Object.entries(opts)) { + if (v == null) continue; + if (k === "class") el.className = v; + else if (k === "text") el.textContent = v; + else if (k === "html") el.innerHTML = v; + else el.setAttribute(k, v); + } + for (const kid of [].concat(kids)) if (kid != null) el.append(kid); + return el; } -function node(tag, options = {}, children = []) { - const element = document.createElement(tag) - for (const [key, value] of Object.entries(options)) { - if (key === "className") { - element.className = value - } else if (key === "text") { - element.textContent = value - } else if (key.startsWith("on")) { - element.addEventListener(key.slice(2).toLowerCase(), value) - } else { - element.setAttribute(key, value) - } - } - for (const child of children) { - element.append(child) - } - return element +function kindBadge(kind) { + const k = KIND[kind] ?? { glyph: "?", label: kind, cls: "" }; + return node("span", { class: `badge ${k.cls}` }, [`${k.glyph} ${k.label}`]); } -function codeBlock(label, text) { - return node("div", { className: "code-block" }, [ - node("label", { text: label }), - node("pre", {}, [node("code", { text })]), - ]) +function verdictAt(strata, name) { + const v = (strata ?? []).find((s) => s.stratum === name); + return v ? v.holds : null; +} +// Mirrors Abi.Correspondence.isFalseFriendShape: surface holds AND intention diverges. +function isFalseFriendShape(strata) { + return verdictAt(strata, "surface") === true && verdictAt(strata, "intention") === false; } -function updateStats() { - const analysis = state.analysis - document.querySelector("#statMatches").textContent = analysis?.matchCount ?? - 0 - document.querySelector("#statUnique").textContent = analysis?.uniquePatternCount ?? 0 - document.querySelector("#statCoverage").textContent = analysis - ? `${analysis.coveragePercentage}%` - : "0%" - document.querySelector("#statDifficulty").textContent = analysis?.difficulty ?? "-" +function filtered() { + const q = state.query.trim().toLowerCase(); + return state.items.filter((it) => { + if (state.kind !== "all" && it.kind !== state.kind) return false; + if (!q) return true; + return [it.concept, it.from?.language, it.to?.language, it.from?.surface, it.to?.surface] + .filter(Boolean).some((s) => String(s).toLowerCase().includes(q)); + }); } -function renderFindings() { - if (!state.analysis) { - return node("div", { className: "empty", text: "No analysis yet." }) +function renderIndex() { + const list = $("index"); + const items = filtered(); + $("indexCount").textContent = `${items.length} of ${state.items.length}`; + list.replaceChildren(); + if (items.length === 0) { + list.append(node("li", { class: "empty", role: "presentation", text: "No correspondences match." })); + renderDetail(null); + return; } + if (state.selected >= items.length) state.selected = 0; + items.forEach((it, i) => { + const li = node("li", { + role: "option", id: `corr-${i}`, tabindex: "-1", + "aria-selected": String(i === state.selected), + }, [ + node("div", { class: "concept", text: it.concept }), + node("div", { class: "pair", text: `${it.from?.language ?? "?"} → ${it.to?.language ?? "?"}` }), + kindBadge(it.kind), + ]); + li.addEventListener("click", () => { state.selected = i; syncSelection(); }); + list.append(li); + }); + renderDetail(items[state.selected]); + list.setAttribute("aria-activedescendant", `corr-${state.selected}`); +} - const children = [ - node("p", { className: "summary", text: state.analysis.summary }), - ] +function syncSelection() { + const items = filtered(); + document.querySelectorAll("#index li[role=option]").forEach((li, i) => { + li.setAttribute("aria-selected", String(i === state.selected)); + }); + $("index").setAttribute("aria-activedescendant", `corr-${state.selected}`); + const el = $(`corr-${state.selected}`); + if (el) el.scrollIntoView({ block: "nearest" }); + renderDetail(items[state.selected]); +} - if (state.analysis.matches.length === 0) { - children.push( - node("div", { className: "empty", text: "No patterns detected." }), - ) - return node("div", {}, children) - } +// ---- view-layers ------------------------------------------------------------ - for (const match of state.analysis.matches) { - const meta = [ - node("span", { className: "pill good", text: `${match.confidence}%` }), - node("span", { className: "pill", text: match.difficulty }), - node("span", { className: "pill", text: match.category }), - ] - if (match.fallback) { - meta.push( - node("span", { - className: "pill warn", - text: `fallback ${match.target}`, - }), - ) - } else { - meta.push(node("span", { className: "pill", text: match.target })) - } +function formCard(form) { + return node("div", { class: "form-card" }, [ + node("div", { class: "form-lang", text: form?.language ?? "?" }), + node("pre", { class: "surface" }, [node("code", { text: form?.surface ?? "" })]), + ]); +} + +function renderForms(it) { + const host = $("paneForms"); + host.replaceChildren(); + if (!it) { host.append(node("div", { class: "empty", text: "Select a correspondence." })); return; } + host.append(node("h2", { class: "concept-head", text: it.concept })); + const k = KIND[it.kind] ?? {}; - children.push( - node("article", { className: "finding" }, [ - node("div", { className: "finding-header" }, [ - node("div", {}, [ - node("div", { className: "glyphs", text: match.glyphs.join(" ") }), - node("strong", { text: match.name }), - ]), - node("div", { className: "meta" }, meta), - ]), - node("div", { className: "finding-grid" }, [ - codeBlock("JavaScript", match.jsExample), - codeBlock(match.target, match.targetCode), - node("div", { className: "narrative" }, [ - node("label", { text: "Narrative" }), - node("div", { className: "narrative-list" }, [ - node("p", { text: match.narrative.celebrate }), - node("p", { text: match.narrative.better }), - node("p", { text: match.narrative.safety }), - node("p", { text: match.narrative.example }), - ]), - ]), - ]), + if (state.view === "raw") { + host.append(node("pre", { class: "surface" }, [node("code", { + text: `${it.from?.language}: ${it.from?.surface}\n${it.to?.language}: ${it.to?.surface}`, + })])); + return; + } + if (state.view === "focus") { + host.append(node("div", { class: "focus-form" }, [ + node("p", { class: "pane-hint", text: `from your ${it.from?.language}: ${it.from?.surface}` }), + formCard(it.to), + ])); + return; + } + if (state.view === "glyph") { + host.append(node("div", { class: "glyph-view" }, [ + node("div", { class: "big", text: k.glyph ?? "?" }), + node("div", { text: k.label ?? it.kind }), + node("div", { class: "row" }, [ + node("div", {}, [node("div", { class: "form-lang", text: it.from?.language }), node("code", { text: it.from?.surface })]), + node("div", { class: "cross", text: "→" }), + node("div", {}, [node("div", { class: "form-lang", text: it.to?.language }), node("code", { text: it.to?.surface })]), ]), - ) + ])); + return; } - - return node("div", {}, children) -} - -function renderPatterns() { - if (state.patterns.length === 0) { - return node("div", { - className: "empty", - text: "Pattern catalogue unavailable.", - }) + if (state.view === "blockly") { + host.append(node("div", { class: "blockly" }, [ + node("div", { class: "block" }, [node("div", { class: "blang", text: it.from?.language }), node("code", { text: it.from?.surface })]), + node("div", { class: "connector", text: `▼ ${k.glyph ?? ""} ${k.label ?? it.kind}` }), + node("div", { class: "block" }, [node("div", { class: "blang", text: it.to?.language }), node("code", { text: it.to?.surface })]), + ])); + return; } - - return node( - "div", - { className: "catalogue" }, - state.patterns.map((pattern) => - node("article", { className: "pattern" }, [ - node("div", { className: "pattern-header" }, [ - node("div", {}, [ - node("div", { - className: "glyphs", - text: pattern.glyphs.join(" "), - }), - node("strong", { text: pattern.name }), - ]), - node("span", { className: "pill", text: `${pattern.confidence}%` }), - ]), - node("div", { className: "pattern-body" }, [ - node("p", { text: pattern.celebrate }), - node("div", { className: "meta" }, [ - node("span", { className: "pill", text: pattern.category }), - node("span", { className: "pill", text: pattern.difficulty }), - node("span", { - className: "pill", - text: pattern.targets.join(", "), - }), - ]), - ]), - ]) - ), - ) + // side-by-side (default) + host.append(node("div", { class: "sxs" }, [ + formCard(it.from), + node("div", { class: "cross" }, [document.createTextNode(k.glyph ?? "→"), node("small", { text: "becomes" })]), + formCard(it.to), + ])); } -function renderLegend() { - return node("pre", { className: "legend-output" }, [ - node("code", { text: state.legend || "Legend unavailable." }), - ]) -} +function renderClassify(it) { + const host = $("paneClassify"); + host.replaceChildren(); + if (!it) { host.append(node("div", { class: "empty", text: "—" })); return; } + const k = KIND[it.kind] ?? { glyph: "?", label: it.kind, pedagogy: "", blurb: "", cls: "" }; -function renderExport() { - const text = state.analysis?.rendered?.markdown || "" - return node("pre", { className: "export-output" }, [ - node("code", { text: text || "No output yet." }), - ]) -} - -function renderTab() { - document.querySelectorAll(".tab").forEach((tab) => { - tab.setAttribute( - "aria-selected", - String(tab.dataset.tab === state.activeTab), - ) - }) + host.append(node("div", { class: "kind-hero" }, [ + node("div", { class: `glyph ${k.cls}`, text: k.glyph }), + node("div", {}, [ + kindBadge(it.kind), + node("div", { class: "pedagogy", text: k.pedagogy }), + node("div", { class: "blurb", text: k.blurb }), + ]), + ])); - tabPanel.replaceChildren( - state.activeTab === "findings" - ? renderFindings() - : state.activeTab === "patterns" - ? renderPatterns() - : state.activeTab === "legend" - ? renderLegend() - : renderExport(), - ) -} + const res = RESIDUE[it.residue?.shape] ?? { glyph: "?", label: it.residue?.shape ?? "—" }; + host.append(node("div", { class: "section" }, [ + node("h3", { text: "Residue" }), + node("div", { class: "residue" }, [ + node("span", { class: "glyph", text: res.glyph }), + node("span", { class: "shape", text: res.label }), + ]), + it.residue?.note ? node("div", { class: "residue-note", text: it.residue.note }) : null, + ])); -async function analyse() { - analyseButton.disabled = true - setStatus("Analysing") - try { - const response = await fetch("/api/analyse", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - code: source.value, - target: target.value, - view: view.value, - difficulty: difficulty.value, - format: "markdown", - }), - }) - const body = await response.json() - if (!response.ok) { - throw new Error(body.error || "Analysis failed") - } - state.analysis = body - state.activeTab = "findings" - updateStats() - renderTab() - setStatus("Complete", "good") - } catch (error) { - setStatus("Failed", "warn") - tabPanel.replaceChildren( - node("div", { className: "empty", text: error.message }), - ) - } finally { - analyseButton.disabled = false + const strataRow = node("div", { class: "strata" }, STRATA.map((s) => { + const v = verdictAt(it.strata, s); + const cls = v === true ? "holds" : v === false ? "diverges" : ""; + const mark = v === true ? "✓" : v === false ? "✗" : "—"; + return node("span", { class: `stratum ${cls}`, text: `${s} ${mark}` }); + })); + const strataSection = node("div", { class: "section" }, [node("h3", { text: "Strata (levels of objects)" }), strataRow]); + if (isFalseFriendShape(it.strata)) { + strataSection.append(node("div", { class: "ff-flag", text: "⚠ False-friend signature: corresponds at the surface, diverges at intention." })); } -} + host.append(strataSection); -async function loadCatalogue() { - const [patternsResponse, legendResponse] = await Promise.all([ - fetch("/api/patterns"), - fetch("/api/legend"), - ]) - if (patternsResponse.ok) { - const body = await patternsResponse.json() - state.patterns = body.patterns + if (it.narrative) { + const blocks = NARR.filter(([key]) => it.narrative[key]) + .map(([key, label]) => node("div", { class: "n" }, [node("b", { text: label }), document.createTextNode(it.narrative[key])])); + if (blocks.length) { + host.append(node("div", { class: "section" }, [node("h3", { text: "Narrative (no shame)" }), node("div", { class: "narrative" }, blocks)])); + } } - if (legendResponse.ok) { - const body = await legendResponse.json() - state.legend = body.legend + if (it.witness) { + host.append(node("div", { class: "section" }, [node("h3", { text: "Witness" }), node("code", { text: it.witness })])); } } -analyseButton.addEventListener("click", analyse) -sampleButton.addEventListener("click", () => { - source.value = sampleSource - analyse() -}) -clearButton.addEventListener("click", () => { - source.value = "" - state.analysis = null - updateStats() - renderTab() - setStatus("Idle") -}) +function renderDetail(it) { renderForms(it); renderClassify(it); } + +// ---- events ----------------------------------------------------------------- -for (const control of [target, view, difficulty]) { - control.addEventListener("change", analyse) +function onKeyNav(e) { + const items = filtered(); + if (!items.length) return; + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + const d = e.key === "ArrowDown" ? 1 : -1; + state.selected = (state.selected + d + items.length) % items.length; + syncSelection(); + } else if (e.key === "Home") { e.preventDefault(); state.selected = 0; syncSelection(); } + else if (e.key === "End") { e.preventDefault(); state.selected = items.length - 1; syncSelection(); } } -document.querySelectorAll(".tab").forEach((tab) => { - tab.addEventListener("click", () => { - state.activeTab = tab.dataset.tab - renderTab() - }) -}) +async function init() { + $("filterKind").addEventListener("change", (e) => { state.kind = e.target.value; state.selected = 0; renderIndex(); }); + $("search").addEventListener("input", (e) => { state.query = e.target.value; state.selected = 0; renderIndex(); }); + $("index").addEventListener("keydown", onKeyNav); + document.querySelectorAll("input[name=view]").forEach((r) => + r.addEventListener("change", (e) => { + state.view = e.target.value; $("viewHint").textContent = e.target.value; + renderForms(filtered()[state.selected]); + })); + + try { + const res = await fetch("/api/cartridges"); + const body = await res.json(); + if (!res.ok) throw new Error(body.error || "load failed"); + state.items = (body.cartridges || []).flatMap((c) => + (c.transitions || []).map((t) => ({ ...t, cartridge: c.cartridge }))); + $("status").textContent = state.items.length + ? `${state.items.length} correspondences across ${body.cartridges.length} cartridge(s)` + : "No cartridges found."; + renderIndex(); + } catch (err) { + $("status").textContent = `Error: ${err.message}`; + $("index").append(node("li", { class: "empty", text: err.message })); + } +} -await loadCatalogue() -renderTab() -analyse() +init(); diff --git a/gui/server.js b/gui/server.js index 8d43de9..87334aa 100644 --- a/gui/server.js +++ b/gui/server.js @@ -1,284 +1,133 @@ // SPDX-License-Identifier: MPL-2.0 - -const projectRootUrl = new URL("../", import.meta.url) -const projectRoot = decodeURIComponent(projectRootUrl.pathname) -const decoder = new TextDecoder() - -const contentTypes = { - ".html": "text/html; charset=utf-8", - ".js": "text/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", -} +// Deno server for the correspondence multi-pane workspace. +// +// Cartridge-driven: reads the validated correspondence facts under +// cartridges/**/*.cartridge.json and serves the browser workspace. No ReScript +// dependency — the workspace presents authored correspondence facts (the +// "Duolingo / Rosetta Stone" lessons), classified per the six CorrespondenceKinds. +// +// deno run --allow-read --allow-net gui/server.js [--port N] [--host H] [--open] +// (add --allow-env to honour EVANGELISER_GUI_PORT; --allow-run for --open) + +const root = new URL("../", import.meta.url); function argValue(name, fallback) { - const index = Deno.args.indexOf(name) - if (index === -1 || index + 1 >= Deno.args.length) { - return fallback - } - return Deno.args[index + 1] + const i = Deno.args.indexOf(name); + return i === -1 || i + 1 >= Deno.args.length ? fallback : Deno.args[i + 1]; } -async function exists(path) { +function envPort() { try { - await Deno.stat(path) - return true - } catch (error) { - if (error instanceof Deno.errors.NotFound) { - return false - } - throw error + return Deno.env.get("EVANGELISER_GUI_PORT"); + } catch { + return undefined; // --allow-env not granted; fine } } -async function ensureCompiled() { - const compiledEntry = new URL("../src/Cli.res.js", import.meta.url) - if (await exists(compiledEntry)) { - return - } - - const command = new Deno.Command(Deno.execPath(), { - args: ["run", "--no-lock", "-A", "npm:rescript", "build"], - cwd: projectRoot, - stdout: "piped", - stderr: "piped", - }) - const output = await command.output() - if (!output.success) { - const detail = decoder.decode(output.stderr) || - decoder.decode(output.stdout) - throw new Error(`ReScript build failed:\n${detail}`) +async function loadCartridges() { + const dir = new URL("cartridges/", root); + const out = []; + async function walk(d) { + let entries; + try { + entries = Deno.readDir(d); + } catch { + return; + } + for await (const e of entries) { + const child = new URL(e.name + (e.isDirectory ? "/" : ""), d); + if (e.isDirectory) { + await walk(child); + continue; + } + if (!e.name.endsWith(".cartridge.json")) continue; + try { + const c = JSON.parse(await Deno.readTextFile(child)); + out.push({ + cartridge: c.name ?? e.name, + description: c.description ?? "", + languages: c.languages ?? [], + transitions: Array.isArray(c.transitions) ? c.transitions : [], + }); + } catch (err) { + console.error(`skip ${e.name}: ${err.message}`); + } + } } + await walk(dir); + return out; } -await ensureCompiled() - -const [Analyser, Output, Patterns, Types, Glyphs] = await Promise.all([ - import(new URL("../src/Analyser.res.js", import.meta.url).href), - import(new URL("../src/Output.res.js", import.meta.url).href), - import(new URL("../src/Patterns.res.js", import.meta.url).href), - import(new URL("../src/Types.res.js", import.meta.url).href), - import(new URL("../src/Glyphs.res.js", import.meta.url).href), -]) - -const viewTags = { - raw: "RAW", - folded: "FOLDED", - glyphed: "GLYPHED", -} +const contentTypes = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", +}; -const difficultyTags = { - beginner: "Beginner", - intermediate: "Intermediate", - advanced: "Advanced", +async function staticFile(name) { + const body = await Deno.readFile(new URL(name, import.meta.url)); + const ext = name.slice(name.lastIndexOf(".")); + return new Response(body, { + headers: { "content-type": contentTypes[ext] ?? "application/octet-stream" }, + }); } function json(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" }, - }) -} - -function targetFrom(value) { - return Types.stringToTargetLang(String(value || "affinescript")) || - "AffineScript" -} - -function viewFrom(value) { - return viewTags[String(value || "raw").toLowerCase()] || "RAW" -} - -function patternsForDifficulty(value) { - const key = String(value || "all").toLowerCase() - const tag = difficultyTags[key] - return tag ? Patterns.getPatternsByDifficulty(tag) : undefined + }); } -function analyseSource(payload) { - const code = String(payload.code || "") - const target = targetFrom(payload.target) - const view = viewFrom(payload.view) - const format = String(payload.format || "markdown") - const filteredPatterns = patternsForDifficulty(payload.difficulty) - const result = filteredPatterns - ? Analyser.analyseWithPatterns(code, filteredPatterns) - : Analyser.analyse(code) - - const matches = result.matches.map((match) => { - const pattern = match.pattern - const effectiveTarget = Types.patternEffectiveTarget(pattern, target) - return { - id: pattern.id, - name: pattern.name, - category: Types.categoryToString(pattern.category), - difficulty: Types.difficultyToString(pattern.difficulty), - confidence: Math.round(match.confidence * 100), - glyphs: pattern.glyphs, - tags: pattern.tags, - startLine: match.startLine, - endLine: match.endLine, - detectedCode: match.code, - jsExample: pattern.jsExample, - target: Types.targetLangLabel(effectiveTarget), - targetCode: Types.patternCodeFor(pattern, target), - fallback: effectiveTarget !== target, - narrative: pattern.narrative, - learningObjectives: pattern.learningObjectives, - bestPractices: pattern.bestPractices, - } - }) - - return { - summary: Analyser.summarise(result), - matchCount: result.matches.length, - uniquePatternCount: new Set(matches.map((match) => match.id)).size, - totalPatterns: result.totalPatterns, - coveragePercentage: Number(result.coveragePercentage.toFixed(1)), - difficulty: Types.difficultyToString(result.difficulty), - analysisTimeMs: result.analysisTime, - target: Types.targetLangLabel(target), - view, - matches, - suggestions: result.suggestedNextPatterns.map((pattern) => ({ - id: pattern.id, - name: pattern.name, - category: Types.categoryToString(pattern.category), - difficulty: Types.difficultyToString(pattern.difficulty), - })), - rendered: { - markdown: Output.format(result, view, "markdown", target), - plain: Output.format(result, view, "plain", target), - selected: Output.format(result, view, format, target), - }, - } -} - -function patternCatalogue() { - return Patterns.patternLibrary.map((pattern) => ({ - id: pattern.id, - name: pattern.name, - category: Types.categoryToString(pattern.category), - difficulty: Types.difficultyToString(pattern.difficulty), - confidence: Math.round(pattern.confidence * 100), - glyphs: pattern.glyphs, - targets: pattern.targets.map((target) => Types.targetLangLabel(target.language)), - celebrate: pattern.narrative.celebrate, - })) -} - -function stats() { - const stats = Patterns.getPatternStats() - return { - total: stats.total, - byCategory: stats.byCategory, - byDifficulty: stats.byDifficulty, - } -} - -async function staticResponse(pathname) { - const routes = { - "/": "app.html", - "/app.js": "app.js", - } - const file = routes[pathname] - if (!file) { - return undefined - } - const fileUrl = new URL(file, import.meta.url) - const body = await Deno.readFile(fileUrl) - const suffix = file.slice(file.lastIndexOf(".")) - return new Response(body, { - headers: { - "content-type": contentTypes[suffix] || "application/octet-stream", - }, - }) -} - -async function handler(request) { - const url = new URL(request.url) +async function handler(req) { + const url = new URL(req.url); try { - if (request.method === "GET") { - const staticFile = await staticResponse(url.pathname) - if (staticFile) { - return staticFile + if (req.method === "GET") { + if (url.pathname === "/") return await staticFile("app.html"); + if (url.pathname === "/app.js") return await staticFile("app.js"); + if (url.pathname === "/api/cartridges") { + return json({ cartridges: await loadCartridges() }); } - if (url.pathname === "/api/patterns") { - return json({ patterns: patternCatalogue(), stats: stats() }) - } - if (url.pathname === "/api/legend") { - return json({ legend: Glyphs.createGlyphLegend() }) - } - } - - if (request.method === "POST" && url.pathname === "/api/analyse") { - const payload = await request.json() - return json(analyseSource(payload)) } - - return json({ error: "Not found" }, 404) - } catch (error) { - console.error(error) - return json({ error: error.message || String(error) }, 500) + return json({ error: "Not found" }, 404); + } catch (err) { + console.error(err); + return json({ error: err.message ?? String(err) }, 500); } } -function openBrowser(url) { - const candidates = [ - ["sensible-browser", url], - ["gio", "open", url], - ] - for (const [command, ...args] of candidates) { - try { - new Deno.Command(command, { - args, - stdout: "null", - stderr: "null", - }).spawn() - return true - } catch { - // Try the next opener. - } - } - return false -} - -const requestedPort = Number( - argValue("--port", Deno.env.get("EVANGELISER_GUI_PORT") || "8765"), -) -const hostname = argValue("--host", "127.0.0.1") -let server -let port = requestedPort - +const requestedPort = Number(argValue("--port", envPort() ?? "8765")); +const hostname = argValue("--host", "127.0.0.1"); +let server; +let port = requestedPort; for (let attempt = 0; attempt < 20; attempt += 1) { try { - port = requestedPort + attempt - server = Deno.serve({ hostname, port, onListen: () => {} }, handler) - break - } catch (error) { - if (String(error.message || error).includes("Address already in use")) { - continue - } - throw error + port = requestedPort + attempt; + server = Deno.serve({ hostname, port, onListen: () => {} }, handler); + break; + } catch (err) { + if (String(err.message ?? err).includes("Address already in use")) continue; + throw err; } } - if (!server) { - throw new Error( - `No free port found from ${requestedPort} to ${requestedPort + 19}`, - ) + throw new Error(`No free port from ${requestedPort} to ${requestedPort + 19}`); } -const url = `http://${hostname}:${port}/` -console.log(`Nextgen Languages Evangeliser GUI: ${url}`) +const addr = `http://${hostname}:${port}/`; +console.log(`Correspondence workspace: ${addr}`); if (Deno.args.includes("--open")) { setTimeout(() => { - const opened = openBrowser(url) - if (!opened) { - console.log(`Open this URL manually: ${url}`) + for (const cmd of [["sensible-browser", addr], ["gio", "open", addr], ["xdg-open", addr]]) { + try { + new Deno.Command(cmd[0], { args: cmd.slice(1), stdout: "null", stderr: "null" }).spawn(); + return; + } catch { + // try the next opener + } } - }, 250) + }, 250); } -await server.finished +await server.finished; From abe6330b5e29b8cc94b495af5a54d3ca23df02b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 04:53:36 +0000 Subject: [PATCH 10/17] Remove legacy ReScript host; Deno + cartridges is the runtime surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo began as ReScript Evangeliser. The engine has been re-pointed to the correspondence model (Idris2 ABI + Zig FFI + AffineScript host binding) with a Deno + cartridge-data runtime surface, so the legacy ReScript host — flagged by the governance anti-pattern check — is now removed. Removed: - All ReScript host sources: src/*.res, src/*.resi (Types, Glyphs, Narrative, Patterns, Scanner, Analyser, Output, Cli) and the six test/*_test.res suites - rescript.json, scripts/rescript-build.sh, and the ReScript Deno tasks/imports in deno.json (plus nodeModulesDir) - Dead .res/.resi entries in .gitignore / .editorconfig / .gitattributes - deno.lock from the index (it is .gitignored; regenerated locally) Replaced (Deno-only, no ReScript): - bin/evangeliser.js — offline CLI reading the cartridges (--kind / --find / --json) - test/run_all.js — cartridge invariant tests mirroring the Idris2 ABI (false-friend signature, residue direction, all six kinds) — 59 checks - Justfile build/watch/clean/install/test/stats/validate-structure recipes - deno.json tasks repointed (build = validate cartridges; gui; test) Docs/manifests now state the host is removed while KEEPING ReScript as a legacy *target* language in correspondence content (the vanished-`return` example, the cartridge target, the Form examples): README, EXPLAINME, ROADMAP, CLAUDE.md, .claude/CLAUDE.md, TOPOLOGY, TEST-NEEDS, Mustfile.epx, config.ncl, CHANGELOG, and .machine_readable/*. Verified locally with Deno 2.8.3: deno fmt --check, deno lint, deno task test (59/59), deno task build/validate (1/1 cartridges), CLI + GUI live probe all green. Licence files and SPDX headers left untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .claude/CLAUDE.md | 28 +- .editorconfig | 6 - .gitattributes | 7 +- .github/workflows/rsr-antipattern.yml | 2 +- .github/workflows/ts-blocker.yml | 4 +- .gitignore | 9 - .machine_readable/6a2/META.a2ml | 2 +- .machine_readable/6a2/PLAYBOOK.a2ml | 6 +- .machine_readable/META.a2ml | 2 +- .machine_readable/STATE.a2ml | 27 +- .machine_readable/bot_directives/roles.a2ml | 2 +- CHANGELOG.adoc | 14 + CLAUDE.md | 46 +- EXPLAINME.adoc | 49 +- Justfile | 51 +- Mustfile.epx | 26 +- README.adoc | 71 +- ROADMAP.adoc | 40 +- TEST-NEEDS.md | 23 +- TOPOLOGY.md | 10 +- bin/evangeliser.js | 151 +- config.ncl | 16 +- deno.json | 18 +- deno.lock | 66 - gui/EvangeliserGui.affine | 6 +- gui/app.html | 499 ++++- gui/app.js | 365 ++-- gui/server.js | 94 +- rescript.json | 27 - scripts/rescript-build.sh | 15 - scripts/validate-cartridges.js | 40 +- src/Analyser.res | 134 -- src/Analyser.resi | 11 - src/Cli.res | 192 -- src/Glyphs.res | 304 ---- src/Narrative.res | 329 ---- src/Output.res | 223 --- src/Patterns.res | 1803 ------------------- src/Scanner.res | 112 -- src/Scanner.resi | 14 - src/Types.res | 280 --- test/Analyser_test.res | 127 -- test/Glyphs_test.res | 104 -- test/Narrative_test.res | 110 -- test/Patterns_test.res | 143 -- test/Scanner_test.res | 155 -- test/Types_test.res | 64 - test/run_all.js | 131 +- 48 files changed, 1213 insertions(+), 4745 deletions(-) delete mode 100644 deno.lock delete mode 100644 rescript.json delete mode 100755 scripts/rescript-build.sh delete mode 100644 src/Analyser.res delete mode 100644 src/Analyser.resi delete mode 100644 src/Cli.res delete mode 100644 src/Glyphs.res delete mode 100644 src/Narrative.res delete mode 100644 src/Output.res delete mode 100644 src/Patterns.res delete mode 100644 src/Scanner.res delete mode 100644 src/Scanner.resi delete mode 100644 src/Types.res delete mode 100644 test/Analyser_test.res delete mode 100644 test/Glyphs_test.res delete mode 100644 test/Narrative_test.res delete mode 100644 test/Patterns_test.res delete mode 100644 test/Scanner_test.res delete mode 100644 test/Types_test.res diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a6b7e7b..f9e2a17 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -8,7 +8,7 @@ - **Model:** Concept / Form / Transition; six `CorrespondenceKind`s (cognate / false-friend / antonym / alien-realization / novel / vanished) as graded Echo fibres; classification runs per stratum. Carrier = Dyadic relation + Echo loss-with-residue (`proven-tests-and-benches` + `echo-types`). - **Division of labour:** we build the engine + interface + vocabulary; the community authors per-language **cartridges** (`standards/cartridges/`). -Host: **ReScript today** (legacy host, being migrated; banned in *new* code per estate policy → AffineScript `.affine`). **AffineScript** is the future host and a first-class target (Zig FFI + Idris2 ABI seams). +Host: **ReScript host removed** (2026-06) — ReScript remains only as a legacy *target* language in correspondence content (cartridges/examples), never host code. The current runtime surface is **Deno + cartridge data**; **AffineScript** is the future host and a first-class target (Zig FFI + Idris2 ABI seams). When working here: classify-don't-translate; keep the no-shame voice; lead with the correspondence model (AffineScript is one first-class target/host, not the sole pitch); author-now / verify-in-CI when the toolchain is absent; **MPL-2.0 SPDX on new files, never relicense or sweep SPDX (licence-label drift is FLAG-ONLY to the owner)**. @@ -19,7 +19,7 @@ When working here: classify-don't-translate; keep the no-shame voice; lead with | Language/Tool | Use Case | Notes | |---------------|----------|-------| | **AffineScript** | Flagship target language; future host | Affine/linear types, borrow checker, QTT, WASM backend | -| **ReScript** | Current host application code; legacy target | Compiles to JS, type-safe | +| **ReScript** | Legacy *target* language only (host removed) | Appears in cartridges/examples; not host code | | **Deno** | Runtime & package management | Replaces Node/npm/bun | | **Rust** | Performance-critical, systems, WASM | Preferred for CLI tools | | **Zig** | FFI, C-ABI bridges, systems | Canonical FFI layer (per `0-AI-MANIFEST.a2ml`) | @@ -28,7 +28,7 @@ When working here: classify-don't-translate; keep the no-shame voice; lead with | **Gleam** | Backend services | Runs on BEAM or compiles to JS | | **Elixir** | BEAM supervision and bot-role orchestration metadata | Support role only; not the host language or flagship target | | **Bash/POSIX Shell** | Scripts, automation | Keep minimal | -| **JavaScript** | Only where ReScript/AffineScript cannot | MCP protocol glue, Deno APIs | +| **JavaScript** | Only where AffineScript cannot (Deno glue) | Workspace server, CLI, cartridge tooling | | **Nickel** | Configuration language | For complex configs | | **Guile Scheme** | State/meta files | STATE.scm, META.scm, ECOSYSTEM.scm | | **Julia** | Batch scripts, data processing | Per RSR | @@ -39,13 +39,13 @@ When working here: classify-don't-translate; keep the no-shame voice; lead with | Banned | Replacement | |--------|-------------| -| TypeScript | AffineScript (preferred) or ReScript | +| TypeScript | AffineScript | | Node.js | Deno | | npm | Deno | | Bun | Deno | | pnpm/yarn | Deno | | Go | Rust or Zig | -| Python | Julia/Rust/ReScript/AffineScript | +| Python | Julia/Rust/AffineScript | | Java/Kotlin | Rust/Tauri/Dioxus | | Swift | Tauri/Dioxus | | React Native | Tauri/Dioxus | @@ -56,40 +56,42 @@ When working here: classify-don't-translate; keep the no-shame voice; lead with **No exceptions for Kotlin/Swift** - use Rust-first approach: -1. **Tauri 2.0+** - Web UI (ReScript/AffineScript) + Rust backend, MIT/Apache-2.0 +1. **Tauri 2.0+** - Web UI (AffineScript) + 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 AffineScript or ReScript +1. **No new TypeScript files** - Convert existing TS to AffineScript 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 or Zig instead -5. **No Python anywhere** - Use Julia for data/batch, Rust for systems, ReScript/AffineScript for apps +5. **No Python anywhere** - Use Julia for data/batch, Rust for systems, AffineScript for apps 6. **No Kotlin/Swift for mobile** - Use Tauri 2.0+ or Dioxus 7. **No V outside the V ecosystem** - Use Zig ### JavaScript Exemptions -JavaScript is allowed only when it is generated ReScript output, Deno/browser runtime glue, or a launcher bridge that cannot currently be written in AffineScript without losing operability. +JavaScript is allowed only when it is Deno/browser runtime glue or a launcher bridge that cannot currently be written in AffineScript without losing operability. (The ReScript host has been removed, so there is no longer any generated-ReScript-output JS.) | Path | Files | Rationale | Unblock condition | |------|-------|-----------|-------------------| -| `bin/evangeliser.js` | 1 | Deno CLI shim importing compiled ReScript output | Replace when AffineScript host CLI is runnable directly | -| `gui/server.js` | 1 | Local Deno HTTP bridge for the AffineScript GUI contract and compiled ReScript analyser | Replace when AffineScript-to-Deno/webview bridge is available | +| `bin/evangeliser.js` | 1 | Deno CLI reading correspondence cartridges (offline fallback) | Replace when AffineScript host CLI is runnable directly | +| `gui/server.js` | 1 | Local Deno HTTP bridge serving the workspace + cartridge API | Replace when AffineScript-to-Deno/webview bridge is available | | `gui/app.js` | 1 | Browser-side event/render bridge for the GUI shell | Replace when AffineScript DOM/TEA bridge can drive this UI directly | +| `scripts/validate-cartridges.js` | 1 | Deno cartridge-schema validator (ajv) | Replace when AffineScript can host the validator | +| `test/run_all.js` | 1 | Deno cartridge invariant tests | Replace when AffineScript test runner is runnable directly | ### TypeScript Exemptions | Path | Files | Rationale | Unblock condition | |------|-------|-----------|-------------------| -| `node_modules/` | generated | Deno-managed npm compatibility cache for ReScript tooling | Do not commit; local cache only | +| _(none)_ | — | No TypeScript in the repo. The former `node_modules/` ReScript-tooling cache is obsolete now the ReScript host is removed. | — | ### BEAM / Elixir Roles -Elixir is a support-role language for BEAM supervision of automation and fleet orchestration. It is not a target language in this repo and must not displace the ReScript host or AffineScript flagship path. +Elixir is a support-role language for BEAM supervision of automation and fleet orchestration. It is not a target language in this repo and must not displace the AffineScript host path. | Role | Owner | Purpose | Boundary | |------|-------|---------|----------| diff --git a/.editorconfig b/.editorconfig index 4c14a5e..9ef239e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -41,12 +41,6 @@ indent_size = 3 [*.hs] indent_size = 2 -[*.res] -indent_size = 2 - -[*.resi] -indent_size = 2 - [*.ncl] indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 7d0c1eb..7251ce6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,8 +8,9 @@ *.ex text eol=lf diff=elixir *.exs text eol=lf diff=elixir *.jl text eol=lf -*.res text eol=lf -*.resi text eol=lf +*.affine text eol=lf +*.idr text eol=lf +*.zig text eol=lf *.ada text eol=lf diff=ada *.adb text eol=lf diff=ada *.ads text eol=lf diff=ada @@ -52,5 +53,3 @@ Containerfile text eol=lf # Lock files Cargo.lock text eol=lf -diff flake.lock text eol=lf -diff -# Pending estate-wide policy: hide ReScript from primary-language detection. -**/*.res linguist-detectable=false diff --git a/.github/workflows/rsr-antipattern.yml b/.github/workflows/rsr-antipattern.yml index 0541bc4..82f57e4 100644 --- a/.github/workflows/rsr-antipattern.yml +++ b/.github/workflows/rsr-antipattern.yml @@ -165,7 +165,7 @@ jobs: - name: Check for tsconfig run: | if [ -f "tsconfig.json" ]; then - echo "❌ tsconfig.json detected - use ReScript instead" + echo "❌ tsconfig.json detected - use AffineScript instead" exit 1 fi echo "✅ No tsconfig.json" diff --git a/.github/workflows/ts-blocker.yml b/.github/workflows/ts-blocker.yml index 9bebeaf..fef3644 100644 --- a/.github/workflows/ts-blocker.yml +++ b/.github/workflows/ts-blocker.yml @@ -27,9 +27,9 @@ jobs: NEW_JS=$(git diff --name-only --diff-filter=A HEAD~1 2>/dev/null | grep -E '\.(js|jsx)$' | grep -v '\.res\.js$' | grep -v '\.gen\.' | grep -v 'node_modules' | grep -v '^bin/evangeliser\.js$' | grep -v '^gui/server\.js$' | grep -v '^gui/app\.js$' || true) if [ -n "$NEW_TS" ] || [ -n "$NEW_JS" ]; then - echo "❌ New TS/JS files detected outside documented Deno/ReScript bridge exemptions." + echo "❌ New TS/JS files detected outside documented Deno bridge exemptions." [ -n "$NEW_TS" ] && echo "$NEW_TS" [ -n "$NEW_JS" ] && echo "$NEW_JS" exit 1 fi - echo "✅ ReScript policy enforced" + echo "✅ JS/TS bridge policy enforced" diff --git a/.gitignore b/.gitignore index 31e6026..3e6380e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,15 +37,6 @@ erl_crash.dump *.jl.mem /Manifest.toml -# ReScript -/lib/bs/ -/lib/ -/.bsb.lock -*.res.js -*.bs.js -.merlin -!/test/run_all.js - # Deno deno.lock .deno/ diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/6a2/META.a2ml index 5883426..44689b5 100644 --- a/.machine_readable/6a2/META.a2ml +++ b/.machine_readable/6a2/META.a2ml @@ -15,7 +15,7 @@ decisions = [ "ADR-1 Classify, don't translate — name the CorrespondenceKind + residue; no universal translator, no Curry-Howard-fidelity goal", "ADR-2 Carrier = Dyadic relation + Echo loss-with-residue (proven-tests-and-benches + echo-types); invariant-path is the governance front-end", "ADR-3 Engine vs cartridge — we build the engine/interface/vocabulary; per-language facts are cartridges (standards/cartridges)", - "ADR-4 Host ReScript -> AffineScript (Zig FFI + Idris2 ABI seams); ReScript banned in new code", + "ADR-4 Host ReScript -> AffineScript (Zig FFI + Idris2 ABI seams); ReScript host removed (Deno + cartridges interim), retained only as a legacy target language", "ADR-5 Emit into PanLL (octads -> VeriSimDB -> panels); not itself the IDE", "ADR-6 Knot-theory framing is an aspirational lens, not a literal computation", ] diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/6a2/PLAYBOOK.a2ml index f1ee58a..0827bf7 100644 --- a/.machine_readable/6a2/PLAYBOOK.a2ml +++ b/.machine_readable/6a2/PLAYBOOK.a2ml @@ -8,9 +8,9 @@ last-updated = "2026-06-18" project = "nextgen-languages-evangeliser" [deployment] -# Host: ReScript today (compiles to JS, run under Deno); AffineScript after the -# Phase-3 host port. Surfaces: Deno CLI (bin/evangeliser.js) and the browser -# GUI (gui/server.js + gui/app.js). +# Host: ReScript host removed — interim surface is Deno + cartridge data; +# AffineScript after the host port. Surfaces: Deno CLI (bin/evangeliser.js, +# reads cartridges) and the browser GUI (gui/server.js + gui/app.js). # method = "ci-triggered" # target = "library + cli + browser-gui" diff --git a/.machine_readable/META.a2ml b/.machine_readable/META.a2ml index 94a53cb..808dfa3 100644 --- a/.machine_readable/META.a2ml +++ b/.machine_readable/META.a2ml @@ -10,7 +10,7 @@ (adr-1 "Classify, don't translate — the engine names the kind of cross-language correspondence (CorrespondenceKind) and its residue; it is not a universal translator and claims no Curry-Howard fidelity") (adr-2 "Formal carrier = Dyadic relation + Echo loss-with-residue (proven-tests-and-benches Dyadic.idr + hyperpolymath/echo-types); invariant-path is the governance front-end") (adr-3 "Engine vs cartridge — we build the general engine, interface and vocabulary; per-language facts are authored as cartridges (standards/cartridges)") - (adr-4 "Host moves ReScript -> AffineScript with Zig FFI + Idris2 ABI seams; ReScript banned in new code per estate policy") + (adr-4 "Host moves ReScript -> AffineScript with Zig FFI + Idris2 ABI seams; the ReScript host has been removed (Deno + cartridges is the interim surface), ReScript retained only as a legacy target language") (adr-5 "The engine emits into PanLL (octads -> VeriSimDB -> panels); it is not itself the IDE") (adr-6 "Knot-theory framing is an aspirational lens, not a literal computation — documented honestly")) (development-practices diff --git a/.machine_readable/STATE.a2ml b/.machine_readable/STATE.a2ml index c252f0e..2d26cbb 100644 --- a/.machine_readable/STATE.a2ml +++ b/.machine_readable/STATE.a2ml @@ -19,23 +19,24 @@ (current-position (phase "Re-point: cross-language correspondence engine + abstraction-model pivot") - (overall-completion 58) + (overall-completion 66) (working-features "Correspondence-model design spec (docs/theory/CORRESPONDENCE-MODEL.adoc) — merged" "Standards/machine-readable hygiene (6a2 manifests, panic-attack gate, eNSAID config) — merged" - "Scanner + Analyser detection engine (ReScript host)" - "Narrative generation (no-shame voice)" - "Makaton-inspired glyph view-layer" - "CLI: scan / patterns / legend / stats" - "RAW/FOLDED/GLYPHED view layers; plain/markdown/html outputs")) + "Idris2 ABI spine (Carrier.idr + Correspondence.idr) — typechecks; Zig FFI mirror; AffineScript host binding" + "Cartridge contract (JSON-Schema) + reference pack (one correspondence per CorrespondenceKind)" + "Browser multi-pane workspace (index / forms / classification; five overlay view-layers) — merged" + "Offline CLI (Deno; reads cartridges): --kind / --find / --json" + "Cartridge invariant tests (Deno; mirrors Abi.Correspondence)" + "Legacy ReScript host removed — runtime surface is Deno + cartridges")) (route-to-mvp (milestones (m0 (status "complete") (items "Correspondence-model spec: Concept/Form/Transition + six kinds + Dyadic/Echo carrier")) (m1 (status "complete") (items "Standards/repo hygiene: 6a2 manifests, panic-attack gate, eNSAID config")) - (m2 (status "in-progress") (items "Identity re-point" "Abstraction-model pivot: Concept/Form/Transition + CorrespondenceKind in src")) - (m3 (status "pending") (items "Browser multi-pane GUI + overlay view-layers + HAS accessibility")) - (m4 (status "gated") (items "AffineScript host port (.res -> .affine) + Zig FFI + Idris2 ABI seams")) + (m2 (status "in-progress") (items "Identity re-point" "Abstraction-model pivot: Concept/Form/Transition + CorrespondenceKind as cartridge facts")) + (m3 (status "complete") (items "Browser multi-pane GUI + overlay view-layers + HAS accessibility")) + (m4 (status "gated") (items "AffineScript host application (ReScript host removed; Zig FFI + Idris2 ABI seams authored)")) (m5 (status "pending") (items "Cartridge contract + 2nd language pack")) (m6 (status "pending") (items "Idris2/Echo witnesses + benches + PanLL octad emission")))) @@ -45,9 +46,11 @@ "Licence-label drift to flag to owner (PMPL badge / 'Palimpsest License (MPL-2.0)' / ROADMAP 'MPL-2.0-or-later') vs MPL-2.0 SPDX — FLAG-ONLY, manual") (critical-next-actions - "Abstraction-model pivot: recast Types/Patterns/Narrative/Analyser to Concept/Form/Transition + CorrespondenceKind" - "Browser multi-pane GUI surface") + "Port the legacy pattern catalogue into cartridge facts (Concept/Form/Transition + CorrespondenceKind)" + "Author a second language pack to prove engine-agnosticism" + "Begin the AffineScript host application against the Idris2 ABI / Zig FFI seams") (session-history (2026-04-16 "Phase 0 decisions locked; Phase 1 started") - (2026-06-18 "Re-point: correspondence-model spec merged (#19); standards hygiene merged (#20); identity re-point"))) + (2026-06-18 "Re-point: correspondence-model spec merged (#19); standards hygiene merged (#20); identity re-point") + (2026-06-27 "ABI/FFI/host spine + multi-pane GUI merged; legacy ReScript host removed (Deno + cartridges)"))) diff --git a/.machine_readable/bot_directives/roles.a2ml b/.machine_readable/bot_directives/roles.a2ml index 2863d16..c038e33 100644 --- a/.machine_readable/bot_directives/roles.a2ml +++ b/.machine_readable/bot_directives/roles.a2ml @@ -11,7 +11,7 @@ scope = "nextgen-languages-evangeliser" [elixir] status = "support-role" purpose = "BEAM/OTP supervision model for automation roles" -boundary = "Elixir may coordinate bot work; repository implementation remains ReScript now and AffineScript after the host migration gate." +boundary = "Elixir may coordinate bot work; the ReScript host has been removed — repository implementation is Deno + cartridges now, and AffineScript after the host migration gate." [[roles]] name = "hypatia" diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6fdbd9f..d57878f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -8,6 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 == [Unreleased] +=== Re-point (2026-06) — correspondence engine; ReScript host removed + +==== Added +- Correspondence model spine: Idris2 ABI (`src/interface/Abi/` — typechecks), Zig FFI mirror (`src/interface/ffi/`), AffineScript host binding (`src/interface/host/`) +- Cartridge contract (JSON-Schema 2020-12) + reference pack (one correspondence per CorrespondenceKind); browser multi-pane workspace (`gui/`) +- Offline Deno CLI reading cartridges (`bin/evangeliser.js`: `--kind` / `--find` / `--json`); cartridge invariant tests (`test/run_all.js`) + +==== Changed +- Identity re-pointed to "Duolingo / Rosetta Stone for programming languages" — *classify, don't translate* +- Runtime surface is now Deno + cartridge data (no compile step); `just build` validates cartridges + +==== Removed +- **Legacy ReScript host** — all `src/*.res` / `src/*.resi` / `test/*_test.res` sources, `rescript.json`, `scripts/rescript-build.sh`, and the ReScript Deno tasks/imports. ReScript remains only as a legacy *target* language in the correspondence catalogue. + === Added - Migration to **Nextgen Languages Evangeliser** — multi-target pattern catalogue - AffineScript added as flagship target (successor to ReScript as the teaching subject) diff --git a/CLAUDE.md b/CLAUDE.md index 2367d39..72871ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ **Nextgen Languages Evangeliser** is **Duolingo / Rosetta Stone for programming languages** — a language-*comprehension and transfer* engine. It works one layer above text editing, on *syntactic and semantic intention*, and **classifies** cross-language correspondences so the effort spent learning one language **transfers** to the next. *Transfer learning across languages is the product.* -Canonical design: `docs/theory/CORRESPONDENCE-MODEL.adoc` — this file mirrors it. The repo began as *ReScript Evangeliser*; ReScript is now a legacy host (being migrated) and a legacy target. +Canonical design: `docs/theory/CORRESPONDENCE-MODEL.adoc` — this file mirrors it. The repo began as *ReScript Evangeliser*; the ReScript host has now been **removed** — ReScript remains only as a legacy *target* language in the correspondence catalogue. The runtime surface is Deno + cartridge data; AffineScript is the future host. ### What it is NOT - **Not an IDE** — that is **PanLL + eNSAID** (the *contact* between human, tool, task, environment). This engine *feeds* PanLL; it is not PanLL. @@ -36,34 +36,31 @@ Emits `octads` → VeriSimDB (`:8097`) + Groove signals → **PanLL** panels; vi ## Technology Stack -- **ReScript** — current / legacy host (being ported; banned in *new* code per estate policy: `.res` → `.affine`) +- **Deno** — runtime, tooling & current host surface (workspace server + CLI + cartridge data); not npm/bun - **AffineScript** — future host + first-class target (Zig FFI + Idris2 ABI seams; OCaml 5.1+; emits typed-wasm IR → WebAssembly) -- **Deno** — runtime & package management (not npm/bun) - **Zig** — FFI layer; **Idris2** — ABI / proofs - **Nickel** — configuration +- **ReScript** — *removed* as host (2026-06); retained only as a legacy *target* language in the catalogue ### Language Policy (Hyperpolymath Standard) -**ALLOWED:** AffineScript (future host), ReScript (legacy host/target), Deno, Zig (FFI), Idris2 (ABI/proofs), Bash/POSIX shell, JavaScript (glue only — see exemptions in `.claude/CLAUDE.md`), Nickel. +**ALLOWED:** AffineScript (future host), Deno, Zig (FFI), Idris2 (ABI/proofs), Bash/POSIX shell, JavaScript (glue only — see exemptions in `.claude/CLAUDE.md`), Nickel. ReScript is allowed only as a legacy *target* language in correspondence content (cartridges, examples), never as host code. -**BANNED:** TypeScript, Node.js, npm/bun, Makefile (use Justfile), V (outside the V ecosystem), Python, Go. **ReScript is banned in _new_ code** — migrate `.res` → `.affine` directly. +**BANNED:** TypeScript, Node.js, npm/bun, Makefile (use Justfile), V (outside the V ecosystem), Python, Go. **ReScript is banned in _new_ host code** — the ReScript host has been removed; do not reintroduce `.res` host source (`.affine` is the host path). ## Project Structure ``` nextgen-languages-evangeliser/ -├── src/ # Host source (ReScript today; AffineScript target) -│ ├── Types.res # Core type model (→ Concept / Form / Transition) -│ ├── Glyphs.res # Makaton-inspired glyph view-layer -│ ├── Narrative.res # Shame-free narrative (per CorrespondenceKind) -│ ├── Patterns.res # Correspondence catalogue (→ reference language pack) -│ ├── Scanner.res # Detection engine -│ ├── Analyser.res # Classification + aggregation -│ ├── Output.res # focus/glyph/blockly/raw/side-by-side view-layers -│ └── Cli.res # CLI entry point (offline fallback) -├── gui/ # Browser multi-pane workspace (primary surface) +├── src/interface/ # Engine spine (no host application code yet) +│ ├── Abi/ # Idris2 ABI — Carrier.idr, Correspondence.idr (typechecked) +│ ├── ffi/ # Zig C-ABI mirror (build.zig + src/ + test/) +│ └── host/ # AffineScript host binding (Correspondence.affine; compiler pending) +├── cartridges/ # Correspondence facts (the "lessons"): schema + reference pack +├── gui/ # Browser multi-pane workspace (primary surface; Deno + static) +├── bin/evangeliser.js # Offline CLI (Deno; reads cartridges) +├── test/run_all.js # Cartridge invariant tests (Deno) ├── docs/theory/CORRESPONDENCE-MODEL.adoc # Canonical design spec -├── test/ # Test suites ├── Justfile # Task orchestration (NOT Makefile) └── deno.json # Deno configuration ``` @@ -71,10 +68,11 @@ nextgen-languages-evangeliser/ ## Common Commands ```bash -just install # Install dependencies (Deno) -just build # Build host sources -just watch # Watch mode -just test # Run tests +just install # Warm dependency cache (Deno) +just build # Validate cartridge data (no compile step — Deno-only) +just gui # Launch the browser correspondence workspace +just watch # Serve workspace with live reload +just test # Run cartridge invariant tests just assail # panic-attack static-analysis scan just validate-rsr # RSR compliance just fmt # Format @@ -88,9 +86,11 @@ We **never** shame developers. The voice stays **celebrate / minimise / better / - ✅ Correspondence-model spec merged (Concept/Form/Transition + six kinds + Dyadic/Echo carrier) - ✅ Standards / repo hygiene merged (6a2 manifests, panic-attack gate, eNSAID config) +- ✅ Idris2 ABI spine + Zig FFI mirror + AffineScript host binding authored (ABI typechecks) +- ✅ Browser multi-pane GUI (overlay view-layers) merged +- ✅ Legacy ReScript host **removed** — runtime surface is Deno + cartridges - 🚧 Identity re-point + abstraction-model pivot (classify, not translate) -- → Browser multi-pane GUI (overlay view-layers) -- → AffineScript host port (Zig FFI + Idris2 ABI) +- → AffineScript host port (Zig FFI + Idris2 ABI) — fill in the host application - → Cartridge contract + second language pack - → Proofs/benches + PanLL octad emission @@ -100,7 +100,7 @@ We **never** shame developers. The voice stays **celebrate / minimise / better / - **Classify, don't translate.** No universal-translator or Curry–Howard-fidelity claims. - **No shame.** Keep the celebrate / minimise / better / safety / example voice. - **Use Deno, not npm/bun. Use Justfile, not Makefile. Use Zig for FFI, Idris2 for ABI/proofs.** -- **ReScript is banned in new code** → AffineScript (`.affine`). +- **The ReScript host is removed.** ReScript is allowed only as a legacy *target* in correspondence content; never reintroduce `.res` host code — the host path is AffineScript (`.affine`). - Toolchain may be absent in this environment — **author now, verify in CI**; never claim a green build you did not run. - **Licence: MPL-2.0 (sole-owner repo). Add SPDX `MPL-2.0` to new files. NEVER relicense or bulk-sweep SPDX; licence-label drift is FLAG-ONLY to the owner.** diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index 20a7654..d141632 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -212,10 +212,13 @@ Transition carries a witness). Ergonomics are read from === Where the host language is going -The host *today* is ReScript — now a *legacy host being migrated*, and a legacy -*target*. Per estate policy ReScript is banned in *new* code (`.res` → `.affine`). The -host is moving to *AffineScript*, with *Zig FFI + Idris2 ABI* seams. AffineScript stays a -first-class teaching target and the future host — but the project's frame is no longer +The ReScript host has been *removed*: there is no `.res` host source left. ReScript +remains only as a legacy *target* language in the correspondence catalogue (it is the +source of the _vanished_ `return` example). The runtime surface today is *Deno + cartridge +data* — the workspace server, the offline CLI, and the validated cartridges. The host is +moving to *AffineScript*, with *Zig FFI + Idris2 ABI* seams (the ABI typechecks under +Idris2 today; the AffineScript host application is still to be written). AffineScript stays +a first-class teaching target and the future host — but the project's frame is no longer "evangelise AffineScript specifically". It is "teach *any* language by transfer, with AffineScript among the first-class targets". @@ -230,10 +233,13 @@ exemplars, with one worked example (JTV grammar v2) stubbed pending repository a * Correspondence-model spec: *merged* (canonical design). * Standards / repo hygiene: *merged*. +* Idris2 ABI spine + Zig FFI mirror + AffineScript host binding: *authored* (ABI typechecks). +* Browser multi-pane GUI: *merged*. +* Legacy ReScript host: *removed* — runtime surface is Deno + cartridges. * Now: identity re-point + abstraction-model pivot (Concept/Form/Transition + CorrespondenceKind in the engine). -* Next: browser multi-pane GUI; AffineScript host port; cartridge contract + a second - language pack; proofs/benches + PanLL octad emission. +* Next: AffineScript host application; cartridge contract + a second language pack; + proofs/benches + PanLL octad emission. See link:ROADMAP.adoc[ROADMAP.adoc] for the sequenced plan, and link:README.adoc[README.adoc] for the front-door summary. @@ -247,24 +253,27 @@ link:README.adoc[README.adoc] for the front-door summary. | `docs/theory/CORRESPONDENCE-MODEL.adoc` | Canonical design spec — Concept/Form/Transition, six CorrespondenceKinds, strata, Dyadic+Echo carrier -| `src/Patterns.res` -| Correspondence / pattern catalogue (target source for cartridge facts) +| `cartridges/correspondence-cartridge.schema.json` +| Cartridge contract — the JSON-Schema for correspondence facts (Concept/Form/Transition + kind + residue + strata) -| `src/Analyser.res` / `src/Analyser.resi` -| Classification + aggregation engine with public interface (per-stratum, kind-aware) +| `cartridges/reference/worked-examples.cartridge.json` +| Reference pack — one correspondence per CorrespondenceKind (the catalogue the GUI + CLI present) -| `src/Narrative.res` -| Shame-free narratives keyed by CorrespondenceKind (transfer / flag / remap / bridge / teach / re-route) +| `src/interface/Abi/Carrier.idr`, `src/interface/Abi/Correspondence.idr` +| Idris2 ABI — Relation/Residue/CorrespondenceKind/Stratum + `isFalseFriendShape` (typechecks under Idris2) -| `src/Glyphs.res` -| Makaton-inspired glyph view-layer — accessible, low-cognitive-load symbol mode +| `src/interface/ffi/` (`build.zig`, `src/main.zig`, `test/`) +| Zig C-ABI mirror of the ABI (`nle_pedagogy_of`, `nle_residue_is_iso`, `nle_is_false_friend`, …) -| `src/Scanner.res` / `src/Scanner.resi` -| File/project scanner with public interface — offline analysis entry point +| `src/interface/host/Correspondence.affine` +| AffineScript host binding (compiler-gated; the future host application attaches here) -| `src/Output.res` -| Overlay view-layers (focus / glyph / blockly / raw / side-by-side) +| `gui/server.js`, `gui/app.html`, `gui/app.js` +| Browser multi-pane workspace (primary surface): index · forms · classification, five overlay view-layers -| `src/Cli.res` -| CLI entry point (offline fallback): `scan`, `patterns`, `legend`, `stats` +| `bin/evangeliser.js` +| Offline CLI (Deno; reads cartridges): `--kind`, `--find`, `--json` + +| `test/run_all.js` +| Cartridge invariant tests (Deno) — mirrors `Abi.Correspondence` (false-friend signature, residue direction) |=== diff --git a/Justfile b/Justfile index 21322bc..872e470 100644 --- a/Justfile +++ b/Justfile @@ -5,7 +5,7 @@ # Per Hyperpolymath policy: # - Use Deno, not npm/bun # - Use justfile, not Makefile -# - Use ReScript, not TypeScript +# - Use AffineScript, not TypeScript (ReScript host removed 2026-06; AffineScript host pending) # List all available recipes import? "contractile.just" @@ -15,20 +15,22 @@ default: # === BUILD === -# Build ReScript sources +# "Build" = validate the cartridge data the workspace + CLI consume. +# There is no compile step: the host is AffineScript (compiler pending) and the +# runtime surface is Deno + cartridge data (the ReScript host was removed). build: - @echo "🔷 Building ReScript..." + @echo "🧩 Validating cartridges (no compile step — Deno-only host)..." deno task build -# Build in watch mode +# Serve the workspace with live reload on change watch: - @echo "👀 Watching for changes..." - npx rescript build -w + @echo "👀 Watching — workspace reloads on change..." + deno run --allow-read --allow-net --watch gui/server.js -# Clean build artifacts +# Clean caches / build leftovers clean: @echo "🧹 Cleaning..." - deno task clean + @rm -rf lib .deno 2>/dev/null || true # Deep clean (including dependencies) clean-all: clean @@ -40,17 +42,16 @@ rebuild: clean-all setup build # === DEVELOPMENT === -# Install dependencies +# Install / warm dependency cache (Deno — no npm runtime deps) install: - @echo "📦 Installing dependencies..." - deno cache scripts/*.ts - deno install + @echo "📦 Warming dependency cache..." + @deno cache scripts/validate-cartridges.js gui/server.js bin/evangeliser.js test/run_all.js # First-time setup setup: install @echo "🎉 Development environment ready!" - @echo "Run 'just build' to compile ReScript" - @echo "Run 'just watch' for development" + @echo "Run 'just gui' to launch the correspondence workspace" + @echo "Run 'just test' to run the cartridge invariant tests" # Format code fmt: @@ -100,7 +101,7 @@ validate-rsr: validate-structure: @echo "📁 Checking project structure..." @test -d src || (echo "❌ Missing src/" && exit 1) - @test -f rescript.json || (echo "❌ Missing rescript.json" && exit 1) + @test -d cartridges || (echo "❌ Missing cartridges/" && exit 1) @test -f deno.json || (echo "❌ Missing deno.json" && exit 1) @echo "✅ Project structure valid" @@ -134,7 +135,7 @@ validate-policy: @echo "📋 Checking language policy..." @test ! -f Makefile || (echo "❌ Makefile detected - use justfile" && exit 1) @test ! -f makefile || (echo "❌ makefile detected - use justfile" && exit 1) - @! find src -name "*.ts" -o -name "*.tsx" 2>/dev/null | grep -q . || (echo "❌ TypeScript in src/ - use ReScript" && exit 1) + @! find src -name "*.ts" -o -name "*.tsx" 2>/dev/null | grep -q . || (echo "❌ TypeScript in src/ - use AffineScript" && exit 1) @echo "✅ Language policy enforced" # === CI/CD === @@ -157,14 +158,18 @@ ci: clean setup build lint test validate-rsr stats: @echo "📊 Project Statistics" @echo "====================" - @echo "ReScript files:" - @find src -name "*.res" 2>/dev/null | wc -l || echo "0" - @echo "Lines of ReScript:" - @find src -name "*.res" -exec cat {} \; 2>/dev/null | wc -l || echo "0" - @echo "Deno scripts:" - @find scripts -name "*.ts" 2>/dev/null | wc -l || echo "0" + @echo "Cartridges:" + @find cartridges -name "*.cartridge.json" 2>/dev/null | wc -l || echo "0" + @echo "AffineScript host files (.affine):" + @find . -name "*.affine" -not -path './.git/*' 2>/dev/null | wc -l || echo "0" + @echo "Idris2 ABI files (.idr):" + @find . -name "*.idr" -not -path './.git/*' 2>/dev/null | wc -l || echo "0" + @echo "Zig FFI files (.zig):" + @find . -name "*.zig" -not -path './.git/*' 2>/dev/null | wc -l || echo "0" + @echo "Deno/JS glue:" + @find bin gui scripts test -name "*.js" 2>/dev/null | wc -l || echo "0" @echo "Documentation files:" - @find docs -name "*.md" -o -name "*.adoc" 2>/dev/null | wc -l || echo "0" + @find docs -name "*.adoc" -o -name "*.md" 2>/dev/null | wc -l || echo "0" # === GIT HOOKS === diff --git a/Mustfile.epx b/Mustfile.epx index 027bfb1..6606b35 100644 --- a/Mustfile.epx +++ b/Mustfile.epx @@ -18,28 +18,26 @@ package_managers: targets: build: consumes: - - src/*.res - - rescript.json - produces: - - src/*.res.js + - cartridges/**/*.cartridge.json + - cartridges/correspondence-cartridge.schema.json + produces: [] command: deno task build clean: consumes: [] produces: [] - command: deno task clean + command: just clean validate: consumes: - - src/*.res - - scripts/*.ts + - cartridges/**/*.cartridge.json produces: [] command: deno task validate test: consumes: - - src/*.res - - src/*.res.js + - cartridges/**/*.cartridge.json + - test/run_all.js produces: [] command: deno task test @@ -53,9 +51,13 @@ invariants: description: "TypeScript files forbidden in src/" check: "! find src -name '*.ts' -o -name '*.tsx' | grep -q ." - rescript_exists: - description: "ReScript sources must exist" - check: "find src -name '*.res' | grep -q ." + cartridges_exist: + description: "Correspondence cartridges must exist" + check: "find cartridges -name '*.cartridge.json' | grep -q ." + + no_rescript_host: + description: "No ReScript host source (host removed; ReScript is a legacy target only)" + check: "! find src -name '*.res' -o -name '*.resi' | grep -q ." deno_config: description: "Deno configuration must exist" diff --git a/README.adoc b/README.adoc index 2fbb9e6..f03b781 100644 --- a/README.adoc +++ b/README.adoc @@ -11,7 +11,7 @@ image:https://img.shields.io/badge/Deno-Runtime-000000.svg[Deno Runtime] > *Learn — to love it!* -- Duolingo / Rosetta Stone for programming languages. A language-comprehension and transfer engine that *classifies* cross-language correspondences so what you learned in one language carries to the next — without shame. -NOTE: The canonical design is link:docs/theory/CORRESPONDENCE-MODEL.adoc[docs/theory/CORRESPONDENCE-MODEL.adoc]. This README mirrors it. The repo began as *ReScript Evangeliser*; ReScript is now a legacy host (being migrated) and a legacy target. See link:ROADMAP.adoc[ROADMAP.adoc] for the re-point. +NOTE: The canonical design is link:docs/theory/CORRESPONDENCE-MODEL.adoc[docs/theory/CORRESPONDENCE-MODEL.adoc]. This README mirrors it. The repo began as *ReScript Evangeliser*; the ReScript host has now been *removed* — ReScript remains only as a legacy _target_ language in the correspondence catalogue. The current runtime surface is Deno + cartridge data; AffineScript is the future host. See link:ROADMAP.adoc[ROADMAP.adoc] for the re-point. == What is This? @@ -109,10 +109,9 @@ The engine *emits*; it does not own the IDE. Analyses become `octads` written to === Prerequisites -* https://deno.land[Deno] (latest stable) -* https://rescript-lang.org[ReScript] 12.2+ (current/legacy host; the host is being ported to AffineScript — see link:ROADMAP.adoc[ROADMAP.adoc]) +* https://deno.land[Deno] (latest stable) — the only runtime dependency -NOTE: AffineScript / Idris2 / Zig may be absent in some environments. Material is authored now and verified in CI — we do not claim local green builds we cannot run. +NOTE: The ReScript host has been removed; the runtime surface is Deno + cartridge data. AffineScript / Idris2 / Zig (future host + ABI/FFI seams) may be absent in some environments — that material is authored now and verified in CI; we do not claim local green builds we cannot run. === Installation @@ -122,28 +121,30 @@ NOTE: AffineScript / Idris2 / Zig may be absent in some environments. Material i git clone https://github.com/hyperpolymath/nextgen-languages-evangeliser.git cd nextgen-languages-evangeliser -# Install dependencies +# Warm the dependency cache just install -# Build host sources +# Validate the cartridge data (no compile step — Deno-only) just build ---- === Usage +The browser workspace (`just gui`) is the primary surface; the CLI is the offline-first fallback. Both read the same validated correspondence cartridges. + [source,bash] ---- -# Scan a source file (offline CLI fallback) -deno run --allow-read bin/evangeliser.js scan +# Launch the multi-pane correspondence workspace (browser) +just gui -# List available correspondences / patterns -deno run --allow-read bin/evangeliser.js patterns +# Offline CLI: list every classified correspondence +deno run --allow-read bin/evangeliser.js -# Show the glyph legend -deno run --allow-read bin/evangeliser.js legend +# Only false friends (the traps) +deno run --allow-read bin/evangeliser.js --kind false-friend -# Display statistics -deno run --allow-read bin/evangeliser.js stats +# Filter by concept or language; machine-readable output +deno run --allow-read bin/evangeliser.js --find erlang --json ---- === Development @@ -165,16 +166,16 @@ The engine is language-agnostic; targets are the languages a learner moves *to*. |*Target* |*Status* |*Why interesting for transfer* |*AffineScript* |First-class; future host |Affine/linear use-once, borrow checking, QTT — dense with _novel/no-anchor_ correspondences; emits typed-wasm → WebAssembly -|*ReScript* |Legacy target + legacy host |Sound inference, Option/Result, pattern matching; source of the _vanished_ `return` example +|*ReScript* |Legacy target (host removed) |Sound inference, Option/Result, pattern matching; source of the _vanished_ `return` example |*TypeScript, Ruby, Prolog, C, JTV, …* |Reference / planned |Cartridge-authored; JTV is the canonical _alien-realization_ + _novel_ worked example |=== == Technology Stack -* *ReScript 12.2* — current/legacy host (being migrated; banned in _new_ code per estate policy: `.res` → `.affine`) +* *Deno* — runtime, tooling, and the current host surface (workspace server + CLI + cartridge data) * *AffineScript* — future host (Zig FFI + Idris2 ABI seams); OCaml 5.1+ toolchain; emits typed-wasm IR → WebAssembly -* *Deno* — runtime and package management * *Zig* — FFI layer (canonical per manifest); *Idris2* — ABI / proofs +* *ReScript* — _removed_ as host; retained only as a legacy _target_ language in the catalogue === Language Policy @@ -184,37 +185,32 @@ Per Hyperpolymath Standard: |=== |*Allowed* |*Banned* -|AffineScript (future host), ReScript (legacy) |TypeScript +|AffineScript (future host), Deno (current host surface) |TypeScript |Deno |Node.js/npm/bun |justfile |Makefile |Zig (FFI), Idris2 (ABI/proofs) |V (outside V ecosystem) |=== -ReScript is banned in _new_ code; existing `.res` migrates to `.affine`. See link:CLAUDE.md[CLAUDE.md] and link:.claude/CLAUDE.md[.claude/CLAUDE.md] for the full policy. +The ReScript host has been removed (no `.res` host code remains); ReScript persists only as a legacy _target_ language in the correspondence catalogue. AffineScript is the future host. See link:CLAUDE.md[CLAUDE.md] and link:.claude/CLAUDE.md[.claude/CLAUDE.md] for the full policy. == Project Structure [source] ---- nextgen-languages-evangeliser/ -+-- src/ # Host source (ReScript today; AffineScript target) -| +-- Types.res # Core type model -| +-- Glyphs.res # Makaton-inspired glyph view-layer -| +-- Narrative.res # Shame-free narrative (per CorrespondenceKind) -| +-- Patterns.res # Correspondence / pattern catalogue -| +-- Scanner.res(i) # Detection engine + interface -| +-- Analyser.res(i) # Classification + aggregation + interface -| +-- Output.res # focus/glyph/blockly/raw/side-by-side view-layers -| +-- Cli.res # CLI entry point (offline fallback) -+-- gui/ # Browser multi-pane workspace (primary surface) -+-- test/ # Test suites -+-- bin/evangeliser.js # Deno CLI shim -+-- docs/ # Documentation -| +-- theory/CORRESPONDENCE-MODEL.adoc # Canonical design spec -+-- rescript.json # Host (ReScript) configuration ++-- src/interface/ # Engine spine (no host application code yet) +| +-- Abi/ # Idris2 ABI: Carrier.idr, Correspondence.idr (typechecked) +| +-- ffi/ # Zig C-ABI mirror (build.zig + src/ + test/) +| +-- host/ # AffineScript host binding (Correspondence.affine; compiler pending) ++-- cartridges/ # Correspondence facts (the "lessons"): schema + reference pack ++-- gui/ # Browser multi-pane workspace (primary surface; Deno + static) ++-- bin/evangeliser.js # Offline CLI (Deno; reads cartridges) ++-- test/run_all.js # Cartridge invariant tests (Deno) ++-- scripts/ # Deno utilities (cartridge validator, GUI launcher) ++-- docs/theory/CORRESPONDENCE-MODEL.adoc # Canonical design spec ++-- abi.ipkg # Idris2 package (typecheck the ABI) +-- deno.json # Deno configuration +-- Justfile # Task orchestration -+-- Mustfile.epx # Deployment contract +-- config.ncl # Nickel configuration +-- CLAUDE.md # AI context ---- @@ -237,7 +233,7 @@ ____ This project follows the *Rhodium Standard Repository (RSR)* framework: -* *Type Safety*: ReScript 12.2 host today; AffineScript (affine/linear types, borrow checking) as the host target. Correspondence carrier is Idris2/Agda-grounded (Dyadic + Echo). +* *Type Safety*: AffineScript (affine/linear types, borrow checking) is the future host; the correspondence carrier is Idris2/Agda-grounded (Dyadic + Echo), with the ABI typechecked under Idris2. * *Offline First*: CLI/TUI fallback; zero network dependency to scan * *Complete Documentation*: see link:docs/[docs/] and link:docs/theory/CORRESPONDENCE-MODEL.adoc[the design spec] * *Security First*: see link:SECURITY.md[SECURITY.md] @@ -283,7 +279,8 @@ See link:LICENSE[LICENSE] for the full license text. * [x] Correspondence-model spec merged (Concept/Form/Transition + six CorrespondenceKinds) * [x] Standards / repo hygiene merged * [-] Identity re-point + abstraction-model pivot (classify, not translate) -* [ ] Browser multi-pane GUI (overlay view-layers) +* [x] Browser multi-pane GUI (overlay view-layers) +* [x] ReScript host removed (Deno + cartridges; AffineScript host pending) * [ ] AffineScript host port (Zig FFI + Idris2 ABI) * [ ] Cartridge contract + 2nd language pack * [ ] Proofs/benches + PanLL octad emission diff --git a/ROADMAP.adoc b/ROADMAP.adoc index d408085..4979f6b 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -25,29 +25,29 @@ design), link:CLAUDE.md[CLAUDE.md], and link:README.adoc[README.adoc] for overvi === M1 — Standards / repo hygiene ✅ * [x] Repo renamed `rescript-evangeliser` → `nextgen-languages-evangeliser` * [x] Estate machine-readable artefacts + SPDX present -* [x] Language-policy perimeter aligned (Deno, Justfile, Zig FFI; TS/Python/Go/V banned; ReScript legacy) +* [x] Language-policy perimeter aligned (Deno, Justfile, Zig FFI; TS/Python/Go/V banned; ReScript host removed — legacy target only) === M2 — Identity re-point + abstraction-model pivot → * [-] Re-point identity documentation to the corrected purpose (this pass) -* [ ] Implement Concept / Form / Transition in `src/Types.res` (engine model, not pattern-list) -* [ ] Implement `CorrespondenceKind` + per-stratum classification in Analyser -* [ ] Thread kind-aware, shame-free narratives through `src/Narrative.res` -* [ ] Reframe `src/Patterns.res` as a *reference language pack* (Forms + Transitions, not "JS→AffineScript patterns") -* [ ] Update tests for the classify-not-translate model - -=== M3 — Browser multi-pane GUI → -* [ ] Browser multi-pane workspace as the primary surface -* [ ] Overlay view-layers: focus / glyph / blockly-flowchart / raw-code / side-by-side -* [ ] View-layers conform to the estate overlay-protocol (additive, idempotent) -* [ ] Accessibility: Hyperpolymath Accessibility Standard Level A → AA (keyboard-only, ≥4.5:1 contrast, ARIA, reduced-motion, plain-language mode) -* [ ] CLI/TUI retained as the offline fallback - -=== M4 — AffineScript host port (Zig FFI + Idris2 ABI) → -* [ ] End-to-end "hello world → typed-wasm → WASM" proof-of-life -* [ ] Port `src/` ReScript → AffineScript (`.res` → `.affine`), module-by-module -* [ ] Zig FFI seam + Idris2 ABI seam for the certifiable carrier -* [ ] Port `test/` in lockstep -* [ ] Note: ReScript banned in _new_ code; AffineScript remains a first-class teaching target and the future host +* [x] Concept / Form / Transition + `CorrespondenceKind` + per-stratum classification realised in the Idris2 ABI (`src/interface/Abi/`) and the cartridge schema +* [x] Kind-aware, shame-free narratives carried as cartridge fields (celebrate / minimise / better / safety / example) +* [ ] Port the legacy pattern catalogue into cartridge facts (Forms + Transitions, not "JS→AffineScript patterns") +* [x] Cartridge invariant tests for the classify-not-translate model (`test/run_all.js`) + +=== M3 — Browser multi-pane GUI ✅ +* [x] Browser multi-pane workspace as the primary surface +* [x] Overlay view-layers: focus / glyph / blockly-flowchart / raw-code / side-by-side +* [x] View-layers conform to the estate overlay-protocol (additive, idempotent) +* [x] Accessibility: Hyperpolymath Accessibility Standard Level A → AA (keyboard-only, ≥4.5:1 contrast, ARIA, reduced-motion, plain-language mode) +* [x] CLI retained as the offline fallback (`bin/evangeliser.js`, reads cartridges) + +=== M4 — AffineScript host application (Zig FFI + Idris2 ABI) → +* [x] Idris2 ABI seam for the certifiable carrier (`src/interface/Abi/` — typechecks) +* [x] Zig FFI seam mirroring the ABI (`src/interface/ffi/`) +* [x] AffineScript host binding authored (`src/interface/host/Correspondence.affine`) +* [x] Legacy ReScript host removed (no `.res` to port — the host is built fresh on the seams) +* [ ] End-to-end "hello world → typed-wasm → WASM" proof-of-life (gated on the AffineScript compiler) +* [ ] Note: ReScript is a legacy _target_ only; AffineScript remains a first-class teaching target and the future host === M5 — Cartridge contract + 2nd language pack → * [ ] Specify the cartridge fact format (concept / from / to / kind / strata / residue / witness) diff --git a/TEST-NEEDS.md b/TEST-NEEDS.md index c803fe0..ea9ed7b 100644 --- a/TEST-NEEDS.md +++ b/TEST-NEEDS.md @@ -6,17 +6,20 @@ | Category | Count | Notes | |----------|-------|-------| -| ReScript unit tests | 6 | `test/{Analyser,Glyphs,Narrative,Patterns,Scanner,Types}_test.res` | -| Test framework | ReScript built-in | Jest-compatible | +| Cartridge invariant tests | 1 suite | `test/run_all.js` (Deno) — 59 assertions over the correspondence facts | +| Cartridge schema validation | 1 | `scripts/validate-cartridges.js` (Deno + ajv, JSON-Schema 2020-12) | +| Idris2 ABI typecheck | 1 | `abi.ipkg` / `src/interface/Abi/*.idr` (CI: idris2-abi.yml) | +| Test framework | Deno + ajv + Idris2 | host-language unit tests removed with the ReScript host | ## What's Covered -- [x] Analyser pattern testing -- [x] Glyph representation tests -- [x] Narrative generator tests -- [x] Pattern matching tests -- [x] Scanner tokenization tests -- [x] Type system tests +- [x] Every correspondence is classified by one of the six CorrespondenceKinds +- [x] Well-formedness: Forms (language + surface), residue shapes, strata names/booleans +- [x] False-friend signature (surface holds ∧ intention diverges) — mirrors `Abi.Correspondence.isFalseFriendShape` +- [x] Residue direction for novel (absent-source) / vanished (absent-target) +- [x] Reference cartridge demonstrates all six kinds +- [x] Cartridge JSON-Schema conformance (ajv) +- [x] Idris2 ABI typechecks (no `believe_me` / `assert_total`) ## Still Missing (for CRG B+) @@ -28,5 +31,7 @@ ## Run Tests ```bash -cd /var/mnt/eclipse/repos/nextgen-languages-evangeliser && rescript build && npm test +deno task test # cartridge invariant tests +deno task validate # cartridge schema validation +just test # same, via the Justfile ``` diff --git a/TOPOLOGY.md b/TOPOLOGY.md index 6f05d9f..2218758 100644 --- a/TOPOLOGY.md +++ b/TOPOLOGY.md @@ -4,18 +4,20 @@ ## Purpose -Multi-target advocacy tool that helps teams migrate from JavaScript/TypeScript to next-generation type-safe languages. Flagship target: AffineScript. Legacy target: ReScript. Analyses codebases, generates migration reports, produces evangelism content, and tracks conversion progress. Runs on Deno. +Cross-language *comprehension and transfer* engine — Duolingo / Rosetta Stone for programming languages. It *classifies* correspondences between a known language and a target so learning transfers (it does not translate, lint, or build IDEs). Flagship / future host: AffineScript. Legacy target: ReScript (host removed). Runs on Deno; the browser multi-pane workspace is the primary surface. ## Module Map ``` nextgen-languages-evangeliser/ ├── bin/ -│ └── evangeliser.js # Main entry point (Deno CLI) -├── src/ # Host source (ReScript today; AffineScript post-Phase 3) +│ └── evangeliser.js # Offline CLI (Deno; reads cartridges) +├── src/interface/ # Engine spine: Idris2 ABI + Zig FFI + AffineScript host binding +├── cartridges/ # Correspondence facts (schema + reference pack) +├── gui/ # Browser multi-pane workspace (primary surface) ├── config.ncl # Nickel configuration ├── docs/ # Usage and output documentation -├── Justfile / Justfile # Task runner recipes +├── Justfile # Task runner recipes └── deno.json # Deno module config ``` diff --git a/bin/evangeliser.js b/bin/evangeliser.js index 14f7bd3..8674964 100644 --- a/bin/evangeliser.js +++ b/bin/evangeliser.js @@ -1,6 +1,149 @@ -#!/usr/bin/env -S deno run --allow-read --allow-env +#!/usr/bin/env -S deno run --allow-read // SPDX-License-Identifier: MPL-2.0 -// Entry point for the nextgen-languages-evangeliser CLI -// Imports and runs the compiled ReScript CLI module (host language; AffineScript planned post-Phase 3) +// Offline CLI for the Nextgen Languages Evangeliser. +// +// Reads the validated correspondence cartridges (cartridges/**/*.cartridge.json) +// and prints the *classified* correspondences — concept · from→to · kind · +// residue. Classify, don't translate: this mirrors the browser workspace's data +// model with zero runtime dependencies (Deno-only — no ReScript). +// +// deno run --allow-read bin/evangeliser.js [--kind K] [--find TEXT] +// [--cartridges DIR] [--json] [--help] +// +// The browser workspace ('just gui') is the primary surface; this CLI is the +// offline-first fallback. -import "../src/Cli.res.js" +const KINDS = { + "cognate": { glyph: "🤝", pedagogy: "transfer directly" }, + "false-friend": { glyph: "🎭", pedagogy: "flag the trap" }, + "antonym": { glyph: "🔄", pedagogy: "remap the intuition" }, + "alien-realization": { glyph: "🛸", pedagogy: "bridge with effort" }, + "novel": { glyph: "✨", pedagogy: "teach de novo" }, + "vanished": { glyph: "👻", pedagogy: "un-learn / re-route" }, +} + +const HELP = `Nextgen Languages Evangeliser — offline correspondence CLI + +Reads the correspondence cartridges and prints classified correspondences. +Classify, don't translate — each correspondence carries its CorrespondenceKind +and residue (what is lost, added, or inverted on the crossing). + +USAGE: + deno run --allow-read bin/evangeliser.js [options] + +OPTIONS: + --kind K only the given kind (${Object.keys(KINDS).join(", ")}) + --find TEXT filter by concept or language (case-insensitive) + --cartridges DIR cartridge root (default: ./cartridges) + --json machine-readable output + -h, --help this help + +The browser workspace ('just gui') is the primary surface; this CLI is the +offline-first fallback.` + +function parseArgs(argv) { + const opts = { kind: null, find: null, json: false, dir: null, help: false } + for (let i = 0; i < argv.length; i++) { + const a = argv[i] + if (a === "--help" || a === "-h") opts.help = true + else if (a === "--json") opts.json = true + else if (a === "--kind") opts.kind = argv[++i] ?? null + else if (a === "--find") opts.find = (argv[++i] ?? "").toLowerCase() + else if (a === "--cartridges") opts.dir = argv[++i] ?? null + } + return opts +} + +async function loadCartridges(dirUrl) { + const out = [] + async function walk(d) { + let entries + try { + entries = Deno.readDir(d) + } catch { + return + } + for await (const e of entries) { + const child = new URL(e.name + (e.isDirectory ? "/" : ""), d) + if (e.isDirectory) { + await walk(child) + continue + } + if (!e.name.endsWith(".cartridge.json")) continue + try { + const c = JSON.parse(await Deno.readTextFile(child)) + out.push({ + cartridge: c.name ?? e.name, + transitions: Array.isArray(c.transitions) ? c.transitions : [], + }) + } catch (err) { + console.error(`skip ${e.name}: ${err.message}`) + } + } + } + await walk(dirUrl) + return out +} + +function verdict(strata, name) { + const v = (strata ?? []).find((s) => s.stratum === name) + return v ? v.holds : null +} + +function render(t) { + const k = KINDS[t.kind] ?? { glyph: "?", pedagogy: "" } + const residue = t.residue?.shape ? ` · residue: ${t.residue.shape}` : "" + const falseFriend = verdict(t.strata, "surface") === true && + verdict(t.strata, "intention") === false + ? " ⚠ false-friend signature (surface holds, intention diverges)" + : null + return [ + `${k.glyph} ${t.concept} [${t.kind} — ${k.pedagogy}]${residue}`, + ` ${t.from?.language ?? "?"}: ${t.from?.surface ?? ""}`, + ` → ${t.to?.language ?? "?"}: ${t.to?.surface ?? ""}`, + t.residue?.note ? ` ↳ ${t.residue.note}` : null, + falseFriend, + ].filter((x) => x != null).join("\n") +} + +async function main() { + const opts = parseArgs(Deno.args) + if (opts.help) { + console.log(HELP) + return + } + + const root = opts.dir + ? new URL(opts.dir.replace(/\/?$/, "/"), `file://${Deno.cwd()}/`) + : new URL("../cartridges/", import.meta.url) + + const cartridges = await loadCartridges(root) + let items = cartridges.flatMap((c) => + c.transitions.map((t) => ({ ...t, cartridge: c.cartridge })) + ) + + if (opts.kind) items = items.filter((t) => t.kind === opts.kind) + if (opts.find) { + items = items.filter((t) => + [t.concept, t.from?.language, t.to?.language, t.from?.surface, t.to?.surface] + .filter(Boolean).some((s) => String(s).toLowerCase().includes(opts.find)) + ) + } + + if (opts.json) { + console.log(JSON.stringify({ count: items.length, correspondences: items }, null, 2)) + return + } + + if (!items.length) { + console.log("No correspondences match.") + return + } + console.log(`Nextgen Languages Evangeliser — ${items.length} correspondence(s)\n`) + console.log(items.map(render).join("\n\n")) + console.log( + "\n(Classify, don't translate · the six CorrespondenceKinds · 'just gui' for the workspace)", + ) +} + +await main() diff --git a/config.ncl b/config.ncl index e05acc4..6ae37b6 100644 --- a/config.ncl +++ b/config.ncl @@ -23,8 +23,8 @@ # Language policy (what we write the toolkit in) languages = { allowed = [ - "AffineScript", # planned host (Phase 3+) - "ReScript", # current host + "AffineScript", # future host (Phase 3+) + "ReScript", # legacy target language only (host removed) "Deno", "Rust", "Zig", # canonical FFI @@ -43,7 +43,7 @@ "V", # use Zig, except inside V-ecosystem projects ], - primary = "ReScript", # current host language + primary = "Deno", # interim host surface (ReScript host removed) primary_planned = "AffineScript", # post-Phase 3 runtime = "Deno", @@ -63,7 +63,7 @@ tasks = { build = "deno task build", - clean = "deno task clean", + clean = "just clean", validate = "deno task validate", test = "deno task test", fmt = "deno task fmt", @@ -91,12 +91,4 @@ "test", ], }, - - # ReScript configuration (current host) - rescript = { - version = "12", - module_format = "es6", - suffix = ".res.js", - uncurried = true, - }, } diff --git a/deno.json b/deno.json index 3b88742..01adcda 100644 --- a/deno.json +++ b/deno.json @@ -5,19 +5,14 @@ "license": "MPL-2.0", "exports": "./bin/evangeliser.js", "tasks": { - "build": "./scripts/rescript-build.sh", - "build:watch": "deno run -A npm:rescript build -w", - "clean": "deno run -A npm:rescript clean", + "build": "deno run -A --no-lock scripts/validate-cartridges.js", + "validate": "deno run -A --no-lock scripts/validate-cartridges.js", + "gui": "deno run --allow-read --allow-net gui/server.js", "lint": "deno lint", "fmt": "deno fmt", - "test": "deno run --allow-read --allow-env test/run_all.js", + "test": "deno run --allow-read test/run_all.js", "pre-commit": "deno task lint && deno task fmt --check" }, - "imports": { - "rescript": "npm:rescript@^12.0.0", - "@rescript/core": "npm:@rescript/core@^1.6.1", - "@rescript/runtime/": "npm:/@rescript/runtime@12.2.0/" - }, "fmt": { "useTabs": false, "lineWidth": 100, @@ -25,12 +20,11 @@ "semiColons": false, "singleQuote": false, "proseWrap": "preserve", - "include": ["bin/", "gui/", "test/run_all.js", "deno.json"] + "include": ["bin/", "gui/", "scripts/", "test/run_all.js", "deno.json"] }, "lint": { "rules": { "tags": ["recommended"] } - }, - "nodeModulesDir": "auto" + } } diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 8c7ed4c..0000000 --- a/deno.lock +++ /dev/null @@ -1,66 +0,0 @@ -{ - "version": "5", - "specifiers": { - "npm:@rescript/core@^1.6.1": "1.6.1_rescript@12.2.0", - "npm:@rescript/runtime@12.2.0": "12.2.0", - "npm:rescript@*": "12.2.0", - "npm:rescript@12": "12.2.0" - }, - "npm": { - "@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"] - }, - "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 - } - }, - "workspace": { - "dependencies": [ - "npm:@rescript/core@^1.6.1", - "npm:@rescript/runtime@12.2.0", - "npm:rescript@12" - ] - } -} diff --git a/gui/EvangeliserGui.affine b/gui/EvangeliserGui.affine index a216712..07d4c73 100644 --- a/gui/EvangeliserGui.affine +++ b/gui/EvangeliserGui.affine @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 // AffineScript TEA GUI model for Nextgen Languages Evangeliser. // -// The browser shell in this directory drives the current Deno/ReScript -// analyser. This file is the AffineScript interface contract for the GUI: -// model, messages, update, view, and subscriptions. +// The browser shell in this directory is a Deno + cartridge workspace (the +// ReScript host has been removed). This file is the AffineScript interface +// contract for the GUI: model, messages, update, view, and subscriptions. enum TargetLang { TargetAffineScript, diff --git a/gui/app.html b/gui/app.html index a335022..8973482 100644 --- a/gui/app.html +++ b/gui/app.html @@ -28,154 +28,450 @@ } @media (prefers-color-scheme: dark) { :root { - --bg: #0e1418; --panel: #161e25; --ink: #e7eef5; --muted: #9fb0bd; - --line: #2a353e; --accent: #36b3ad; --accent-ink: #06211f; - --code: #0a1016; --code-ink: #dce6ef; --focus: #4fd0c9; - --k-cognate: #58c98a; --k-false: #ef8a7e; --k-antonym: #74b6e6; - --k-alien: #b79ce6; --k-novel: #e0b85a; --k-vanished: #aab6c2; + --bg: #0e1418; + --panel: #161e25; + --ink: #e7eef5; + --muted: #9fb0bd; + --line: #2a353e; + --accent: #36b3ad; + --accent-ink: #06211f; + --code: #0a1016; + --code-ink: #dce6ef; + --focus: #4fd0c9; + --k-cognate: #58c98a; + --k-false: #ef8a7e; + --k-antonym: #74b6e6; + --k-alien: #b79ce6; + --k-novel: #e0b85a; + --k-vanished: #aab6c2; } } - * { box-sizing: border-box; } + * { + box-sizing: border-box; + } body { - margin: 0; min-height: 100vh; background: var(--bg); color: var(--ink); + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } - button, select, input { font: inherit; color: var(--ink); } - :focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } - .app { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } + button, select, input { + font: inherit; + color: var(--ink); + } + :focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; + } + .app { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; + } header.topbar { - display: flex; flex-wrap: wrap; gap: 14px 18px; align-items: center; - justify-content: space-between; padding: 12px 18px; - border-bottom: 1px solid var(--line); background: var(--panel); + display: flex; + flex-wrap: wrap; + gap: 14px 18px; + align-items: center; + justify-content: space-between; + padding: 12px 18px; + border-bottom: 1px solid var(--line); + background: var(--panel); + } + .brand { + display: flex; + gap: 12px; + align-items: center; + min-width: 0; } - .brand { display: flex; gap: 12px; align-items: center; min-width: 0; } .mark { - width: 40px; height: 40px; border-radius: 9px; display: grid; place-items: center; + width: 40px; + height: 40px; + border-radius: 9px; + display: grid; + place-items: center; background: linear-gradient(135deg, var(--accent), var(--k-alien)); - color: #fff; font-weight: 800; font-size: 17px; + color: #fff; + font-weight: 800; + font-size: 17px; + } + h1 { + margin: 0; + font-size: 17px; + line-height: 1.15; + } + .motto { + margin-top: 2px; + color: var(--muted); + font-style: italic; + } + .controls { + display: flex; + flex-wrap: wrap; + gap: 10px 14px; + align-items: center; + } + .field { + display: flex; + gap: 6px; + align-items: center; + } + .field > label { + color: var(--muted); + font-size: 12px; } - h1 { margin: 0; font-size: 17px; line-height: 1.15; } - .motto { margin-top: 2px; color: var(--muted); font-style: italic; } - .controls { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; } - .field { display: flex; gap: 6px; align-items: center; } - .field > label { color: var(--muted); font-size: 12px; } select, input[type="search"] { - min-height: 34px; padding: 0 10px; border: 1px solid var(--line); - border-radius: 7px; background: var(--panel); + min-height: 34px; + padding: 0 10px; + border: 1px solid var(--line); + border-radius: 7px; + background: var(--panel); } fieldset.views { - display: flex; gap: 2px; margin: 0; padding: 3px; border: 1px solid var(--line); - border-radius: 9px; background: var(--panel); + display: flex; + gap: 2px; + margin: 0; + padding: 3px; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--panel); + } + fieldset.views legend { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); } - fieldset.views legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } .views label { - padding: 6px 10px; border-radius: 7px; cursor: pointer; color: var(--muted); - font-size: 13px; white-space: nowrap; + padding: 6px 10px; + border-radius: 7px; + cursor: pointer; + color: var(--muted); + font-size: 13px; + white-space: nowrap; + } + .views input { + position: absolute; + opacity: 0; + pointer-events: none; + } + .views input:checked + span { + color: var(--accent-ink); + } + .views label:has(input:checked) { + background: var(--accent); + color: var(--accent-ink); + } + .views label:has(input:focus-visible) { + outline: 3px solid var(--focus); + outline-offset: 1px; } - .views input { position: absolute; opacity: 0; pointer-events: none; } - .views input:checked + span { color: var(--accent-ink); } - .views label:has(input:checked) { background: var(--accent); color: var(--accent-ink); } - .views label:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 1px; } main.workspace { - min-height: 0; display: grid; gap: 14px; padding: 14px; + min-height: 0; + display: grid; + gap: 14px; + padding: 14px; grid-template-columns: minmax(240px, 0.85fr) minmax(360px, 1.4fr) minmax(280px, 1fr); } .pane { - min-width: 0; min-height: 0; border: 1px solid var(--line); border-radius: 10px; - background: var(--panel); display: grid; grid-template-rows: auto 1fr; overflow: hidden; + min-width: 0; + min-height: 0; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--panel); + display: grid; + grid-template-rows: auto 1fr; + overflow: hidden; } .pane-header { - display: flex; gap: 10px; align-items: baseline; justify-content: space-between; - padding: 11px 13px; border-bottom: 1px solid var(--line); + display: flex; + gap: 10px; + align-items: baseline; + justify-content: space-between; + padding: 11px 13px; + border-bottom: 1px solid var(--line); + } + .pane-title { + font-weight: 700; + } + .pane-hint { + color: var(--muted); + font-size: 12px; + } + .pane-body { + min-height: 0; + overflow: auto; + padding: 12px; } - .pane-title { font-weight: 700; } - .pane-hint { color: var(--muted); font-size: 12px; } - .pane-body { min-height: 0; overflow: auto; padding: 12px; } /* index pane */ - .index { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } + .index { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 6px; + } .index li { - border: 1px solid var(--line); border-radius: 8px; padding: 9px 11px; cursor: pointer; - display: grid; gap: 4px; background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + padding: 9px 11px; + cursor: pointer; + display: grid; + gap: 4px; + background: var(--panel); + } + .index li[aria-selected="true"] { + border-color: var(--accent); + box-shadow: inset 0 0 0 1px var(--accent); + } + .index .concept { + font-weight: 650; + } + .index .pair { + color: var(--muted); + font-size: 12px; } - .index li[aria-selected="true"] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); } - .index .concept { font-weight: 650; } - .index .pair { color: var(--muted); font-size: 12px; } .badge { - display: inline-flex; align-items: center; gap: 5px; border-radius: 999px; - padding: 2px 9px; font-size: 12px; font-weight: 650; border: 1px solid currentColor; + display: inline-flex; + align-items: center; + gap: 5px; + border-radius: 999px; + padding: 2px 9px; + font-size: 12px; + font-weight: 650; + border: 1px solid currentColor; + } + .k-cognate { + color: var(--k-cognate); + } + .k-false { + color: var(--k-false); + } + .k-antonym { + color: var(--k-antonym); + } + .k-alien { + color: var(--k-alien); + } + .k-novel { + color: var(--k-novel); + } + .k-vanished { + color: var(--k-vanished); } - .k-cognate { color: var(--k-cognate); } .k-false { color: var(--k-false); } - .k-antonym { color: var(--k-antonym); } .k-alien { color: var(--k-alien); } - .k-novel { color: var(--k-novel); } .k-vanished { color: var(--k-vanished); } /* forms pane / view-layers */ - .concept-head { margin: 0 0 12px; font-size: 16px; } - .sxs { display: grid; grid-template-columns: 1fr auto 1fr; gap: 12px; align-items: center; } - .form-card { min-width: 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; } + .concept-head { + margin: 0 0 12px; + font-size: 16px; + } + .sxs { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 12px; + align-items: center; + } + .form-card { + min-width: 0; + border: 1px solid var(--line); + border-radius: 9px; + overflow: hidden; + } .form-lang { - padding: 6px 10px; font-size: 12px; font-weight: 650; color: var(--muted); - background: color-mix(in srgb, var(--accent) 8%, transparent); border-bottom: 1px solid var(--line); + padding: 6px 10px; + font-size: 12px; + font-weight: 650; + color: var(--muted); + background: color-mix(in srgb, var(--accent) 8%, transparent); + border-bottom: 1px solid var(--line); } pre.surface { - margin: 0; padding: 12px; overflow: auto; background: var(--code); color: var(--code-ink); + margin: 0; + padding: 12px; + overflow: auto; + background: var(--code); + color: var(--code-ink); font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } - .cross { font-size: 22px; text-align: center; } - .cross small { display: block; color: var(--muted); font-size: 11px; } - .focus-form { font-size: 15px; } - .glyph-view { text-align: center; padding: 18px 8px; } - .glyph-view .big { font-size: 56px; line-height: 1; } - .glyph-view .row { display: flex; justify-content: center; gap: 18px; margin-top: 14px; flex-wrap: wrap; } - .blockly { display: grid; gap: 10px; justify-items: center; } + .cross { + font-size: 22px; + text-align: center; + } + .cross small { + display: block; + color: var(--muted); + font-size: 11px; + } + .focus-form { + font-size: 15px; + } + .glyph-view { + text-align: center; + padding: 18px 8px; + } + .glyph-view .big { + font-size: 56px; + line-height: 1; + } + .glyph-view .row { + display: flex; + justify-content: center; + gap: 18px; + margin-top: 14px; + flex-wrap: wrap; + } + .blockly { + display: grid; + gap: 10px; + justify-items: center; + } .block { - width: 100%; max-width: 460px; border: 2px solid var(--accent); border-radius: 12px; - padding: 10px 14px; background: color-mix(in srgb, var(--accent) 7%, var(--panel)); + width: 100%; + max-width: 460px; + border: 2px solid var(--accent); + border-radius: 12px; + padding: 10px 14px; + background: color-mix(in srgb, var(--accent) 7%, var(--panel)); + } + .block .blang { + font-size: 12px; + color: var(--muted); + font-weight: 650; + } + .block code { + font: 13px/1.4 ui-monospace, Menlo, Consolas, monospace; + } + .connector { + font-size: 20px; + color: var(--muted); } - .block .blang { font-size: 12px; color: var(--muted); font-weight: 650; } - .block code { font: 13px/1.4 ui-monospace, Menlo, Consolas, monospace; } - .connector { font-size: 20px; color: var(--muted); } /* classification pane */ - .kind-hero { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; } - .kind-hero .glyph { font-size: 34px; } - .kind-hero .pedagogy { font-weight: 700; } - .kind-hero .blurb { color: var(--muted); font-size: 13px; } - .section { margin: 14px 0; } - .section h3 { margin: 0 0 7px; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); } - .residue { display: flex; gap: 8px; align-items: baseline; } - .residue .shape { font-weight: 650; } - .strata { display: flex; flex-wrap: wrap; gap: 6px; } + .kind-hero { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 12px; + } + .kind-hero .glyph { + font-size: 34px; + } + .kind-hero .pedagogy { + font-weight: 700; + } + .kind-hero .blurb { + color: var(--muted); + font-size: 13px; + } + .section { + margin: 14px 0; + } + .section h3 { + margin: 0 0 7px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--muted); + } + .residue { + display: flex; + gap: 8px; + align-items: baseline; + } + .residue .shape { + font-weight: 650; + } + .strata { + display: flex; + flex-wrap: wrap; + gap: 6px; + } .stratum { - display: inline-flex; gap: 5px; align-items: center; border: 1px solid var(--line); - border-radius: 999px; padding: 2px 9px; font-size: 12px; + display: inline-flex; + gap: 5px; + align-items: center; + border: 1px solid var(--line); + border-radius: 999px; + padding: 2px 9px; + font-size: 12px; + } + .stratum.holds { + color: var(--k-cognate); + border-color: currentColor; + } + .stratum.diverges { + color: var(--k-false); + border-color: currentColor; } - .stratum.holds { color: var(--k-cognate); border-color: currentColor; } - .stratum.diverges { color: var(--k-false); border-color: currentColor; } .ff-flag { - margin-top: 8px; padding: 8px 10px; border-radius: 8px; font-size: 13px; - color: var(--k-false); border: 1px solid currentColor; + margin-top: 8px; + padding: 8px 10px; + border-radius: 8px; + font-size: 13px; + color: var(--k-false); + border: 1px solid currentColor; background: color-mix(in srgb, var(--k-false) 8%, transparent); } - .narrative { display: grid; gap: 8px; } - .narrative .n { border-left: 3px solid var(--accent); padding: 6px 10px; border-radius: 0 8px 8px 0; - background: color-mix(in srgb, var(--accent) 6%, transparent); } - .narrative .n b { display: block; font-size: 11px; text-transform: uppercase; color: var(--muted); } - .residue-note { color: var(--muted); } + .narrative { + display: grid; + gap: 8px; + } + .narrative .n { + border-left: 3px solid var(--accent); + padding: 6px 10px; + border-radius: 0 8px 8px 0; + background: color-mix(in srgb, var(--accent) 6%, transparent); + } + .narrative .n b { + display: block; + font-size: 11px; + text-transform: uppercase; + color: var(--muted); + } + .residue-note { + color: var(--muted); + } - .empty { padding: 24px; text-align: center; color: var(--muted); border: 1px dashed var(--line); border-radius: 8px; } + .empty { + padding: 24px; + text-align: center; + color: var(--muted); + border: 1px dashed var(--line); + border-radius: 8px; + } footer.statusbar { - padding: 8px 18px; border-top: 1px solid var(--line); background: var(--panel); - color: var(--muted); font-size: 13px; display: flex; justify-content: space-between; gap: 10px; + padding: 8px 18px; + border-top: 1px solid var(--line); + background: var(--panel); + color: var(--muted); + font-size: 13px; + display: flex; + justify-content: space-between; + gap: 10px; } - @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } } + @media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + animation: none !important; + } + } @media (max-width: 1000px) { - main.workspace { grid-template-columns: 1fr; } - .sxs { grid-template-columns: 1fr; } .cross { transform: rotate(90deg); } + main.workspace { + grid-template-columns: 1fr; + } + .sxs { + grid-template-columns: 1fr; + } + .cross { + transform: rotate(90deg); + } } @@ -204,11 +500,17 @@

    Nextgen Languages Evangeliser

    - +
    View layer - + @@ -224,7 +526,8 @@

    Nextgen Languages Evangeliser

    -
      +
        +
      diff --git a/gui/app.js b/gui/app.js index 0ddd9cb..bc551f8 100644 --- a/gui/app.js +++ b/gui/app.js @@ -8,103 +8,152 @@ // per-stratum verdicts, and a no-shame narrative. const KIND = { - "cognate": { glyph: "🤝", label: "Cognate", pedagogy: "Transfer directly", cls: "k-cognate", blurb: "Same idea — your intuition carries over." }, - "false-friend": { glyph: "🎭", label: "False friend", pedagogy: "Flag the trap", cls: "k-false", blurb: "Looks familiar; behaves differently." }, - "antonym": { glyph: "🔄", label: "Antonym", pedagogy: "Remap the intuition", cls: "k-antonym", blurb: "Related but inverted — flip your expectation." }, - "alien-realization": { glyph: "🛸", label: "Alien realization", pedagogy: "Bridge with effort", cls: "k-alien", blurb: "Same goal, unfamiliar machinery." }, - "novel": { glyph: "✨", label: "Novel", pedagogy: "Teach de novo", cls: "k-novel", blurb: "No prior anchor — learn it fresh." }, - "vanished": { glyph: "👻", label: "Vanished", pedagogy: "Un-learn / re-route", cls: "k-vanished", blurb: "A habit to drop — it's gone here." }, -}; + "cognate": { + glyph: "🤝", + label: "Cognate", + pedagogy: "Transfer directly", + cls: "k-cognate", + blurb: "Same idea — your intuition carries over.", + }, + "false-friend": { + glyph: "🎭", + label: "False friend", + pedagogy: "Flag the trap", + cls: "k-false", + blurb: "Looks familiar; behaves differently.", + }, + "antonym": { + glyph: "🔄", + label: "Antonym", + pedagogy: "Remap the intuition", + cls: "k-antonym", + blurb: "Related but inverted — flip your expectation.", + }, + "alien-realization": { + glyph: "🛸", + label: "Alien realization", + pedagogy: "Bridge with effort", + cls: "k-alien", + blurb: "Same goal, unfamiliar machinery.", + }, + "novel": { + glyph: "✨", + label: "Novel", + pedagogy: "Teach de novo", + cls: "k-novel", + blurb: "No prior anchor — learn it fresh.", + }, + "vanished": { + glyph: "👻", + label: "Vanished", + pedagogy: "Un-learn / re-route", + cls: "k-vanished", + blurb: "A habit to drop — it's gone here.", + }, +} const RESIDUE = { - "none": { glyph: "∅", label: "none (true isomorphism)" }, - "inverted": { glyph: "🔃", label: "inverted" }, - "lossy": { glyph: "〜", label: "lossy" }, + "none": { glyph: "∅", label: "none (true isomorphism)" }, + "inverted": { glyph: "🔃", label: "inverted" }, + "lossy": { glyph: "〜", label: "lossy" }, "absent-source": { glyph: "⌀→", label: "absent source (novel)" }, "absent-target": { glyph: "→⌀", label: "absent target (vanished)" }, -}; -const STRATA = ["surface", "structure", "intention", "trope", "invariant"]; +} +const STRATA = ["surface", "structure", "intention", "trope", "invariant"] const NARR = [ - ["celebrate", "Celebrate"], ["minimise", "Minimise"], ["better", "Better"], - ["safety", "Safety"], ["example", "Example"], -]; + ["celebrate", "Celebrate"], + ["minimise", "Minimise"], + ["better", "Better"], + ["safety", "Safety"], + ["example", "Example"], +] -const state = { items: [], view: "side-by-side", kind: "all", query: "", selected: 0 }; +const state = { items: [], view: "side-by-side", kind: "all", query: "", selected: 0 } -const $ = (id) => document.getElementById(id); +const $ = (id) => document.getElementById(id) function node(tag, opts = {}, kids = []) { - const el = document.createElement(tag); + const el = document.createElement(tag) for (const [k, v] of Object.entries(opts)) { - if (v == null) continue; - if (k === "class") el.className = v; - else if (k === "text") el.textContent = v; - else if (k === "html") el.innerHTML = v; - else el.setAttribute(k, v); + if (v == null) continue + if (k === "class") el.className = v + else if (k === "text") el.textContent = v + else if (k === "html") el.innerHTML = v + else el.setAttribute(k, v) } - for (const kid of [].concat(kids)) if (kid != null) el.append(kid); - return el; + for (const kid of [].concat(kids)) if (kid != null) el.append(kid) + return el } function kindBadge(kind) { - const k = KIND[kind] ?? { glyph: "?", label: kind, cls: "" }; - return node("span", { class: `badge ${k.cls}` }, [`${k.glyph} ${k.label}`]); + const k = KIND[kind] ?? { glyph: "?", label: kind, cls: "" } + return node("span", { class: `badge ${k.cls}` }, [`${k.glyph} ${k.label}`]) } function verdictAt(strata, name) { - const v = (strata ?? []).find((s) => s.stratum === name); - return v ? v.holds : null; + const v = (strata ?? []).find((s) => s.stratum === name) + return v ? v.holds : null } // Mirrors Abi.Correspondence.isFalseFriendShape: surface holds AND intention diverges. function isFalseFriendShape(strata) { - return verdictAt(strata, "surface") === true && verdictAt(strata, "intention") === false; + return verdictAt(strata, "surface") === true && verdictAt(strata, "intention") === false } function filtered() { - const q = state.query.trim().toLowerCase(); + const q = state.query.trim().toLowerCase() return state.items.filter((it) => { - if (state.kind !== "all" && it.kind !== state.kind) return false; - if (!q) return true; + if (state.kind !== "all" && it.kind !== state.kind) return false + if (!q) return true return [it.concept, it.from?.language, it.to?.language, it.from?.surface, it.to?.surface] - .filter(Boolean).some((s) => String(s).toLowerCase().includes(q)); - }); + .filter(Boolean).some((s) => String(s).toLowerCase().includes(q)) + }) } function renderIndex() { - const list = $("index"); - const items = filtered(); - $("indexCount").textContent = `${items.length} of ${state.items.length}`; - list.replaceChildren(); + const list = $("index") + const items = filtered() + $("indexCount").textContent = `${items.length} of ${state.items.length}` + list.replaceChildren() if (items.length === 0) { - list.append(node("li", { class: "empty", role: "presentation", text: "No correspondences match." })); - renderDetail(null); - return; + list.append( + node("li", { class: "empty", role: "presentation", text: "No correspondences match." }), + ) + renderDetail(null) + return } - if (state.selected >= items.length) state.selected = 0; + if (state.selected >= items.length) state.selected = 0 items.forEach((it, i) => { const li = node("li", { - role: "option", id: `corr-${i}`, tabindex: "-1", + role: "option", + id: `corr-${i}`, + tabindex: "-1", "aria-selected": String(i === state.selected), }, [ node("div", { class: "concept", text: it.concept }), - node("div", { class: "pair", text: `${it.from?.language ?? "?"} → ${it.to?.language ?? "?"}` }), + node("div", { + class: "pair", + text: `${it.from?.language ?? "?"} → ${it.to?.language ?? "?"}`, + }), kindBadge(it.kind), - ]); - li.addEventListener("click", () => { state.selected = i; syncSelection(); }); - list.append(li); - }); - renderDetail(items[state.selected]); - list.setAttribute("aria-activedescendant", `corr-${state.selected}`); + ]) + li.addEventListener("click", () => { + state.selected = i + syncSelection() + }) + list.append(li) + }) + renderDetail(items[state.selected]) + list.setAttribute("aria-activedescendant", `corr-${state.selected}`) } function syncSelection() { - const items = filtered(); + const items = filtered() document.querySelectorAll("#index li[role=option]").forEach((li, i) => { - li.setAttribute("aria-selected", String(i === state.selected)); - }); - $("index").setAttribute("aria-activedescendant", `corr-${state.selected}`); - const el = $(`corr-${state.selected}`); - if (el) el.scrollIntoView({ block: "nearest" }); - renderDetail(items[state.selected]); + li.setAttribute("aria-selected", String(i === state.selected)) + }) + $("index").setAttribute("aria-activedescendant", `corr-${state.selected}`) + const el = $(`corr-${state.selected}`) + if (el) el.scrollIntoView({ block: "nearest" }) + renderDetail(items[state.selected]) } // ---- view-layers ------------------------------------------------------------ @@ -113,62 +162,86 @@ function formCard(form) { return node("div", { class: "form-card" }, [ node("div", { class: "form-lang", text: form?.language ?? "?" }), node("pre", { class: "surface" }, [node("code", { text: form?.surface ?? "" })]), - ]); + ]) } function renderForms(it) { - const host = $("paneForms"); - host.replaceChildren(); - if (!it) { host.append(node("div", { class: "empty", text: "Select a correspondence." })); return; } - host.append(node("h2", { class: "concept-head", text: it.concept })); - const k = KIND[it.kind] ?? {}; + const host = $("paneForms") + host.replaceChildren() + if (!it) { + host.append(node("div", { class: "empty", text: "Select a correspondence." })) + return + } + host.append(node("h2", { class: "concept-head", text: it.concept })) + const k = KIND[it.kind] ?? {} if (state.view === "raw") { host.append(node("pre", { class: "surface" }, [node("code", { text: `${it.from?.language}: ${it.from?.surface}\n${it.to?.language}: ${it.to?.surface}`, - })])); - return; + })])) + return } if (state.view === "focus") { host.append(node("div", { class: "focus-form" }, [ - node("p", { class: "pane-hint", text: `from your ${it.from?.language}: ${it.from?.surface}` }), + node("p", { + class: "pane-hint", + text: `from your ${it.from?.language}: ${it.from?.surface}`, + }), formCard(it.to), - ])); - return; + ])) + return } if (state.view === "glyph") { host.append(node("div", { class: "glyph-view" }, [ node("div", { class: "big", text: k.glyph ?? "?" }), node("div", { text: k.label ?? it.kind }), node("div", { class: "row" }, [ - node("div", {}, [node("div", { class: "form-lang", text: it.from?.language }), node("code", { text: it.from?.surface })]), + node("div", {}, [ + node("div", { class: "form-lang", text: it.from?.language }), + node("code", { text: it.from?.surface }), + ]), node("div", { class: "cross", text: "→" }), - node("div", {}, [node("div", { class: "form-lang", text: it.to?.language }), node("code", { text: it.to?.surface })]), + node("div", {}, [ + node("div", { class: "form-lang", text: it.to?.language }), + node("code", { text: it.to?.surface }), + ]), ]), - ])); - return; + ])) + return } if (state.view === "blockly") { host.append(node("div", { class: "blockly" }, [ - node("div", { class: "block" }, [node("div", { class: "blang", text: it.from?.language }), node("code", { text: it.from?.surface })]), + node("div", { class: "block" }, [ + node("div", { class: "blang", text: it.from?.language }), + node("code", { text: it.from?.surface }), + ]), node("div", { class: "connector", text: `▼ ${k.glyph ?? ""} ${k.label ?? it.kind}` }), - node("div", { class: "block" }, [node("div", { class: "blang", text: it.to?.language }), node("code", { text: it.to?.surface })]), - ])); - return; + node("div", { class: "block" }, [ + node("div", { class: "blang", text: it.to?.language }), + node("code", { text: it.to?.surface }), + ]), + ])) + return } // side-by-side (default) host.append(node("div", { class: "sxs" }, [ formCard(it.from), - node("div", { class: "cross" }, [document.createTextNode(k.glyph ?? "→"), node("small", { text: "becomes" })]), + node("div", { class: "cross" }, [ + document.createTextNode(k.glyph ?? "→"), + node("small", { text: "becomes" }), + ]), formCard(it.to), - ])); + ])) } function renderClassify(it) { - const host = $("paneClassify"); - host.replaceChildren(); - if (!it) { host.append(node("div", { class: "empty", text: "—" })); return; } - const k = KIND[it.kind] ?? { glyph: "?", label: it.kind, pedagogy: "", blurb: "", cls: "" }; + const host = $("paneClassify") + host.replaceChildren() + if (!it) { + host.append(node("div", { class: "empty", text: "—" })) + return + } + const k = KIND[it.kind] ?? { glyph: "?", label: it.kind, pedagogy: "", blurb: "", cls: "" } host.append(node("div", { class: "kind-hero" }, [ node("div", { class: `glyph ${k.cls}`, text: k.glyph }), @@ -177,9 +250,9 @@ function renderClassify(it) { node("div", { class: "pedagogy", text: k.pedagogy }), node("div", { class: "blurb", text: k.blurb }), ]), - ])); + ])) - const res = RESIDUE[it.residue?.shape] ?? { glyph: "?", label: it.residue?.shape ?? "—" }; + const res = RESIDUE[it.residue?.shape] ?? { glyph: "?", label: it.residue?.shape ?? "—" } host.append(node("div", { class: "section" }, [ node("h3", { text: "Residue" }), node("div", { class: "residue" }, [ @@ -187,72 +260,120 @@ function renderClassify(it) { node("span", { class: "shape", text: res.label }), ]), it.residue?.note ? node("div", { class: "residue-note", text: it.residue.note }) : null, - ])); + ])) - const strataRow = node("div", { class: "strata" }, STRATA.map((s) => { - const v = verdictAt(it.strata, s); - const cls = v === true ? "holds" : v === false ? "diverges" : ""; - const mark = v === true ? "✓" : v === false ? "✗" : "—"; - return node("span", { class: `stratum ${cls}`, text: `${s} ${mark}` }); - })); - const strataSection = node("div", { class: "section" }, [node("h3", { text: "Strata (levels of objects)" }), strataRow]); + const strataRow = node( + "div", + { class: "strata" }, + STRATA.map((s) => { + const v = verdictAt(it.strata, s) + const cls = v === true ? "holds" : v === false ? "diverges" : "" + const mark = v === true ? "✓" : v === false ? "✗" : "—" + return node("span", { class: `stratum ${cls}`, text: `${s} ${mark}` }) + }), + ) + const strataSection = node("div", { class: "section" }, [ + node("h3", { text: "Strata (levels of objects)" }), + strataRow, + ]) if (isFalseFriendShape(it.strata)) { - strataSection.append(node("div", { class: "ff-flag", text: "⚠ False-friend signature: corresponds at the surface, diverges at intention." })); + strataSection.append( + node("div", { + class: "ff-flag", + text: "⚠ False-friend signature: corresponds at the surface, diverges at intention.", + }), + ) } - host.append(strataSection); + host.append(strataSection) if (it.narrative) { const blocks = NARR.filter(([key]) => it.narrative[key]) - .map(([key, label]) => node("div", { class: "n" }, [node("b", { text: label }), document.createTextNode(it.narrative[key])])); + .map(([key, label]) => + node("div", { class: "n" }, [ + node("b", { text: label }), + document.createTextNode(it.narrative[key]), + ]) + ) if (blocks.length) { - host.append(node("div", { class: "section" }, [node("h3", { text: "Narrative (no shame)" }), node("div", { class: "narrative" }, blocks)])); + host.append( + node("div", { class: "section" }, [ + node("h3", { text: "Narrative (no shame)" }), + node("div", { class: "narrative" }, blocks), + ]), + ) } } if (it.witness) { - host.append(node("div", { class: "section" }, [node("h3", { text: "Witness" }), node("code", { text: it.witness })])); + host.append( + node("div", { class: "section" }, [ + node("h3", { text: "Witness" }), + node("code", { text: it.witness }), + ]), + ) } } -function renderDetail(it) { renderForms(it); renderClassify(it); } +function renderDetail(it) { + renderForms(it) + renderClassify(it) +} // ---- events ----------------------------------------------------------------- function onKeyNav(e) { - const items = filtered(); - if (!items.length) return; + const items = filtered() + if (!items.length) return if (e.key === "ArrowDown" || e.key === "ArrowUp") { - e.preventDefault(); - const d = e.key === "ArrowDown" ? 1 : -1; - state.selected = (state.selected + d + items.length) % items.length; - syncSelection(); - } else if (e.key === "Home") { e.preventDefault(); state.selected = 0; syncSelection(); } - else if (e.key === "End") { e.preventDefault(); state.selected = items.length - 1; syncSelection(); } + e.preventDefault() + const d = e.key === "ArrowDown" ? 1 : -1 + state.selected = (state.selected + d + items.length) % items.length + syncSelection() + } else if (e.key === "Home") { + e.preventDefault() + state.selected = 0 + syncSelection() + } else if (e.key === "End") { + e.preventDefault() + state.selected = items.length - 1 + syncSelection() + } } async function init() { - $("filterKind").addEventListener("change", (e) => { state.kind = e.target.value; state.selected = 0; renderIndex(); }); - $("search").addEventListener("input", (e) => { state.query = e.target.value; state.selected = 0; renderIndex(); }); - $("index").addEventListener("keydown", onKeyNav); + $("filterKind").addEventListener("change", (e) => { + state.kind = e.target.value + state.selected = 0 + renderIndex() + }) + $("search").addEventListener("input", (e) => { + state.query = e.target.value + state.selected = 0 + renderIndex() + }) + $("index").addEventListener("keydown", onKeyNav) document.querySelectorAll("input[name=view]").forEach((r) => r.addEventListener("change", (e) => { - state.view = e.target.value; $("viewHint").textContent = e.target.value; - renderForms(filtered()[state.selected]); - })); + state.view = e.target.value + $("viewHint").textContent = e.target.value + renderForms(filtered()[state.selected]) + }) + ) try { - const res = await fetch("/api/cartridges"); - const body = await res.json(); - if (!res.ok) throw new Error(body.error || "load failed"); + const res = await fetch("/api/cartridges") + const body = await res.json() + if (!res.ok) throw new Error(body.error || "load failed") state.items = (body.cartridges || []).flatMap((c) => - (c.transitions || []).map((t) => ({ ...t, cartridge: c.cartridge }))); + (c.transitions || []).map((t) => ({ ...t, cartridge: c.cartridge })) + ) $("status").textContent = state.items.length ? `${state.items.length} correspondences across ${body.cartridges.length} cartridge(s)` - : "No cartridges found."; - renderIndex(); + : "No cartridges found." + renderIndex() } catch (err) { - $("status").textContent = `Error: ${err.message}`; - $("index").append(node("li", { class: "empty", text: err.message })); + $("status").textContent = `Error: ${err.message}` + $("index").append(node("li", { class: "empty", text: err.message })) } } -init(); +init() diff --git a/gui/server.js b/gui/server.js index 87334aa..d0d5489 100644 --- a/gui/server.js +++ b/gui/server.js @@ -9,125 +9,125 @@ // deno run --allow-read --allow-net gui/server.js [--port N] [--host H] [--open] // (add --allow-env to honour EVANGELISER_GUI_PORT; --allow-run for --open) -const root = new URL("../", import.meta.url); +const root = new URL("../", import.meta.url) function argValue(name, fallback) { - const i = Deno.args.indexOf(name); - return i === -1 || i + 1 >= Deno.args.length ? fallback : Deno.args[i + 1]; + const i = Deno.args.indexOf(name) + return i === -1 || i + 1 >= Deno.args.length ? fallback : Deno.args[i + 1] } function envPort() { try { - return Deno.env.get("EVANGELISER_GUI_PORT"); + return Deno.env.get("EVANGELISER_GUI_PORT") } catch { - return undefined; // --allow-env not granted; fine + return undefined // --allow-env not granted; fine } } async function loadCartridges() { - const dir = new URL("cartridges/", root); - const out = []; + const dir = new URL("cartridges/", root) + const out = [] async function walk(d) { - let entries; + let entries try { - entries = Deno.readDir(d); + entries = Deno.readDir(d) } catch { - return; + return } for await (const e of entries) { - const child = new URL(e.name + (e.isDirectory ? "/" : ""), d); + const child = new URL(e.name + (e.isDirectory ? "/" : ""), d) if (e.isDirectory) { - await walk(child); - continue; + await walk(child) + continue } - if (!e.name.endsWith(".cartridge.json")) continue; + if (!e.name.endsWith(".cartridge.json")) continue try { - const c = JSON.parse(await Deno.readTextFile(child)); + const c = JSON.parse(await Deno.readTextFile(child)) out.push({ cartridge: c.name ?? e.name, description: c.description ?? "", languages: c.languages ?? [], transitions: Array.isArray(c.transitions) ? c.transitions : [], - }); + }) } catch (err) { - console.error(`skip ${e.name}: ${err.message}`); + console.error(`skip ${e.name}: ${err.message}`) } } } - await walk(dir); - return out; + await walk(dir) + return out } const contentTypes = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", -}; +} async function staticFile(name) { - const body = await Deno.readFile(new URL(name, import.meta.url)); - const ext = name.slice(name.lastIndexOf(".")); + const body = await Deno.readFile(new URL(name, import.meta.url)) + const ext = name.slice(name.lastIndexOf(".")) return new Response(body, { headers: { "content-type": contentTypes[ext] ?? "application/octet-stream" }, - }); + }) } function json(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" }, - }); + }) } async function handler(req) { - const url = new URL(req.url); + const url = new URL(req.url) try { if (req.method === "GET") { - if (url.pathname === "/") return await staticFile("app.html"); - if (url.pathname === "/app.js") return await staticFile("app.js"); + if (url.pathname === "/") return await staticFile("app.html") + if (url.pathname === "/app.js") return await staticFile("app.js") if (url.pathname === "/api/cartridges") { - return json({ cartridges: await loadCartridges() }); + return json({ cartridges: await loadCartridges() }) } } - return json({ error: "Not found" }, 404); + return json({ error: "Not found" }, 404) } catch (err) { - console.error(err); - return json({ error: err.message ?? String(err) }, 500); + console.error(err) + return json({ error: err.message ?? String(err) }, 500) } } -const requestedPort = Number(argValue("--port", envPort() ?? "8765")); -const hostname = argValue("--host", "127.0.0.1"); -let server; -let port = requestedPort; +const requestedPort = Number(argValue("--port", envPort() ?? "8765")) +const hostname = argValue("--host", "127.0.0.1") +let server +let port = requestedPort for (let attempt = 0; attempt < 20; attempt += 1) { try { - port = requestedPort + attempt; - server = Deno.serve({ hostname, port, onListen: () => {} }, handler); - break; + port = requestedPort + attempt + server = Deno.serve({ hostname, port, onListen: () => {} }, handler) + break } catch (err) { - if (String(err.message ?? err).includes("Address already in use")) continue; - throw err; + if (String(err.message ?? err).includes("Address already in use")) continue + throw err } } if (!server) { - throw new Error(`No free port from ${requestedPort} to ${requestedPort + 19}`); + throw new Error(`No free port from ${requestedPort} to ${requestedPort + 19}`) } -const addr = `http://${hostname}:${port}/`; -console.log(`Correspondence workspace: ${addr}`); +const addr = `http://${hostname}:${port}/` +console.log(`Correspondence workspace: ${addr}`) if (Deno.args.includes("--open")) { setTimeout(() => { for (const cmd of [["sensible-browser", addr], ["gio", "open", addr], ["xdg-open", addr]]) { try { - new Deno.Command(cmd[0], { args: cmd.slice(1), stdout: "null", stderr: "null" }).spawn(); - return; + new Deno.Command(cmd[0], { args: cmd.slice(1), stdout: "null", stderr: "null" }).spawn() + return } catch { // try the next opener } } - }, 250); + }, 250) } -await server.finished; +await server.finished diff --git a/rescript.json b/rescript.json deleted file mode 100644 index 582766e..0000000 --- a/rescript.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "nextgen-languages-evangeliser", - "sources": [ - { - "dir": "src", - "subdirs": true - }, - { - "dir": "test", - "subdirs": true, - "type": "dev" - } - ], - "package-specs": [ - { - "module": "esmodule", - "in-source": true - } - ], - "suffix": ".res.js", - "dependencies": [ - "@rescript/core" - ], - "warnings": { - "number": "+a-4-9-27-40-42-48-50-61-102-109" - } -} diff --git a/scripts/rescript-build.sh b/scripts/rescript-build.sh deleted file mode 100755 index b865d98..0000000 --- a/scripts/rescript-build.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 - -set -euo pipefail - -LOCK="lib/rescript.lock" - -if [ -f "$LOCK" ]; then - PID="$(tr -cd '0-9' < "$LOCK")" - if [ -n "$PID" ] && ! kill -0 "$PID" 2>/dev/null; then - mv "$LOCK" "/tmp/nextgen-language-evangeliser-rescript.lock.${PID}.stale" - fi -fi - -exec deno run -A npm:rescript build "$@" diff --git a/scripts/validate-cartridges.js b/scripts/validate-cartridges.js index 70a6c0d..113e198 100644 --- a/scripts/validate-cartridges.js +++ b/scripts/validate-cartridges.js @@ -7,39 +7,39 @@ // // Exits non-zero if any cartridge fails — suitable for CI. -import Ajv2020 from "npm:ajv@8/dist/2020.js"; +import Ajv2020 from "npm:ajv@8/dist/2020.js" -const dir = new URL("../cartridges/", import.meta.url); +const dir = new URL("../cartridges/", import.meta.url) const schema = JSON.parse( await Deno.readTextFile(new URL("correspondence-cartridge.schema.json", dir)), -); +) -const AjvCtor = Ajv2020.default ?? Ajv2020; -const validate = new AjvCtor({ allErrors: true, strict: false }).compile(schema); +const AjvCtor = Ajv2020.default ?? Ajv2020 +const validate = new AjvCtor({ allErrors: true, strict: false }).compile(schema) -let count = 0; -let bad = 0; +let count = 0 +let bad = 0 async function walk(d) { for await (const entry of Deno.readDir(d)) { - const child = new URL(entry.name + (entry.isDirectory ? "/" : ""), d); + const child = new URL(entry.name + (entry.isDirectory ? "/" : ""), d) if (entry.isDirectory) { - await walk(child); - continue; + await walk(child) + continue } - if (!entry.name.endsWith(".cartridge.json")) continue; - count++; - const data = JSON.parse(await Deno.readTextFile(child)); + if (!entry.name.endsWith(".cartridge.json")) continue + count++ + const data = JSON.parse(await Deno.readTextFile(child)) if (validate(data)) { - console.log(`VALID ${entry.name} (${data.transitions.length} transitions)`); + console.log(`VALID ${entry.name} (${data.transitions.length} transitions)`) } else { - bad++; - console.error(`INVALID ${entry.name}`); - console.error(JSON.stringify(validate.errors, null, 2)); + bad++ + console.error(`INVALID ${entry.name}`) + console.error(JSON.stringify(validate.errors, null, 2)) } } } -await walk(dir); -console.log(`\n${count - bad}/${count} cartridges valid.`); -Deno.exit(bad ? 1 : 0); +await walk(dir) +console.log(`\n${count - bad}/${count} cartridges valid.`) +Deno.exit(bad ? 1 : 0) diff --git a/src/Analyser.res b/src/Analyser.res deleted file mode 100644 index 0f2536b..0000000 --- a/src/Analyser.res +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Analyser: Orchestrates scanning and produces analysis results -// Computes coverage, difficulty assessment, and pattern suggestions - -open Types - -// Determine overall difficulty based on matched patterns -let assessDifficulty = (matches: array): difficultyLevel => { - let advancedCount = matches->Array.filter(m => m.pattern.difficulty === Advanced)->Array.length - let intermediateCount = - matches->Array.filter(m => m.pattern.difficulty === Intermediate)->Array.length - - if advancedCount > 0 { - Advanced - } else if intermediateCount > 0 { - Intermediate - } else { - Beginner - } -} - -// Suggest next patterns to learn based on what was matched -let suggestNextPatterns = ( - matches: array, - allPatterns: array, -): array => { - let matchedIds = matches->Array.map(m => m.pattern.id) - let matchedCategories = - matches - ->Array.map(m => categoryToString(m.pattern.category)) - ->Set.fromArray - - // Suggest related patterns from matched categories that weren't found - let relatedSuggestions = - matches - ->Array.flatMap(m => m.pattern.relatedPatterns) - ->Array.filter(id => !(matchedIds->Array.includes(id))) - ->Array.filterMap(id => allPatterns->Array.find(p => p.id === id)) - - // Suggest patterns from unmatched categories (prioritise Beginner) - let unmatchedCategorySuggestions = - allPatterns - ->Array.filter(p => !(matchedCategories->Set.has(categoryToString(p.category)))) - ->Array.filter(p => p.difficulty === Beginner) - - // Combine, deduplicate, take top 5 - let seen = Dict.make() - Array.concat(relatedSuggestions, unmatchedCategorySuggestions) - ->Array.filter(p => { - switch seen->Dict.get(p.id) { - | Some(_) => false - | None => - seen->Dict.set(p.id, true) - true - } - }) - ->Array.slice(~start=0, ~end=5) -} - -// Compute coverage: what percentage of available categories were matched -let computeCoverage = (matches: array): float => { - let totalCategories = 20 // from Types.patternCategory - let matchedCategories = - matches - ->Array.map(m => categoryToString(m.pattern.category)) - ->Set.fromArray - ->Set.size - - if totalCategories === 0 { - 0.0 - } else { - Int.toFloat(matchedCategories) /. Int.toFloat(totalCategories) *. 100.0 - } -} - -// Run full analysis on a code string -let analyse = (code: string): analysisResult => { - let startTime = Date.now() - let matches = Scanner.scanAll(code) - let endTime = Date.now() - - { - matches, - totalPatterns: Patterns.getPatternCount(), - coveragePercentage: computeCoverage(matches), - difficulty: assessDifficulty(matches), - suggestedNextPatterns: suggestNextPatterns(matches, Patterns.patternLibrary), - analysisTime: endTime -. startTime, - memoryUsed: 0, - } -} - -// Analyse with a custom pattern set -let analyseWithPatterns = (code: string, patterns: array): analysisResult => { - let startTime = Date.now() - let matches = Scanner.scanCode(code, patterns) - let endTime = Date.now() - - { - matches, - totalPatterns: patterns->Array.length, - coveragePercentage: computeCoverage(matches), - difficulty: assessDifficulty(matches), - suggestedNextPatterns: suggestNextPatterns(matches, patterns), - analysisTime: endTime -. startTime, - memoryUsed: 0, - } -} - -// Generate a narrative summary for the analysis -let summarise = (result: analysisResult): string => { - let matchCount = result.matches->Array.length - let uniquePatterns = Scanner.uniquePatterns(result.matches)->Array.length - let diffStr = difficultyToString(result.difficulty) - let coverageStr = Float.toFixed(result.coveragePercentage, ~digits=1) - - if matchCount === 0 { - "No JavaScript patterns detected. Try pasting some JavaScript code to see how ReScript can improve it!" - } else { - let patternsWord = if uniquePatterns === 1 { "pattern" } else { "patterns" } - `Found ${Int.toString(matchCount)} match(es) across ${Int.toString(uniquePatterns)} unique ${patternsWord}.\n` ++ - `Code complexity: ${diffStr}\n` ++ - `Category coverage: ${coverageStr}%\n` ++ - if result.suggestedNextPatterns->Array.length > 0 { - let suggestions = - result.suggestedNextPatterns - ->Array.map(p => p.name) - ->Array.join(", ") - `\nSuggested next patterns to explore: ${suggestions}` - } else { - "" - } - } -} diff --git a/src/Analyser.resi b/src/Analyser.resi deleted file mode 100644 index de6f7ea..0000000 --- a/src/Analyser.resi +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Analyser: public interface for code analysis orchestration - -// Run full analysis on JavaScript code using the built-in pattern library -let analyse: (string) => Types.analysisResult - -// Run analysis with a custom set of patterns -let analyseWithPatterns: (string, array) => Types.analysisResult - -// Generate a human-readable narrative summary of analysis results -let summarise: (Types.analysisResult) => string diff --git a/src/Cli.res b/src/Cli.res deleted file mode 100644 index 31f21db..0000000 --- a/src/Cli.res +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// CLI: Entry point for the nextgen-languages-evangeliser command-line tool -// Usage: evangeliser scan | patterns | legend - -open Types - -// Read file contents via Deno or Node fs -@module("node:fs") external readFileSync: (string, string) => string = "readFileSync" - -// Read command-line arguments -@val external argv: array = "process.argv" - -// Parse CLI options from args -type cliOptions = { - command: string, - file: option, - format: string, - difficulty: option, - view: viewLayer, - target: targetLang, -} - -let parseArgs = (args: array): cliOptions => { - // Skip first two args (deno/node + script path) - let userArgs = args->Array.slice(~start=2, ~end=Array.length(args)) - - let command = userArgs->Array.get(0)->Option.getOr("help") - let file = ref(None) - let format = ref("plain") - let difficulty = ref(None) - let view = ref(RAW) - let target = ref(flagshipTarget) - - userArgs->Array.forEachWithIndex((_arg, idx) => { - let arg = userArgs->Array.getUnsafe(idx) - switch arg { - | "--format" => - switch userArgs->Array.get(idx + 1) { - | Some("markdown") => format := "markdown" - | Some("html") => format := "html" - | _ => format := "plain" - } - | "--difficulty" => - switch userArgs->Array.get(idx + 1) { - | Some("beginner") => difficulty := Some(Beginner) - | Some("intermediate") => difficulty := Some(Intermediate) - | Some("advanced") => difficulty := Some(Advanced) - | _ => () - } - | "--view" => - switch userArgs->Array.get(idx + 1) { - | Some("raw") => view := RAW - | Some("folded") => view := FOLDED - | Some("glyphed") => view := GLYPHED - | _ => () - } - | "--target" => - switch userArgs->Array.get(idx + 1) { - | Some(t) => - switch stringToTargetLang(t) { - | Some(lang) => target := lang - | None => () // unknown target: keep default (flagship) - } - | None => () - } - | _ => - // First non-flag arg after command is the file - if idx > 0 && !(String.startsWith(arg, "--")) { - let prevArg = userArgs->Array.get(idx - 1)->Option.getOr("") - if !(String.startsWith(prevArg, "--")) || idx === 1 { - if file.contents === None && idx >= 1 { - file := Some(arg) - } - } - } - } - }) - - { - command, - file: file.contents, - format: format.contents, - difficulty: difficulty.contents, - view: view.contents, - target: target.contents, - } -} - -let showHelp = (): string => { - `Nextgen Languages Evangeliser - "Celebrate good, minimize bad, show better" - -Usage: - evangeliser scan Scan a JavaScript file for improvable patterns - evangeliser patterns List all available transformation patterns - evangeliser legend Show the glyph legend - evangeliser stats Show pattern library statistics - evangeliser help Show this help message - -Options: - --format plain|markdown|html Output format (default: plain) - --difficulty beginner|intermediate|advanced Filter patterns by difficulty - --view raw|folded|glyphed View layer (default: raw) - --target affinescript|rescript|rust|gleam|zig - Target language for transformations - (default: affinescript — the flagship target). - Patterns that have not yet been ported to the - requested target fall back to their first - available example with a note. - -Examples: - evangeliser scan app.js - evangeliser scan utils.js --format markdown --view folded - evangeliser scan app.js --target rescript - evangeliser patterns --difficulty beginner -` -} - -let showStats = (): string => { - let stats = Patterns.getPatternStats() - let lines = ref(`Pattern Library Statistics\n`) - lines := lines.contents ++ `Total patterns: ${Int.toString(stats.total)}\n\n` - lines := lines.contents ++ `By difficulty:\n` - stats.byDifficulty->Dict.toArray->Array.forEach(((key, count)) => { - lines := lines.contents ++ ` ${key}: ${Int.toString(count)}\n` - }) - lines := lines.contents ++ `\nBy category:\n` - stats.byCategory->Dict.toArray->Array.forEach(((key, count)) => { - lines := lines.contents ++ ` ${key}: ${Int.toString(count)}\n` - }) - - // Also count patterns per target, so users can see coverage at a glance. - let byTarget = Dict.make() - Patterns.patternLibrary->Array.forEach(p => { - p.targets->Array.forEach(t => { - let key = targetLangLabel(t.language) - let current = byTarget->Dict.get(key)->Option.getOr(0) - byTarget->Dict.set(key, current + 1) - }) - }) - lines := lines.contents ++ `\nBy target (patterns supplying that target):\n` - byTarget->Dict.toArray->Array.forEach(((key, count)) => { - lines := lines.contents ++ ` ${key}: ${Int.toString(count)}\n` - }) - - lines.contents -} - -let run = (): unit => { - let opts = parseArgs(argv) - - let output = switch opts.command { - | "scan" => - switch opts.file { - | None => "Error: Please specify a JavaScript file to scan.\n\nUsage: evangeliser scan \n" - | Some(filePath) => - try { - let code = readFileSync(filePath, "utf-8") - let result = switch opts.difficulty { - | None => Analyser.analyse(code) - | Some(diff) => - let filtered = Patterns.getPatternsByDifficulty(diff) - Analyser.analyseWithPatterns(code, filtered) - } - Output.format(result, opts.view, opts.format, opts.target) - } catch { - | JsExn(e) => - let msg = e->JsExn.message->Option.getOr("Unknown error") - `Error reading file '${filePath}': ${msg}\n` - } - } - - | "patterns" => - Output.formatPatternList(opts.format) - - | "legend" => - Glyphs.createGlyphLegend() - - | "stats" => - showStats() - - | "help" | "--help" | "-h" => - showHelp() - - | unknown => - `Unknown command: '${unknown}'\n\n` ++ showHelp() - } - - Console.log(output) -} - -// Execute -run() diff --git a/src/Glyphs.res b/src/Glyphs.res deleted file mode 100644 index 230fbd3..0000000 --- a/src/Glyphs.res +++ /dev/null @@ -1,304 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Makaton-inspired glyph system for Nextgen Languages Evangeliser -// Glyphs transcend syntax to show semantic meaning. - -open Types - -// Core set of glyphs representing fundamental programming concepts -let coreGlyphs: Dict.t = Dict.fromArray([ - ( - "TRANSFORM", - { - symbol: `🔄`, - name: "Transform", - meaning: "Data transformation or mapping operation", - semanticCategory: Transformation, - usageExample: "array.map(x => x * 2) becomes array->Array.map(x => x * 2)", - }, - ), - ( - "TARGET", - { - symbol: `🎯`, - name: "Target", - meaning: "Precise type targeting - no ambiguity", - semanticCategory: Safety, - usageExample: "Explicit type annotation ensures correctness", - }, - ), - ( - "SHIELD", - { - symbol: `🛡️`, - name: "Shield", - meaning: "Protection from null/undefined errors", - semanticCategory: Safety, - usageExample: "Option prevents null reference errors", - }, - ), - ( - "FLOW", - { - symbol: `➡️`, - name: "Flow", - meaning: "Pipe operator - data flows naturally left to right", - semanticCategory: Flow, - usageExample: "data->transform->filter->map reads like English", - }, - ), - ( - "BRANCH", - { - symbol: `🌿`, - name: "Branch", - meaning: "Pattern matching - exhaustive branching", - semanticCategory: Flow, - usageExample: "switch statement with all cases covered", - }, - ), - ( - "PACKAGE", - { - symbol: `📦`, - name: "Package", - meaning: "Module or encapsulated data structure", - semanticCategory: Structure, - usageExample: "Module or record type grouping related data", - }, - ), - ( - "LINK", - { - symbol: `🔗`, - name: "Link", - meaning: "Composition - connecting functions or modules", - semanticCategory: Structure, - usageExample: "Function composition or module composition", - }, - ), - ( - "CRYSTAL", - { - symbol: `💎`, - name: "Crystal", - meaning: "Immutability - unchanging, pure data", - semanticCategory: Data, - usageExample: "Immutable data structures prevent accidental mutation", - }, - ), - ( - "HOURGLASS", - { - symbol: `⏳`, - name: "Hourglass", - meaning: "Async operation - time-dependent computation", - semanticCategory: Flow, - usageExample: "async/await or Promise handling", - }, - ), - ( - "CHECKPOINT", - { - symbol: `✅`, - name: "Checkpoint", - meaning: "Result type - explicit success/failure handling", - semanticCategory: Safety, - usageExample: "Result makes error handling explicit", - }, - ), - ( - "LIGHT", - { - symbol: `💡`, - name: "Light", - meaning: "Type inference - compiler figures it out for you", - semanticCategory: Safety, - usageExample: "ReScript infers types without verbose annotations", - }, - ), -]) - -// Extended glyphs for additional patterns -let extendedGlyphs: Dict.t = Dict.fromArray([ - ( - "GEAR", - { - symbol: `⚙️`, - name: "Gear", - meaning: "Function - a working mechanism", - semanticCategory: Transformation, - usageExample: "Pure function that transforms inputs to outputs", - }, - ), - ( - "STACK", - { - symbol: `📚`, - name: "Stack", - meaning: "Collection or sequence of items", - semanticCategory: Data, - usageExample: "Array, List, or Stack data structure", - }, - ), - ( - "LOCK", - { - symbol: `🔒`, - name: "Lock", - meaning: "Encapsulation - private implementation", - semanticCategory: Structure, - usageExample: "Private module members or abstract types", - }, - ), - ( - "KEY", - { - symbol: `🔑`, - name: "Key", - meaning: "Access - public API or interface", - semanticCategory: Structure, - usageExample: "Public module signature or interface", - }, - ), - ( - "RECYCLE", - { - symbol: `♻️`, - name: "Recycle", - meaning: "Recursion - function calls itself", - semanticCategory: Flow, - usageExample: "Recursive function processing", - }, - ), - ( - "FILTER", - { - symbol: `🔍`, - name: "Filter", - meaning: "Selection or filtering operation", - semanticCategory: Transformation, - usageExample: "Array.filter or pattern matching with guards", - }, - ), - ( - "MERGE", - { - symbol: `🔀`, - name: "Merge", - meaning: "Combining or merging data", - semanticCategory: Transformation, - usageExample: "Object spread, record update, or concatenation", - }, - ), - ( - "WARNING", - { - symbol: `⚠️`, - name: "Warning", - meaning: "Potential issue or anti-pattern", - semanticCategory: Safety, - usageExample: "Code that could be improved for safety", - }, - ), - ( - "ROCKET", - { - symbol: `🚀`, - name: "Rocket", - meaning: "Performance optimization", - semanticCategory: Transformation, - usageExample: "Fast, optimized code generation", - }, - ), - ( - "PUZZLE", - { - symbol: `🧩`, - name: "Puzzle", - meaning: "Type composition - building complex types from simple ones", - semanticCategory: Structure, - usageExample: "Variant types, record types, or type composition", - }, - ), -]) - -// Get all available glyphs -let getAllGlyphs = (): array => { - let core = coreGlyphs->Dict.valuesToArray - let extended = extendedGlyphs->Dict.valuesToArray - Array.concat(core, extended) -} - -// Get glyph by symbol -let getGlyphBySymbol = (symbol: string): option => { - getAllGlyphs()->Array.find(g => g.symbol === symbol) -} - -// Get glyphs by semantic category -let getGlyphsByCategory = (category: semanticCategory): array => { - getAllGlyphs()->Array.filter(g => g.semanticCategory === category) -} - -// Annotate code with glyphs based on detected patterns -let annotateWithGlyphs = (code: string, glyphSymbols: array): string => { - let glyphBar = glyphSymbols->Array.join(" ") - `${glyphBar}\n${code}` -} - -// Create a glyph legend for educational purposes -let createGlyphLegend = (): string => { - let categories = [Safety, Transformation, Flow, Structure, Data] - - let legend = ref("# Glyph Legend\n\nVisual symbols representing programming concepts:\n\n") - - categories->Array.forEach(category => { - let categoryGlyphs = getGlyphsByCategory(category) - if categoryGlyphs->Array.length > 0 { - let categoryName = switch category { - | Safety => "Safety" - | Transformation => "Transformation" - | Flow => "Flow" - | Structure => "Structure" - | State => "State" - | Data => "Data" - } - legend := legend.contents ++ `## ${categoryName}\n\n` - - categoryGlyphs->Array.forEach(glyph => { - legend := - legend.contents ++ - `- ${glyph.symbol} **${glyph.name}**: ${glyph.meaning}\n` ++ - ` - Example: ${glyph.usageExample}\n\n` - }) - } - }) - - legend.contents -} - -// Get glyphs for a specific pattern category -let getGlyphsForPattern = (patternCategory: patternCategory): array => { - switch patternCategory { - | NullSafety => [`🛡️`, `✅`, `💎`] - | Async => [`⏳`, `🔄`, `➡️`] - | ErrorHandling => [`✅`, `🛡️`, `🌿`] - | ArrayOperations => [`🔄`, `📚`, `🔍`] - | Conditionals => [`🌿`, `🎯`, `🧩`] - | Destructuring => [`📦`, `🔑`, `🔀`] - | Defaults => [`🛡️`, `💎`, `🎯`] - | Functional => [`⚙️`, `🔗`, `💎`] - | Templates => [`🔄`, `🔀`, `➡️`] - | ArrowFunctions => [`⚙️`, `➡️`, `🔄`] - | Variants => [`🧩`, `🌿`, `🎯`] - | Modules => [`📦`, `🔒`, `🔑`] - | TypeSafety => [`🎯`, `🛡️`, `💡`] - | Immutability => [`💎`, `🔒`, `🛡️`] - | PatternMatching => [`🌿`, `🎯`, `✅`] - | PipeOperator => [`➡️`, `🔄`, `🔗`] - | OopToFp => [`⚙️`, `💎`, `🔗`] - | ClassesToRecords => [`📦`, `💎`, `🎯`] - | InheritanceToComposition => [`🔗`, `🧩`, `📦`] - | StateMachines => [`🌿`, `🧩`, `✅`] - | DataModeling => [`🧩`, `📦`, `🎯`] - } -} diff --git a/src/Narrative.res b/src/Narrative.res deleted file mode 100644 index df31986..0000000 --- a/src/Narrative.res +++ /dev/null @@ -1,329 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Narrative generation for Nextgen Languages Evangeliser -// Philosophy: "Celebrate good, minimize bad, show better" -// - NEVER shame developers -// - ALWAYS celebrate what their JavaScript does well -// - Gently show how ReScript makes it even better - -open Types - -// Template for narrative generation -type narrativeTemplate = { - celebrate: array, - minimize: array, - better: array, - safety: array, -} - -// Pick a random element from an array -let pickRandom = (arr: array): string => { - let len = arr->Array.length - if len === 0 { - "" - } else { - let idx = Math.floor(Math.random() *. Int.toFloat(len))->Float.toInt - arr->Array.getUnsafe(idx) - } -} - -// Narrative templates organized by category -let narrativeTemplates: Dict.t = Dict.fromArray([ - ( - "null-safety", - { - celebrate: [ - "You're already thinking about null and undefined - that's great defensive programming!", - "Nice! You're checking for null values - you understand the importance of safety.", - "You've got null checks in place - you're already thinking like a type-safe developer!", - "Smart move checking for null! You know that data isn't always what you expect.", - ], - minimize: [ - "The only small thing is that it's easy to forget one of these checks somewhere...", - "It's just a tiny bit verbose with all these conditional checks...", - "These checks work, though they do add a bit of cognitive overhead...", - "Nothing wrong with this approach, it's just that there's a way to make it even simpler...", - ], - better: [ - "ReScript's Option type makes null safety automatic - you literally can't forget a check!", - "With ReScript, the compiler ensures you handle the None case - no runtime surprises!", - "ReScript's Option type gives you the same safety with cleaner, more expressive code.", - "In ReScript, null safety is built into the type system - it's impossible to forget!", - ], - safety: [ - "The compiler won't let your code compile until you've handled both Some and None cases.", - "100% null-safe at compile time - no more 'Cannot read property of undefined' errors!", - "Type-level guarantee that you've considered the absence of a value.", - "ReScript eliminates an entire class of runtime errors related to null/undefined.", - ], - }, - ), - ( - "async", - { - celebrate: [ - "Async/await is a huge improvement over callback hell - you're writing modern JavaScript!", - "Great use of async/await! You understand asynchronous programming patterns.", - "Nice! You're already using async/await - you know how to handle asynchronous operations.", - "You've mastered one of JavaScript's most powerful features - async/await!", - ], - minimize: [ - "The only thing is that error handling can get a bit scattered with try/catch...", - "It works great, though tracking all the possible error states can be tricky...", - "This is solid code, there's just a way to make async errors more explicit...", - "Nothing wrong here, but error paths aren't always obvious in the type system...", - ], - better: [ - "ReScript makes async operations type-safe with Promise types that flow through your code!", - "With ReScript, your async functions have explicit return types - no surprises!", - "ReScript's type system tracks async operations and ensures you handle all cases.", - "In ReScript, async errors are part of the type signature - impossible to ignore!", - ], - safety: [ - "The compiler ensures you handle both success and failure cases for async operations.", - "Type-level tracking of async operations prevents race conditions and timing bugs.", - "Explicit Promise types make async behavior clear and prevent common mistakes.", - "ReScript's type system catches async errors at compile time, not runtime.", - ], - }, - ), - ( - "error-handling", - { - celebrate: [ - "Excellent! You're using try/catch - you know that errors need handling!", - "Great defensive programming! You're catching errors before they crash the app.", - "You understand that things can go wrong - that's mature error handling!", - "Nice! You're already thinking about the unhappy path - many developers forget this.", - ], - minimize: [ - "The only small thing is that errors can sometimes slip through if you're not careful...", - "It works well, though it's easy to forget error handling in some code paths...", - "This is good, there's just a way to make errors impossible to ignore...", - "Nothing wrong with try/catch, but the type system doesn't help track errors...", - ], - better: [ - "ReScript's Result type makes errors explicit - they're part of your function signature!", - "With Result, you can't ignore errors - the compiler won't let you!", - "ReScript treats errors as values, making them composable and type-safe.", - "In ReScript, error handling is enforced by the type system - no forgotten catch blocks!", - ], - safety: [ - "The compiler ensures you handle both Ok and Error cases - no silent failures!", - "Result types make error handling explicit and impossible to ignore.", - "Type-safe error handling means errors are documented in function signatures.", - "ReScript eliminates the possibility of unhandled exceptions at compile time.", - ], - }, - ), - ( - "array-operations", - { - celebrate: [ - "Perfect! You're using array methods like map and filter - functional programming at its best!", - "Great use of array operations! You understand how to work with collections.", - "Nice! These array methods are exactly the right tool for transforming data.", - "You're writing functional code with map/filter/reduce - that's excellent!", - ], - minimize: [ - "The only thing is that these methods can sometimes be chained in ways that are hard to read...", - "It works perfectly, there's just a more readable way to express these transformations...", - "This is fine, though deeply nested callbacks can get a bit hard to follow...", - "Nothing wrong here, but there's a syntax that makes data flow even clearer...", - ], - better: [ - "ReScript's pipe operator lets you write these transformations in reading order!", - "With ReScript's ->, data transformations read like a sentence from left to right!", - "ReScript makes array operations more readable with the pipe operator and better inference.", - "In ReScript, you can chain operations naturally with -> and the compiler handles the rest!", - ], - safety: [ - "Type inference ensures your array transformations are always type-safe.", - "The compiler catches type mismatches in your data pipelines before runtime.", - "ReScript's array operations are fully typed - no runtime type errors!", - "Type-safe array operations prevent common mistakes like accessing undefined indices.", - ], - }, - ), - ( - "conditionals", - { - celebrate: [ - "Good use of conditionals! You're handling different cases in your logic.", - "Nice! You're thinking through the different paths your code can take.", - "Great! You understand that code needs to branch based on different conditions.", - "You're handling multiple cases - that's thorough programming!", - ], - minimize: [ - "The only thing is that it's possible to miss a case with if/else chains...", - "It works, though the compiler can't help you know if you've covered all cases...", - "This is solid, there's just a way to make sure you never forget a case...", - "Nothing wrong with if/else, but there's a more exhaustive way to handle cases...", - ], - better: [ - "ReScript's pattern matching ensures you handle ALL cases - the compiler checks!", - "With switch expressions, ReScript won't compile until all cases are covered!", - "ReScript's pattern matching is exhaustive - impossible to forget a case!", - "In ReScript, switch is an expression with compile-time exhaustiveness checking!", - ], - safety: [ - "Exhaustive pattern matching means every case is handled - no forgotten branches!", - "The compiler proves you've covered all possible values - no runtime surprises!", - "Pattern matching with exhaustiveness checking eliminates an entire class of bugs.", - "ReScript's switch won't compile until you've handled every possible case!", - ], - }, - ), - ( - "functional", - { - celebrate: [ - "Excellent! You're using pure functions - you understand functional programming!", - "Great! These functions are predictable and testable - that's good design!", - "Nice functional code! You're avoiding side effects and mutations.", - "You're writing pure functions - that's a sign of a skilled developer!", - ], - minimize: [ - "The only thing is that JavaScript doesn't enforce purity - it's easy to slip...", - "It works great, though mutations can accidentally creep in...", - "This is solid functional code, there's just a way to make purity automatic...", - "Nothing wrong here, but JavaScript allows mutations that can break purity...", - ], - better: [ - "ReScript makes immutability the default - you have to explicitly opt into mutation!", - "With ReScript, functional programming is natural and enforced by the type system!", - "ReScript's immutable-by-default data structures prevent accidental side effects!", - "In ReScript, pure functional programming is the easy path, not the hard one!", - ], - safety: [ - "Immutability by default means no accidental mutations - safer concurrent code!", - "The type system ensures your functions are actually pure - no hidden side effects!", - "ReScript's functional features prevent entire categories of bugs related to shared state.", - "Compiler-enforced immutability makes your code more predictable and testable!", - ], - }, - ), - ( - "default", - { - celebrate: [ - "You're already writing good JavaScript - this pattern works!", - "Nice! You understand this programming concept well.", - "Great code! You're using modern JavaScript features effectively.", - "You've got the right idea - this is solid programming!", - ], - minimize: [ - "There's just a small opportunity to make this even better...", - "It works perfectly, there's just a more type-safe way to express this...", - "Nothing wrong with this approach, but ReScript has a nice enhancement...", - "This is fine, there's just a way to get more compiler help...", - ], - better: [ - "ReScript brings type safety and better error messages to this pattern!", - "With ReScript, you get the same functionality with stronger guarantees!", - "ReScript makes this pattern more explicit and catches errors earlier!", - "In ReScript, this becomes even more clear and type-safe!", - ], - safety: [ - "The compiler provides stronger guarantees about correctness!", - "Type safety catches potential issues before they become runtime errors!", - "ReScript's type system helps prevent common mistakes in this pattern!", - "Compile-time checking means more confidence in your code!", - ], - }, - ), -]) - -// Get template for a category -let getTemplateForCategory = (category: patternCategory): narrativeTemplate => { - let categoryStr = categoryToString(category) - switch narrativeTemplates->Dict.get(categoryStr) { - | Some(template) => template - | None => - switch narrativeTemplates->Dict.get("default") { - | Some(t) => t - | None => { - celebrate: [], - minimize: [], - better: [], - safety: [], - } - } - } -} - -// Generate narrative based on pattern category -let generateCategoryNarrative = (category: patternCategory, patternName: string): narrative => { - let template = getTemplateForCategory(category) - - { - celebrate: pickRandom(template.celebrate), - minimize: pickRandom(template.minimize), - better: pickRandom(template.better), - safety: pickRandom(template.safety), - example: `See how the ${patternName} pattern works in ReScript!`, - } -} - -// Generate an encouraging narrative for a pattern -let generateNarrative = (pattern: pattern): narrative => { - pattern.narrative -} - -// Format narrative for display -let formatNarrative = (narrative: narrative, format: string): string => { - switch format { - | "plain" => - `${narrative.celebrate} - -${narrative.minimize} - -${narrative.better} - -🛡️ Safety: ${narrative.safety} - -💡 ${narrative.example}` - - | "html" => - `
      -

      ✨ ${narrative.celebrate}

      -

      💭 ${narrative.minimize}

      -

      🚀 ${narrative.better}

      -

      🛡️ Safety: ${narrative.safety}

      -

      💡 ${narrative.example}

      -
      ` - - | _ => - // markdown (default) - `✨ **You were close!** ${narrative.celebrate} - -💭 ${narrative.minimize} - -🚀 **Even better:** ${narrative.better} - -🛡️ **Safety:** ${narrative.safety} - -💡 *${narrative.example}*` - } -} - -// Generate a success message for completed transformations -let generateSuccessMessage = (patternName: string, _difficulty: string): string => { - let messages = [ - `Awesome! You've mastered the ${patternName} pattern! 🎉`, - `Great job! ${patternName} is now part of your ReScript toolkit! ✨`, - `Excellent work! You understand ${patternName} in ReScript! 🚀`, - `Well done! ${patternName} - another pattern conquered! 💪`, - ] - pickRandom(messages) -} - -// Generate an encouraging hint for learning -let generateHint = (pattern: pattern): string => { - let hints = [ - `💡 Tip: ${pattern.bestPractices->Array.get(0)->Option.getOr("Practice makes perfect!")}`, - `🎯 Remember: ${pattern.learningObjectives->Array.get(0)->Option.getOr("Focus on understanding the concept.")}`, - `⚠️ Watch out: ${pattern.commonMistakes->Array.get(0)->Option.getOr("Take your time and read the examples.")}`, - `✨ Pro tip: Try transforming your own code to really internalize this pattern!`, - ] - pickRandom(hints) -} diff --git a/src/Output.res b/src/Output.res deleted file mode 100644 index 6da4fea..0000000 --- a/src/Output.res +++ /dev/null @@ -1,223 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Output: Format analysis results for terminal display -// Supports RAW, FOLDED, and GLYPHED view layers -// Target-aware: honours the requested target language, falls back to the -// first target the pattern actually supplies if the requested one is -// missing (e.g. AffineScript requested but pattern only ships ReScript). - -open Types - -// ANSI colour codes for terminal output -let bold = s => `\x1b[1m${s}\x1b[0m` -let green = s => `\x1b[32m${s}\x1b[0m` -let yellow = s => `\x1b[33m${s}\x1b[0m` -let cyan = s => `\x1b[36m${s}\x1b[0m` -let dim = s => `\x1b[2m${s}\x1b[0m` -let magenta = s => `\x1b[35m${s}\x1b[0m` - -// Format a single pattern match for RAW view -let formatMatchRaw = (m: patternMatch, format: string, target: targetLang): string => { - let glyphBar = m.pattern.glyphs->Array.join(" ") - let diffStr = difficultyToString(m.pattern.difficulty) - let narrative = Narrative.formatNarrative(m.pattern.narrative, format) - let effectiveTarget = patternEffectiveTarget(m.pattern, target) - let targetCode = patternCodeFor(m.pattern, target) - let targetLabel = targetLangLabel(effectiveTarget) - let targetTag = targetLangSyntaxTag(effectiveTarget) - let fallbackNote = - effectiveTarget == target - ? "" - : ` ${dim(`(fallback — pattern not yet ported to ${targetLangLabel(target)})`)}` - - switch format { - | "markdown" => - `### ${glyphBar} ${m.pattern.name}\n\n` ++ - `**Difficulty:** ${diffStr} | **Confidence:** ${Float.toFixed(m.pattern.confidence *. 100.0, ~digits=0)}%\n\n` ++ - `**JavaScript:**\n\`\`\`javascript\n${m.pattern.jsExample}\n\`\`\`\n\n` ++ - `**${targetLabel}:**\n\`\`\`${targetTag}\n${targetCode}\n\`\`\`\n\n` ++ - narrative ++ "\n\n---\n" - - | "html" => - `
      \n` ++ - `

      ${glyphBar} ${m.pattern.name}

      \n` ++ - `

      Difficulty: ${diffStr} | Confidence: ${Float.toFixed(m.pattern.confidence *. 100.0, ~digits=0)}%

      \n` ++ - `
      ${m.pattern.jsExample}
      \n` ++ - `
      ${targetCode}
      \n` ++ - narrative ++ - `\n
      \n` - - | _ => - // plain text (terminal) - bold(`${glyphBar} ${m.pattern.name}`) ++ "\n" ++ - dim(`Difficulty: ${diffStr} | Confidence: ${Float.toFixed(m.pattern.confidence *. 100.0, ~digits=0)}%`) ++ "\n\n" ++ - cyan("JavaScript:") ++ "\n" ++ m.pattern.jsExample ++ "\n\n" ++ - green(`${targetLabel}:`) ++ fallbackNote ++ "\n" ++ targetCode ++ "\n\n" ++ - narrative ++ "\n\n" ++ - dim("---") ++ "\n" - } -} - -// Format analysis results in RAW view (each match shown individually) -let formatRaw = (result: analysisResult, format: string, target: targetLang): string => { - if result.matches->Array.length === 0 { - "No patterns detected. Try pasting some JavaScript code!\n" - } else { - let header = switch format { - | "markdown" => "# Nextgen Languages Evangeliser - Analysis Results\n\n" - | "html" => "

      Nextgen Languages Evangeliser - Analysis Results

      \n" - | _ => bold("Nextgen Languages Evangeliser - Analysis Results") ++ "\n\n" - } - - let summary = Analyser.summarise(result) - let matches = - result.matches - ->Array.map(m => formatMatchRaw(m, format, target)) - ->Array.join("\n") - - header ++ summary ++ "\n\n" ++ matches - } -} - -// Format analysis results in FOLDED view (grouped by category) -let formatFolded = (result: analysisResult, format: string, target: targetLang): string => { - if result.matches->Array.length === 0 { - "No patterns detected. Try pasting some JavaScript code!\n" - } else { - let header = switch format { - | "markdown" => "# Nextgen Languages Evangeliser - Analysis Results (Grouped)\n\n" - | "html" => "

      Nextgen Languages Evangeliser - Analysis Results (Grouped)

      \n" - | _ => bold("Nextgen Languages Evangeliser - Analysis Results (Grouped)") ++ "\n\n" - } - - let summary = Analyser.summarise(result) - - // Group matches by category - let groups: Dict.t> = Dict.make() - result.matches->Array.forEach(m => { - let cat = categoryToString(m.pattern.category) - let existing = groups->Dict.get(cat)->Option.getOr([]) - groups->Dict.set(cat, existing->Array.concat([m])) - }) - - let body = - groups - ->Dict.toArray - ->Array.map(((cat, matches)) => { - let catHeader = switch format { - | "markdown" => `## ${cat} (${Int.toString(matches->Array.length)} matches)\n\n` - | "html" => `

      ${cat} (${Int.toString(matches->Array.length)} matches)

      \n` - | _ => yellow(`[${cat}]`) ++ ` ${dim(`(${Int.toString(matches->Array.length)} matches)`)}` ++ "\n\n" - } - let matchStr = - matches - ->Array.map(m => formatMatchRaw(m, format, target)) - ->Array.join("\n") - catHeader ++ matchStr - }) - ->Array.join("\n") - - header ++ summary ++ "\n\n" ++ body - } -} - -// Format analysis results in GLYPHED view (glyph-annotated summary) -let formatGlyphed = (result: analysisResult, format: string, _target: targetLang): string => { - if result.matches->Array.length === 0 { - "No patterns detected. Try pasting some JavaScript code!\n" - } else { - let header = switch format { - | "markdown" => "# Nextgen Languages Evangeliser - Glyph Overview\n\n" - | "html" => "

      Nextgen Languages Evangeliser - Glyph Overview

      \n" - | _ => bold("Nextgen Languages Evangeliser - Glyph Overview") ++ "\n\n" - } - - let uniquePatterns = Scanner.uniquePatterns(result.matches) - - let glyphMap = - uniquePatterns - ->Array.map(p => { - let glyphs = p.glyphs->Array.join(" ") - switch format { - | "markdown" => `- ${glyphs} **${p.name}**: ${p.narrative.better}\n` - | "html" => `
    • ${glyphs} ${p.name}: ${p.narrative.better}
    • \n` - | _ => `${glyphs} ${magenta(p.name)}: ${p.narrative.better}\n` - } - }) - ->Array.join("") - - let legend = switch format { - | "plain" => "\n" ++ dim("Use 'evangeliser legend' to see the full glyph legend.") ++ "\n" - | _ => "" - } - - header ++ Analyser.summarise(result) ++ "\n\n" ++ glyphMap ++ legend - } -} - -// Format results using the specified view layer and target language. -let format = (result: analysisResult, view: viewLayer, outputFormat: string, target: targetLang): string => { - switch view { - | RAW => formatRaw(result, outputFormat, target) - | FOLDED => formatFolded(result, outputFormat, target) - | GLYPHED => formatGlyphed(result, outputFormat, target) - | WYSIWYG => formatRaw(result, "html", target) // WYSIWYG falls back to HTML for now - } -} - -// Format the pattern list for the `patterns` command. -// Annotates each pattern with the set of targets it currently supports, -// so users can see at a glance which patterns have been ported to -// AffineScript and which are still ReScript-only. -let formatPatternList = (format: string): string => { - let stats = Patterns.getPatternStats() - let header = switch format { - | "markdown" => - `# Pattern Library (${Int.toString(stats.total)} patterns)\n\n` - | _ => - bold(`Pattern Library (${Int.toString(stats.total)} patterns)`) ++ "\n\n" - } - - let categories = [ - NullSafety, Async, ErrorHandling, ArrayOperations, Conditionals, - Destructuring, Defaults, Functional, Templates, ArrowFunctions, - Variants, Modules, TypeSafety, Immutability, PatternMatching, - PipeOperator, OopToFp, ClassesToRecords, InheritanceToComposition, - StateMachines, DataModeling, - ] - - let body = - categories - ->Array.map(cat => { - let patterns = Patterns.getPatternsByCategory(cat) - let catStr = categoryToString(cat) - let glyphs = Glyphs.getGlyphsForPattern(cat)->Array.join(" ") - let catHeader = switch format { - | "markdown" => - `## ${glyphs} ${catStr} (${Int.toString(patterns->Array.length)})\n\n` - | _ => - yellow(`${glyphs} ${catStr}`) ++ dim(` (${Int.toString(patterns->Array.length)})`) ++ "\n" - } - - let patternList = - patterns - ->Array.map(p => { - let diffStr = difficultyToString(p.difficulty) - let targetTags = - p.targets - ->Array.map(t => targetLangLabel(t.language)) - ->Array.join(", ") - switch format { - | "markdown" => - `- **${p.name}** (${diffStr}) [targets: ${targetTags}] - ${p.narrative.celebrate}\n` - | _ => - ` ${green(p.name)} ${dim(`[${diffStr}]`)} ${dim(`[${targetTags}]`)} ${p.narrative.celebrate}\n` - } - }) - ->Array.join("") - - catHeader ++ patternList ++ "\n" - }) - ->Array.join("") - - header ++ body -} diff --git a/src/Patterns.res b/src/Patterns.res deleted file mode 100644 index 3caa6b7..0000000 --- a/src/Patterns.res +++ /dev/null @@ -1,1803 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Pattern Library for Nextgen Languages Evangeliser -// Multi-target transformation patterns from JavaScript/TypeScript to AffineScript (flagship) / ReScript (legacy) - -open Types - -// Helper to create a pattern with glyphs. -// -// Phase 1b note: the `~rescriptExample` parameter is preserved so all 52 -// existing call-sites compile unchanged. It is wrapped internally into -// the new multi-target `targets` field as a single ReScript entry. -// Patterns authored in Phase 2 onward will call `makePatternMultiTarget` -// (below) to supply per-target examples including the flagship AffineScript. -let makePattern = ( - ~id, - ~name, - ~category, - ~difficulty, - ~jsPattern, - ~confidence, - ~jsExample, - ~rescriptExample, - ~narrative, - ~tags, - ~relatedPatterns, - ~learningObjectives, - ~commonMistakes, - ~bestPractices, -): pattern => { - { - id, - name, - category, - difficulty, - jsPattern, - confidence, - jsExample, - targets: [{language: ReScript, code: rescriptExample}], - narrative, - glyphs: Glyphs.getGlyphsForPattern(category), - tags, - relatedPatterns, - learningObjectives, - commonMistakes, - bestPractices, - } -} - -// Multi-target pattern constructor — use this for new (Phase 2+) patterns -// that supply AffineScript (flagship) alongside ReScript (or any other -// target language). -let makePatternMultiTarget = ( - ~id, - ~name, - ~category, - ~difficulty, - ~jsPattern, - ~confidence, - ~jsExample, - ~targets, - ~narrative, - ~tags, - ~relatedPatterns, - ~learningObjectives, - ~commonMistakes, - ~bestPractices, -): pattern => { - { - id, - name, - category, - difficulty, - jsPattern, - confidence, - jsExample, - targets, - narrative, - glyphs: Glyphs.getGlyphsForPattern(category), - tags, - relatedPatterns, - learningObjectives, - commonMistakes, - bestPractices, - } -} - -// NULL SAFETY PATTERNS -let nullCheckBasic = makePattern( - ~id="null-check-basic", - ~name="Basic Null Check", - ~category=NullSafety, - ~difficulty=Beginner, - ~jsPattern=`if\\s*\\(\\s*(\\w+)\\s*!==?\\s*null\\s*&&\\s*\\1\\s*!==?\\s*undefined\\s*\\)`, - ~confidence=0.9, - ~jsExample=`if (user !== null && user !== undefined) { - console.log(user.name); -}`, - ~rescriptExample=`switch user { -| Some(u) => Console.log(u.name) -| None => () -}`, - ~narrative={ - celebrate: "You're checking for null and undefined - that's great defensive programming!", - minimize: "The only small thing is that it's easy to forget one of these checks somewhere...", - better: "ReScript's Option type makes null safety automatic - you literally can't forget a check!", - safety: "The compiler won't let your code compile until you've handled both Some and None cases.", - example: "Option types eliminate an entire class of 'Cannot read property of undefined' errors!", - }, - ~tags=["null", "undefined", "option", "safety"], - ~relatedPatterns=["null-check-ternary", "optional-chaining"], - ~learningObjectives=["Understand Option<'a> type", "Pattern matching for null safety", "Exhaustiveness checking"], - ~commonMistakes=["Forgetting to handle None case", "Using null instead of None"], - ~bestPractices=[ - "Always use Option for nullable values", - "Use pattern matching to handle all cases", - "Prefer Option.map over explicit pattern matching", - ], -) - -let nullCheckTernary = makePattern( - ~id="null-check-ternary", - ~name="Null Check Ternary", - ~category=NullSafety, - ~difficulty=Beginner, - ~jsPattern=`(\\w+)\\s*\\?\\s*(\\w+)\\.\\w+\\s*:\\s*['\"]\\w+['\"]|null|undefined`, - ~confidence=0.85, - ~jsExample=`const name = user ? user.name : 'Guest';`, - ~rescriptExample=`let name = user->Option.mapOr("Guest", u => u.name)`, - ~narrative={ - celebrate: "Nice! You're using a ternary to handle the null case - that's concise!", - minimize: "It works great, though it can get verbose with nested checks...", - better: "ReScript's Option.mapOr does this in one line with type safety!", - safety: "The compiler ensures the default value matches the expected type.", - example: "Option.mapOr handles the None case automatically!", - }, - ~tags=["ternary", "default", "option"], - ~relatedPatterns=["null-check-basic", "default-params"], - ~learningObjectives=["Option.mapOr function", "Type-safe defaults"], - ~commonMistakes=["Wrong default type"], - ~bestPractices=["Use Option.mapOr for simple defaults"], -) - -let optionalChaining = makePattern( - ~id="optional-chaining", - ~name="Optional Chaining", - ~category=NullSafety, - ~difficulty=Intermediate, - ~jsPattern=`\\w+\\?\\.[\\w.?]+`, - ~confidence=0.95, - ~jsExample=`const city = user?.address?.city;`, - ~rescriptExample=`let city = user->Option.flatMap(u => u.address) - ->Option.map(a => a.city)`, - ~narrative={ - celebrate: "Excellent! You're using optional chaining - you know modern JavaScript!", - minimize: "It's really nice, though the result can still be undefined...", - better: "ReScript's Option.flatMap chains safely and the type tells you it's optional!", - safety: "The type system tracks the optionality through the entire chain.", - example: "Chain Option operations with flatMap and map!", - }, - ~tags=["optional-chaining", "nested", "option"], - ~relatedPatterns=["null-check-basic"], - ~learningObjectives=["Option.flatMap", "Chaining optional values"], - ~commonMistakes=["Using map instead of flatMap"], - ~bestPractices=["Use flatMap for nested Options"], -) - -// ASYNC PATTERNS -let asyncAwaitBasic = makePattern( - ~id="async-await-basic", - ~name="Async/Await", - ~category=Async, - ~difficulty=Intermediate, - ~jsPattern=`async\\s+function|\\basync\\s*\\(`, - ~confidence=0.95, - ~jsExample=`async function fetchUser(id) { - const response = await fetch(\`/api/users/\${id}\`); - return await response.json(); -}`, - ~rescriptExample=`let fetchUser = async (id) => { - let response = await fetch(\`/api/users/\${id}\`) - await response->Response.json -}`, - ~narrative={ - celebrate: "Excellent! You're using async/await - much better than callbacks!", - minimize: "It's great, though error handling can get scattered...", - better: "ReScript has async/await with type-safe Promises!", - safety: "Promise types are checked at compile time.", - example: "Type-safe async operations prevent common Promise mistakes!", - }, - ~tags=["async", "await", "promise"], - ~relatedPatterns=["promise-then", "try-catch-async"], - ~learningObjectives=["Async/await in ReScript", "Promise types"], - ~commonMistakes=["Forgetting await", "Missing error handling"], - ~bestPractices=["Always handle Promise rejections"], -) - -let promiseThen = makePattern( - ~id="promise-then", - ~name="Promise Then Chain", - ~category=Async, - ~difficulty=Beginner, - ~jsPattern=`\\.then\\s*\\([^)]+\\)`, - ~confidence=0.9, - ~jsExample=`fetch('/api/data') - .then(res => res.json()) - .then(data => console.log(data));`, - ~rescriptExample=`fetch("/api/data") -->Promise.then(res => res->Response.json) -->Promise.then(data => { - Console.log(data) - Promise.resolve() -})`, - ~narrative={ - celebrate: "Good use of Promise chains - you understand asynchronous flow!", - minimize: "It works well, though deeply nested thens can be hard to read...", - better: "ReScript's Promise.then with pipe operator makes chains more readable!", - safety: "Promise types are tracked through the chain.", - example: "Type-safe Promise chains with pipe operator!", - }, - ~tags=["promise", "then", "chain"], - ~relatedPatterns=["async-await-basic"], - ~learningObjectives=["Promise.then", "Promise chaining"], - ~commonMistakes=["Forgetting to return Promise"], - ~bestPractices=["Consider async/await for readability"], -) - -// ERROR HANDLING PATTERNS -let tryCatchBasic = makePattern( - ~id="try-catch-basic", - ~name="Basic Try/Catch", - ~category=ErrorHandling, - ~difficulty=Beginner, - ~jsPattern=`try\\s*\\{[^}]+\\}\\s*catch\\s*\\([^)]*\\)\\s*\\{`, - ~confidence=0.9, - ~jsExample=`try { - const result = riskyOperation(); - return result; -} catch (error) { - console.error(error); - return null; -}`, - ~rescriptExample=`switch riskyOperation() { -| result => Ok(result) -| exception Exn.Error(e) => Error(e) -}`, - ~narrative={ - celebrate: "Excellent error handling with try/catch - you're thinking defensively!", - minimize: "It works great, though errors aren't part of the function signature...", - better: "ReScript's Result type makes errors explicit in the type!", - safety: "Callers must handle both Ok and Error cases.", - example: "Result makes error handling impossible to ignore!", - }, - ~tags=["try-catch", "error", "result"], - ~relatedPatterns=["error-result-type"], - ~learningObjectives=["Result type", "Explicit error handling"], - ~commonMistakes=["Ignoring error cases"], - ~bestPractices=["Use Result for expected errors"], -) - -let errorResultType = makePattern( - ~id="error-result-type", - ~name="Result Type Pattern", - ~category=ErrorHandling, - ~difficulty=Intermediate, - ~jsPattern=`return\\s*\\{\\s*(?:success|ok|error)\\s*:`, - ~confidence=0.7, - ~jsExample=`function divide(a, b) { - if (b === 0) { - return { error: 'Division by zero' }; - } - return { success: a / b }; -}`, - ~rescriptExample=`let divide = (a, b) => { - if b == 0 { - Error("Division by zero") - } else { - Ok(a / b) - } -}`, - ~narrative={ - celebrate: "Smart! You're using an object to represent success/error - that's Result-like!", - minimize: "It works, though the shape isn't enforced by types...", - better: "ReScript's built-in Result type is exactly this, but type-safe!", - safety: "The compiler ensures you handle both Ok and Error.", - example: "Result<'a, 'error> is a first-class type!", - }, - ~tags=["result", "error", "success"], - ~relatedPatterns=["try-catch-basic"], - ~learningObjectives=["Result type usage", "Error as value"], - ~commonMistakes=["Inconsistent error shapes"], - ~bestPractices=["Always use Result for fallible operations"], -) - -// ARRAY OPERATION PATTERNS -let arrayMap = makePattern( - ~id="array-map", - ~name="Array.map", - ~category=ArrayOperations, - ~difficulty=Beginner, - ~jsPattern=`\\.map\\s*\\(\\s*(?:\\w+|\\([^)]*\\))\\s*=>`, - ~confidence=0.95, - ~jsExample=`const doubled = numbers.map(n => n * 2);`, - ~rescriptExample=`let doubled = numbers->Array.map(n => n * 2)`, - ~narrative={ - celebrate: "Perfect! Array.map is functional programming at its best!", - minimize: "Nothing wrong here, ReScript just adds the pipe operator...", - better: "ReScript's pipe operator makes data flow left-to-right!", - safety: "Type inference ensures the transformation is type-safe.", - example: "The -> operator makes transformations read like sentences!", - }, - ~tags=["array", "map", "transform"], - ~relatedPatterns=["array-filter", "array-reduce", "pipe-operator"], - ~learningObjectives=["Array.map", "Pipe operator"], - ~commonMistakes=["Side effects in map"], - ~bestPractices=["Keep map pure, use pipe for readability"], -) - -let arrayFilter = makePattern( - ~id="array-filter", - ~name="Array.filter", - ~category=ArrayOperations, - ~difficulty=Beginner, - ~jsPattern=`\\.filter\\s*\\(\\s*(?:\\w+|\\([^)]*\\))\\s*=>`, - ~confidence=0.95, - ~jsExample=`const evens = numbers.filter(n => n % 2 === 0);`, - ~rescriptExample=`let evens = numbers->Array.filter(n => mod(n, 2) == 0)`, - ~narrative={ - celebrate: "Great use of filter - functional programming done right!", - minimize: "Works perfectly, ReScript adds type-safe predicates...", - better: "ReScript's type system ensures your predicate always returns bool!", - safety: "The compiler checks that filter predicates return boolean.", - example: "Type-safe filtering with pipe operator!", - }, - ~tags=["array", "filter", "predicate"], - ~relatedPatterns=["array-map"], - ~learningObjectives=["Array.filter", "Boolean predicates"], - ~commonMistakes=["Predicate returning non-boolean"], - ~bestPractices=["Keep predicates pure and simple"], -) - -let arrayReduce = makePattern( - ~id="array-reduce", - ~name="Array.reduce", - ~category=ArrayOperations, - ~difficulty=Intermediate, - ~jsPattern=`\\.reduce\\s*\\(\\s*\\([^)]*\\)\\s*=>`, - ~confidence=0.9, - ~jsExample=`const sum = numbers.reduce((acc, n) => acc + n, 0);`, - ~rescriptExample=`let sum = numbers->Array.reduce(0, (acc, n) => acc + n)`, - ~narrative={ - celebrate: "Excellent! Array.reduce is a powerful functional pattern!", - minimize: "It's great, though the parameter order can be confusing...", - better: "ReScript puts the initial value first - more intuitive!", - safety: "Type inference ensures accumulator and result types match.", - example: "Type-safe reduce with clear parameter order!", - }, - ~tags=["array", "reduce", "fold"], - ~relatedPatterns=["array-map"], - ~learningObjectives=["Array.reduce", "Accumulator patterns"], - ~commonMistakes=["Wrong initial value type"], - ~bestPractices=["Consider specialized functions like sum, join"], -) - -let arrayFind = makePattern( - ~id="array-find", - ~name="Array.find", - ~category=ArrayOperations, - ~difficulty=Beginner, - ~jsPattern=`\\.find\\s*\\(\\s*(?:\\w+|\\([^)]*\\))\\s*=>`, - ~confidence=0.9, - ~jsExample=`const user = users.find(u => u.id === userId);`, - ~rescriptExample=`let user = users->Array.find(u => u.id == userId)`, - ~narrative={ - celebrate: "Good! You're using find to search arrays efficiently!", - minimize: "It works, though the result can be undefined...", - better: "ReScript's Array.find returns Option to handle 'not found' safely!", - safety: "The Option return type forces you to handle the not-found case.", - example: "Option<'a> makes missing values explicit!", - }, - ~tags=["array", "find", "search"], - ~relatedPatterns=["null-check-basic"], - ~learningObjectives=["Array.find with Option"], - ~commonMistakes=["Not checking for undefined"], - ~bestPractices=["Pattern match on find result"], -) - -// CONDITIONAL PATTERNS -let switchStatement = makePattern( - ~id="switch-statement", - ~name="Switch Statement", - ~category=Conditionals, - ~difficulty=Beginner, - ~jsPattern=`switch\\s*\\([^)]+\\)\\s*\\{`, - ~confidence=0.9, - ~jsExample=`switch (status) { - case 'loading': return 'Loading...'; - case 'success': return data; - case 'error': return 'Error!'; - default: return null; -}`, - ~rescriptExample=`switch status { -| Loading => "Loading..." -| Success(data) => data -| Error(_) => "Error!" -}`, - ~narrative={ - celebrate: "Good use of switch! You're handling multiple cases systematically.", - minimize: "It works, though the 'default' case can hide missing cases...", - better: "ReScript's pattern matching ensures you handle ALL cases - the compiler checks!", - safety: "The compiler ensures every variant is handled.", - example: "Exhaustive pattern matching catches missing cases at compile time!", - }, - ~tags=["switch", "conditionals", "pattern-matching"], - ~relatedPatterns=["if-else-chain"], - ~learningObjectives=["Pattern matching basics", "Exhaustiveness"], - ~commonMistakes=["Relying on default for unhandled cases"], - ~bestPractices=["Let the compiler ensure completeness"], -) - -let ifElseChain = makePattern( - ~id="if-else-chain", - ~name="If/Else Chain", - ~category=Conditionals, - ~difficulty=Beginner, - ~jsPattern=`if\\s*\\([^)]+\\)\\s*\\{[^}]*\\}\\s*else\\s*if`, - ~confidence=0.85, - ~jsExample=`if (x > 10) { - return 'large'; -} else if (x > 5) { - return 'medium'; -} else { - return 'small'; -}`, - ~rescriptExample=`if x > 10 { - "large" -} else if x > 5 { - "medium" -} else { - "small" -}`, - ~narrative={ - celebrate: "You're handling multiple conditions - that's thorough logic!", - minimize: "It works, though ReScript can make this even cleaner...", - better: "ReScript if/else is an expression that returns a value!", - safety: "All branches must return the same type.", - example: "Expression-based control flow is safer and cleaner!", - }, - ~tags=["if", "else", "conditionals"], - ~relatedPatterns=["switch-statement"], - ~learningObjectives=["If as expression", "Type consistency across branches"], - ~commonMistakes=["Inconsistent return types"], - ~bestPractices=["Consider pattern matching for complex conditions"], -) - -// FUNCTIONAL PATTERNS -let pureFunction = makePattern( - ~id="pure-function", - ~name="Pure Function", - ~category=Functional, - ~difficulty=Beginner, - ~jsPattern=`const\\s+\\w+\\s*=\\s*\\([^)]*\\)\\s*=>\\s*[^{]`, - ~confidence=0.6, - ~jsExample=`const add = (a, b) => a + b;`, - ~rescriptExample=`let add = (a, b) => a + b`, - ~narrative={ - celebrate: "Excellent! Pure functions are predictable and testable!", - minimize: "JavaScript doesn't enforce purity, but you're doing it right...", - better: "ReScript's immutability by default makes purity natural!", - safety: "No hidden state mutations possible.", - example: "Pure functions are the foundation of reliable code!", - }, - ~tags=["pure", "function", "immutable"], - ~relatedPatterns=["array-map"], - ~learningObjectives=["Pure functions", "Immutability"], - ~commonMistakes=["Accidental mutations"], - ~bestPractices=["Keep functions pure when possible"], -) - -let higherOrderFunction = makePattern( - ~id="higher-order-function", - ~name="Higher Order Function", - ~category=Functional, - ~difficulty=Intermediate, - ~jsPattern=`\\([^)]*=>\\s*\\([^)]*\\)\\s*=>`, - ~confidence=0.8, - ~jsExample=`const multiply = factor => num => num * factor; -const double = multiply(2);`, - ~rescriptExample=`let multiply = factor => num => num * factor -let double = multiply(2)`, - ~narrative={ - celebrate: "Great! Higher-order functions show advanced functional skills!", - minimize: "Works perfectly, ReScript's syntax is nearly identical...", - better: "ReScript's currying makes this pattern natural!", - safety: "Type inference tracks through all the layers.", - example: "Curried functions compose beautifully!", - }, - ~tags=["higher-order", "currying", "functional"], - ~relatedPatterns=["pure-function"], - ~learningObjectives=["Currying", "Function composition"], - ~commonMistakes=["Losing track of closure variables"], - ~bestPractices=["Use currying for reusable transformations"], -) - -// DESTRUCTURING PATTERNS -let objectDestructuring = makePattern( - ~id="object-destructuring", - ~name="Object Destructuring", - ~category=Destructuring, - ~difficulty=Beginner, - ~jsPattern=`const\\s*\\{[^}]+\\}\\s*=`, - ~confidence=0.9, - ~jsExample=`const { name, age, email } = user;`, - ~rescriptExample=`let {name, age, email} = user`, - ~narrative={ - celebrate: "Nice destructuring! You're extracting exactly what you need.", - minimize: "It's clean, though missing properties silently become undefined...", - better: "ReScript destructuring is type-checked - missing fields are compile errors!", - safety: "The compiler guarantees every destructured field exists on the record.", - example: "Type-safe destructuring means no 'undefined' surprises!", - }, - ~tags=["destructuring", "object", "record"], - ~relatedPatterns=["array-destructuring"], - ~learningObjectives=["Record destructuring", "Type-safe field access"], - ~commonMistakes=["Destructuring non-existent fields"], - ~bestPractices=["Destructure only what you need"], -) - -let arrayDestructuring = makePattern( - ~id="array-destructuring", - ~name="Array Destructuring", - ~category=Destructuring, - ~difficulty=Beginner, - ~jsPattern=`const\\s*\\[[^\\]]+\\]\\s*=`, - ~confidence=0.85, - ~jsExample=`const [first, second, ...rest] = items;`, - ~rescriptExample=`switch items { -| [first, second, ...rest] => // use values -| _ => // handle other cases -}`, - ~narrative={ - celebrate: "Great! Array destructuring makes your code expressive!", - minimize: "It works, though arrays might not have enough elements...", - better: "ReScript uses pattern matching for array destructuring with safety!", - safety: "Pattern matching forces you to handle the case where the array is too short.", - example: "Safe array destructuring through exhaustive pattern matching!", - }, - ~tags=["destructuring", "array", "pattern-matching"], - ~relatedPatterns=["object-destructuring"], - ~learningObjectives=["Array pattern matching", "Rest patterns"], - ~commonMistakes=["Assuming array has enough elements"], - ~bestPractices=["Always handle the empty/short case"], -) - -// DEFAULT PARAMETER PATTERNS -let defaultParams = makePattern( - ~id="default-params", - ~name="Default Parameters", - ~category=Defaults, - ~difficulty=Beginner, - ~jsPattern=`function\\s+\\w+\\s*\\([^)]*=\\s*[^,)]+`, - ~confidence=0.85, - ~jsExample=`function greet(name = 'World', greeting = 'Hello') { - return \`\${greeting}, \${name}!\`; -}`, - ~rescriptExample=`let greet = (~name="World", ~greeting="Hello") => { - \`\${greeting}, \${name}!\` -}`, - ~narrative={ - celebrate: "Smart use of default parameters - you're making your API flexible!", - minimize: "Works well, though optional and default can get confusing...", - better: "ReScript's labeled arguments with defaults are type-safe and self-documenting!", - safety: "The compiler ensures default values match the parameter type.", - example: "Labeled arguments make function calls self-documenting!", - }, - ~tags=["defaults", "parameters", "labeled"], - ~relatedPatterns=["null-check-ternary"], - ~learningObjectives=["Labeled arguments", "Default values"], - ~commonMistakes=["Wrong default type"], - ~bestPractices=["Use labeled arguments for clarity"], -) - -let nullishCoalescing = makePattern( - ~id="nullish-coalescing", - ~name="Nullish Coalescing", - ~category=Defaults, - ~difficulty=Beginner, - ~jsPattern=`\\w+\\s*\\?\\?\\s*`, - ~confidence=0.9, - ~jsExample=`const displayName = user.name ?? 'Anonymous';`, - ~rescriptExample=`let displayName = user.name->Option.getOr("Anonymous")`, - ~narrative={ - celebrate: "Good use of nullish coalescing - you know modern JavaScript!", - minimize: "It's concise, though ?? only covers null and undefined...", - better: "ReScript's Option.getOr is explicit about what 'missing' means!", - safety: "Option type makes the absence of a value part of the type signature.", - example: "Option.getOr is the type-safe equivalent of ??!", - }, - ~tags=["nullish", "coalescing", "default", "option"], - ~relatedPatterns=["default-params", "null-check-ternary"], - ~learningObjectives=["Option.getOr", "Default value patterns"], - ~commonMistakes=["Confusing ?? with ||"], - ~bestPractices=["Use Option.getOr for explicit defaults"], -) - -// TEMPLATE LITERAL PATTERNS -let templateLiterals = makePattern( - ~id="template-literals", - ~name="Template Literals", - ~category=Templates, - ~difficulty=Beginner, - ~jsPattern="`.+\\$\\{.+\\}.+`", - ~confidence=0.9, - ~jsExample="`Hello, ${name}! You have ${count} messages.`", - ~rescriptExample="`Hello, ${name}! You have ${Int.toString(count)} messages.`", - ~narrative={ - celebrate: "Nice! Template literals are much better than string concatenation!", - minimize: "They're great, though any expression can be interpolated without type checking...", - better: "ReScript template literals require explicit type conversion - no accidental coercion!", - safety: "The compiler ensures interpolated values are strings - no [object Object] surprises.", - example: "Type-safe string interpolation prevents silent coercion bugs!", - }, - ~tags=["template", "string", "interpolation"], - ~relatedPatterns=[], - ~learningObjectives=["String interpolation", "Explicit type conversion"], - ~commonMistakes=["Interpolating non-string values without conversion"], - ~bestPractices=["Always convert non-string values explicitly"], -) - -let taggedTemplates = makePattern( - ~id="tagged-templates", - ~name="Tagged Template Literals", - ~category=Templates, - ~difficulty=Advanced, - ~jsPattern=`\\w+\s*\`.+\\$\\{`, - ~confidence=0.7, - ~jsExample=`const query = sql\`SELECT * FROM users WHERE id = \${userId}\`;`, - ~rescriptExample=`// Use a dedicated query builder with type-safe parameters -let query = Sql.select(~table="users", ~where=Sql.eq("id", userId))`, - ~narrative={ - celebrate: "Advanced! Tagged templates show deep JavaScript knowledge!", - minimize: "They're powerful, though the tag function's types aren't checked...", - better: "ReScript uses dedicated type-safe builders for domain-specific strings!", - safety: "Type-safe query builders prevent SQL injection by construction.", - example: "Domain-specific builders are safer than string interpolation!", - }, - ~tags=["template", "tagged", "dsl"], - ~relatedPatterns=["template-literals"], - ~learningObjectives=["Domain-specific builders", "Type-safe DSLs"], - ~commonMistakes=["SQL injection through templates"], - ~bestPractices=["Use type-safe builders for SQL, HTML, etc."], -) - -// ARROW FUNCTION PATTERNS -let arrowFunction = makePattern( - ~id="arrow-function", - ~name="Arrow Function", - ~category=ArrowFunctions, - ~difficulty=Beginner, - ~jsPattern=`(?:const|let)\\s+\\w+\\s*=\\s*\\([^)]*\\)\\s*=>\\s*\\{`, - ~confidence=0.8, - ~jsExample=`const calculateTotal = (items) => { - return items.reduce((sum, item) => sum + item.price, 0); -};`, - ~rescriptExample=`let calculateTotal = items => { - items->Array.reduce(0, (sum, item) => sum + item.price) -}`, - ~narrative={ - celebrate: "Great! Arrow functions are concise and avoid 'this' confusion!", - minimize: "They're perfect, ReScript's syntax is just a tiny bit cleaner...", - better: "ReScript functions are always arrow-style with automatic return!", - safety: "No 'this' binding issues - all functions are lexically scoped.", - example: "ReScript functions always return the last expression - no 'return' needed!", - }, - ~tags=["arrow", "function", "lambda"], - ~relatedPatterns=["pure-function"], - ~learningObjectives=["Function syntax", "Implicit return"], - ~commonMistakes=["Forgetting return in multi-line arrows"], - ~bestPractices=["Use concise body when possible"], -) - -let arrowImplicitReturn = makePattern( - ~id="arrow-implicit-return", - ~name="Arrow Implicit Return", - ~category=ArrowFunctions, - ~difficulty=Beginner, - ~jsPattern=`(?:const|let)\\s+\\w+\\s*=\\s*\\([^)]*\\)\\s*=>\\s*[^{]`, - ~confidence=0.75, - ~jsExample=`const double = (n) => n * 2;`, - ~rescriptExample=`let double = n => n * 2`, - ~narrative={ - celebrate: "Clean! Implicit return makes your arrow functions concise!", - minimize: "It's already great, ReScript just drops the parentheses too...", - better: "ReScript's function syntax is minimal - everything is an expression!", - safety: "The return type is inferred and checked automatically.", - example: "Minimal syntax, maximum type safety!", - }, - ~tags=["arrow", "implicit-return", "concise"], - ~relatedPatterns=["arrow-function", "pure-function"], - ~learningObjectives=["Expression-based functions", "Type inference"], - ~commonMistakes=["Accidentally returning an object literal"], - ~bestPractices=["Prefer concise form for simple transforms"], -) - -// VARIANT PATTERNS -let enumToVariant = makePattern( - ~id="enum-to-variant", - ~name="Enum to Variant", - ~category=Variants, - ~difficulty=Intermediate, - ~jsPattern=`(?:const|var|let)\\s+\\w+\\s*=\\s*(?:Object\\.freeze\\()?\\{[^}]*:\\s*['\"]\\w+['\"]`, - ~confidence=0.7, - ~jsExample=`const Status = Object.freeze({ - LOADING: 'loading', - SUCCESS: 'success', - ERROR: 'error' -});`, - ~rescriptExample=`type status = Loading | Success | Error`, - ~narrative={ - celebrate: "Smart! You're creating an enum-like pattern for safety!", - minimize: "Object.freeze helps, but the values are still just strings at runtime...", - better: "ReScript variants are real types - no string matching, no typos possible!", - safety: "Variants are exhaustively checked - add a new case and the compiler shows every spot to update.", - example: "Variants make impossible states truly impossible!", - }, - ~tags=["enum", "variant", "type-safe"], - ~relatedPatterns=["switch-statement", "union-to-variant"], - ~learningObjectives=["Variant types", "Exhaustive matching"], - ~commonMistakes=["Typos in string enums"], - ~bestPractices=["Use variants for finite sets of values"], -) - -let unionToVariant = makePattern( - ~id="union-to-variant", - ~name="Union Type to Variant", - ~category=Variants, - ~difficulty=Intermediate, - ~jsPattern=`type\\s+\\w+\\s*=\\s*['\"]\\w+['\"]\\s*\\|`, - ~confidence=0.65, - ~jsExample=`// TypeScript: type Shape = 'circle' | 'square' | 'triangle'; -const area = (shape) => { - if (shape === 'circle') return Math.PI * r * r; - if (shape === 'square') return s * s; -};`, - ~rescriptExample=`type shape = Circle(float) | Square(float) | Triangle(float, float) - -let area = shape => - switch shape { - | Circle(r) => Math.Constants.pi *. r *. r - | Square(s) => s *. s - | Triangle(b, h) => 0.5 *. b *. h - }`, - ~narrative={ - celebrate: "Good thinking with union types - you want type safety!", - minimize: "String unions help, but they can't carry data...", - better: "ReScript variants carry data AND are exhaustively checked!", - safety: "Each variant case can hold different typed data - impossible to mix up.", - example: "Variants with payloads are like tagged unions on steroids!", - }, - ~tags=["union", "variant", "tagged-union"], - ~relatedPatterns=["enum-to-variant"], - ~learningObjectives=["Variants with payloads", "Tagged unions"], - ~commonMistakes=["Forgetting to handle new variant cases"], - ~bestPractices=["Give variants meaningful payload types"], -) - -// MODULE PATTERNS -let namespaceToModule = makePattern( - ~id="namespace-to-module", - ~name="Namespace to Module", - ~category=Modules, - ~difficulty=Intermediate, - ~jsPattern=`(?:export\\s+)?(?:const|class)\\s+\\w+\\s*=\\s*\\{`, - ~confidence=0.5, - ~jsExample=`export const MathUtils = { - add: (a, b) => a + b, - subtract: (a, b) => a - b, - multiply: (a, b) => a * b, -};`, - ~rescriptExample=`// MathUtils.res - each file IS a module -let add = (a, b) => a + b -let subtract = (a, b) => a - b -let multiply = (a, b) => a * b`, - ~narrative={ - celebrate: "Good! You're organizing code into namespaces - clean architecture!", - minimize: "Object namespaces work, but they're just convention, not enforced...", - better: "In ReScript, every file is automatically a module with proper encapsulation!", - safety: "Modules have interfaces (.resi files) that enforce what's public.", - example: "File = module, no boilerplate needed!", - }, - ~tags=["module", "namespace", "organization"], - ~relatedPatterns=["class-to-module"], - ~learningObjectives=["File-based modules", "Module interfaces"], - ~commonMistakes=["Over-nesting modules"], - ~bestPractices=["Use .resi files for public API control"], -) - -// TYPE SAFETY PATTERNS -let typeAssertionToType = makePattern( - ~id="type-assertion-to-type", - ~name="Type Assertion to Sound Type", - ~category=TypeSafety, - ~difficulty=Intermediate, - ~jsPattern=`as\\s+\\w+|<\\w+>\\w+`, - ~confidence=0.6, - ~jsExample=`const input = document.getElementById('name') as HTMLInputElement; -const value = (input as any).value;`, - ~rescriptExample=`// Use proper bindings instead of type assertions -@val @return(nullable) -external getElementById: string => option = "document.getElementById" - -switch getElementById("name") { -| Some(el) => // safely use element -| None => // handle missing element -}`, - ~narrative={ - celebrate: "You know about type annotations - that's type-aware programming!", - minimize: "Type assertions override the compiler - they can hide real errors...", - better: "ReScript has no type assertions - the type system is 100% sound!", - safety: "Sound types mean if it compiles, the types are correct. No 'as any' escape hatch.", - example: "100% sound type system - no type assertions, no type lies!", - }, - ~tags=["type-safety", "assertion", "sound"], - ~relatedPatterns=["any-to-typed"], - ~learningObjectives=["Sound type system", "External bindings"], - ~commonMistakes=["Reaching for 'as any'"], - ~bestPractices=["Write proper bindings instead of type assertions"], -) - -let anyToTyped = makePattern( - ~id="any-to-typed", - ~name="Any Type to Proper Type", - ~category=TypeSafety, - ~difficulty=Intermediate, - ~jsPattern=`:\\s*any\\b|as\\s+any`, - ~confidence=0.85, - ~jsExample=`function processData(data: any) { - return data.map((item: any) => item.name); -}`, - ~rescriptExample=`type item = {name: string} - -let processData = (data: array) => { - data->Array.map(item => item.name) -}`, - ~narrative={ - celebrate: "You're using TypeScript's type system - that's progress from plain JS!", - minimize: "'any' is sometimes needed, but it turns off type checking entirely...", - better: "ReScript has no 'any' type - everything is properly typed from the start!", - safety: "No escape hatches means the compiler catches ALL type errors.", - example: "No 'any', no 'unknown', no type holes - just correct types!", - }, - ~tags=["any", "type-safety", "typed"], - ~relatedPatterns=["type-assertion-to-type"], - ~learningObjectives=["Eliminating any", "Full type coverage"], - ~commonMistakes=["Using any as a quick fix"], - ~bestPractices=["Define proper types for all data"], -) - -// IMMUTABILITY PATTERNS -let constToLet = makePattern( - ~id="const-to-let", - ~name="Const to Immutable Let", - ~category=Immutability, - ~difficulty=Beginner, - ~jsPattern=`(?:let|var)\\s+\\w+\\s*=`, - ~confidence=0.5, - ~jsExample=`let count = 0; -count = count + 1; // mutation allowed! - -var name = 'initial'; -name = 'changed'; // mutation allowed!`, - ~rescriptExample=`let count = 0 -// count = count + 1 // ERROR: can't reassign let binding! - -let count2 = count + 1 // create a new binding instead`, - ~narrative={ - celebrate: "You're using variables to track state - the foundation of programming!", - minimize: "let and var allow reassignment, which can cause subtle bugs...", - better: "ReScript's let is immutable by default - you create new bindings instead!", - safety: "Immutable bindings prevent accidental state corruption.", - example: "Immutable by default, mutable only when you explicitly ask for it!", - }, - ~tags=["immutable", "let", "const", "binding"], - ~relatedPatterns=["spread-to-update"], - ~learningObjectives=["Immutable bindings", "Creating new values"], - ~commonMistakes=["Trying to reassign let bindings"], - ~bestPractices=["Prefer creating new bindings over mutation"], -) - -let spreadToUpdate = makePattern( - ~id="spread-to-update", - ~name="Spread to Record Update", - ~category=Immutability, - ~difficulty=Beginner, - ~jsPattern=`\\{\\s*\\.\\.\\.\\w+`, - ~confidence=0.9, - ~jsExample=`const updated = { ...user, name: 'New Name', age: 30 };`, - ~rescriptExample=`let updated = {...user, name: "New Name", age: 30}`, - ~narrative={ - celebrate: "Excellent! Spread operator for immutable updates - that's functional thinking!", - minimize: "It's great, though spread is shallow and types aren't checked...", - better: "ReScript's record update syntax is type-safe and looks almost identical!", - safety: "The compiler ensures updated fields exist and have the right types.", - example: "Type-safe record updates with familiar spread-like syntax!", - }, - ~tags=["spread", "immutable", "record-update"], - ~relatedPatterns=["const-to-let"], - ~learningObjectives=["Record update syntax", "Immutable data updates"], - ~commonMistakes=["Shallow copy issues"], - ~bestPractices=["Use record update for immutable modifications"], -) - -// PATTERN MATCHING PATTERNS -let switchToMatch = makePattern( - ~id="switch-to-match", - ~name="Switch to Pattern Match", - ~category=PatternMatching, - ~difficulty=Intermediate, - ~jsPattern=`switch\\s*\\([^)]+\\)\\s*\\{\\s*case`, - ~confidence=0.9, - ~jsExample=`switch (action.type) { - case 'INCREMENT': return { count: state.count + 1 }; - case 'DECREMENT': return { count: state.count - 1 }; - case 'RESET': return { count: 0 }; - default: return state; -}`, - ~rescriptExample=`switch action { -| Increment => {...state, count: state.count + 1} -| Decrement => {...state, count: state.count - 1} -| Reset => {...state, count: 0} -}`, - ~narrative={ - celebrate: "Good pattern! Switch on action type is a solid state management approach!", - minimize: "It works, but string matching can have typos and the default hides missing cases...", - better: "ReScript pattern matching on variants is exhaustive - no default needed!", - safety: "Add a new action and the compiler shows every switch that needs updating.", - example: "Exhaustive pattern matching is the heart of ReScript!", - }, - ~tags=["switch", "pattern-matching", "exhaustive"], - ~relatedPatterns=["switch-statement", "enum-to-variant"], - ~learningObjectives=["Deep pattern matching", "Exhaustiveness"], - ~commonMistakes=["Using default as a catch-all"], - ~bestPractices=["Never use a default/wildcard without good reason"], -) - -let nestedTernaryToMatch = makePattern( - ~id="nested-ternary-to-match", - ~name="Nested Ternary to Match", - ~category=PatternMatching, - ~difficulty=Intermediate, - ~jsPattern=`\\?[^:]+:[^?]+\\?`, - ~confidence=0.7, - ~jsExample=`const label = status === 'loading' ? 'Loading...' - : status === 'error' ? 'Error!' - : status === 'success' ? 'Done!' - : 'Unknown';`, - ~rescriptExample=`let label = switch status { -| Loading => "Loading..." -| Error(_) => "Error!" -| Success => "Done!" -}`, - ~narrative={ - celebrate: "You're handling multiple cases - that's thorough!", - minimize: "Nested ternaries work but can be hard to read...", - better: "ReScript's switch is cleaner and exhaustive - no 'Unknown' default needed!", - safety: "The compiler ensures you handle every possible case.", - example: "Pattern matching replaces nested ternaries with clarity!", - }, - ~tags=["ternary", "pattern-matching", "readability"], - ~relatedPatterns=["switch-to-match"], - ~learningObjectives=["Replacing ternaries", "Switch expressions"], - ~commonMistakes=["Deeply nested ternaries"], - ~bestPractices=["Use switch for more than 2 cases"], -) - -// PIPE OPERATOR PATTERNS -let chainToPipe = makePattern( - ~id="chain-to-pipe", - ~name="Method Chain to Pipe", - ~category=PipeOperator, - ~difficulty=Beginner, - ~jsPattern=`\\w+\\.\\w+\\([^)]*\\)\\.\\w+\\([^)]*\\)\\.\\w+`, - ~confidence=0.8, - ~jsExample=`const result = users - .filter(u => u.active) - .map(u => u.name) - .sort();`, - ~rescriptExample=`let result = users -->Array.filter(u => u.active) -->Array.map(u => u.name) -->Array.toSorted(String.compare)`, - ~narrative={ - celebrate: "Excellent method chaining! You know how to compose operations!", - minimize: "It's clean, though methods must be on the prototype...", - better: "ReScript's pipe operator works with ANY function, not just methods!", - safety: "Pipe operator is more flexible than method chaining and fully typed.", - example: "The -> operator: data flows left-to-right through any function!", - }, - ~tags=["pipe", "chain", "composition"], - ~relatedPatterns=["array-map", "array-filter"], - ~learningObjectives=["Pipe operator ->", "Data-first functions"], - ~commonMistakes=["Forgetting -> syntax"], - ~bestPractices=["Use pipe for readable data transformations"], -) - -let nestedCallsToPipe = makePattern( - ~id="nested-calls-to-pipe", - ~name="Nested Calls to Pipe", - ~category=PipeOperator, - ~difficulty=Beginner, - ~jsPattern=`\\w+\\(\\w+\\([^)]*\\)\\)`, - ~confidence=0.7, - ~jsExample=`const result = capitalize(trim(toLowerCase(input)));`, - ~rescriptExample=`let result = input -->String.toLowerCase -->String.trim -->String.capitalize`, - ~narrative={ - celebrate: "You're composing functions - that's functional programming!", - minimize: "Nested calls read inside-out, which can be confusing...", - better: "ReScript's pipe operator makes the flow read naturally left-to-right!", - safety: "Each step's types are checked as data flows through the pipe.", - example: "Read data transformations like a sentence, not an onion!", - }, - ~tags=["pipe", "nested", "readability"], - ~relatedPatterns=["chain-to-pipe"], - ~learningObjectives=["Pipe vs nesting", "Readable composition"], - ~commonMistakes=["Reading order confusion"], - ~bestPractices=["Use pipe for 2+ nested function calls"], -) - -// OOP TO FP PATTERNS -let classMethodToFunction = makePattern( - ~id="class-method-to-function", - ~name="Class Method to Function", - ~category=OopToFp, - ~difficulty=Intermediate, - ~jsPattern=`class\\s+\\w+\\s*\\{[^}]*\\w+\\s*\\([^)]*\\)\\s*\\{`, - ~confidence=0.7, - ~jsExample=`class Calculator { - add(a, b) { return a + b; } - subtract(a, b) { return a - b; } -} -const calc = new Calculator(); -calc.add(1, 2);`, - ~rescriptExample=`// Calculator.res -let add = (a, b) => a + b -let subtract = (a, b) => a - b - -// Usage: -Calculator.add(1, 2)`, - ~narrative={ - celebrate: "Good OOP! Classes organize related operations together!", - minimize: "Classes work, but they carry mutable state and 'this' binding issues...", - better: "ReScript modules group functions without 'this', 'new', or hidden state!", - safety: "Pure functions in modules - no constructor issues, no 'this' confusion.", - example: "Modules: all the organization of classes, none of the complexity!", - }, - ~tags=["class", "module", "oop", "fp"], - ~relatedPatterns=["namespace-to-module"], - ~learningObjectives=["Classes to modules", "Stateless programming"], - ~commonMistakes=["Trying to use 'this' in ReScript"], - ~bestPractices=["Think modules and records, not classes and instances"], -) - -// CLASSES TO RECORDS PATTERNS -let classToRecord = makePattern( - ~id="class-to-record", - ~name="Class to Record", - ~category=ClassesToRecords, - ~difficulty=Intermediate, - ~jsPattern=`class\\s+\\w+\\s*\\{\\s*constructor`, - ~confidence=0.8, - ~jsExample=`class User { - constructor(name, email, age) { - this.name = name; - this.email = email; - this.age = age; - } -}`, - ~rescriptExample=`type user = { - name: string, - email: string, - age: int, -} - -let makeUser = (~name, ~email, ~age) => {name, email, age}`, - ~narrative={ - celebrate: "Classes for data structures - you're organizing your data well!", - minimize: "Constructor classes are verbose and allow mutation of fields...", - better: "ReScript records are simpler, immutable, and fully typed!", - safety: "Records are immutable by default - no accidental field mutations.", - example: "Records: data without the ceremony of classes!", - }, - ~tags=["class", "record", "data"], - ~relatedPatterns=["class-method-to-function"], - ~learningObjectives=["Record types", "Constructor functions"], - ~commonMistakes=["Adding methods to records"], - ~bestPractices=["Use records for data, modules for functions"], -) - -// INHERITANCE TO COMPOSITION PATTERNS -let inheritanceToComposition = makePattern( - ~id="inheritance-to-composition", - ~name="Inheritance to Composition", - ~category=InheritanceToComposition, - ~difficulty=Advanced, - ~jsPattern=`class\\s+\\w+\\s+extends\\s+\\w+`, - ~confidence=0.9, - ~jsExample=`class Animal { speak() { return '...'; } } -class Dog extends Animal { speak() { return 'Woof!'; } } -class Cat extends Animal { speak() { return 'Meow!'; } }`, - ~rescriptExample=`type animal = Dog | Cat | Bird - -let speak = animal => - switch animal { - | Dog => "Woof!" - | Cat => "Meow!" - | Bird => "Tweet!" - }`, - ~narrative={ - celebrate: "You understand inheritance - it's a fundamental OOP concept!", - minimize: "Deep inheritance hierarchies can become rigid and hard to change...", - better: "ReScript variants + pattern matching give you polymorphism without inheritance!", - safety: "Adding a new variant forces you to update all pattern matches - no forgotten overrides.", - example: "Variants: polymorphism that the compiler checks exhaustively!", - }, - ~tags=["inheritance", "composition", "variant", "polymorphism"], - ~relatedPatterns=["class-to-record", "enum-to-variant"], - ~learningObjectives=["Composition over inheritance", "Variant-based polymorphism"], - ~commonMistakes=["Deep inheritance hierarchies"], - ~bestPractices=["Prefer variants and modules over class hierarchies"], -) - -// STATE MACHINE PATTERNS -let stateMachine = makePattern( - ~id="state-machine", - ~name="State Machine", - ~category=StateMachines, - ~difficulty=Advanced, - ~jsPattern=`(?:state|status)\\s*===?\\s*['\"]\\w+['\"]`, - ~confidence=0.6, - ~jsExample=`let state = 'idle'; -function transition(event) { - if (state === 'idle' && event === 'start') state = 'running'; - else if (state === 'running' && event === 'pause') state = 'paused'; - else if (state === 'paused' && event === 'resume') state = 'running'; - else if (state === 'running' && event === 'stop') state = 'idle'; -}`, - ~rescriptExample=`type state = Idle | Running | Paused -type event = Start | Pause | Resume | Stop - -let transition = (state, event) => - switch (state, event) { - | (Idle, Start) => Running - | (Running, Pause) => Paused - | (Paused, Resume) => Running - | (Running, Stop) => Idle - | (state, _) => state - }`, - ~narrative={ - celebrate: "State machines! You understand that systems have discrete states!", - minimize: "String-based states can have typos and invalid transitions...", - better: "ReScript variants make each state and transition explicit and type-checked!", - safety: "Invalid state transitions are caught at compile time, not runtime.", - example: "Type-safe state machines where illegal states are impossible!", - }, - ~tags=["state-machine", "variant", "transition"], - ~relatedPatterns=["enum-to-variant", "switch-to-match"], - ~learningObjectives=["Variant-based state machines", "Tuple pattern matching"], - ~commonMistakes=["Missing state transitions"], - ~bestPractices=["Model all states as variants, all events as variants"], -) - -// DATA MODELING PATTERNS -let interfaceToType = makePattern( - ~id="interface-to-type", - ~name="Interface to Type", - ~category=DataModeling, - ~difficulty=Intermediate, - ~jsPattern=`(?:interface|type)\\s+\\w+\\s*\\{`, - ~confidence=0.6, - ~jsExample=`interface User { - id: number; - name: string; - email: string; - role: 'admin' | 'user' | 'guest'; -}`, - ~rescriptExample=`type role = Admin | User | Guest - -type user = { - id: int, - name: string, - email: string, - role: role, -}`, - ~narrative={ - celebrate: "Great data modeling! Interfaces define clear contracts!", - minimize: "Interfaces are structural and can be widened accidentally...", - better: "ReScript types are nominal and variants replace string unions!", - safety: "Nominal types prevent accidental structural compatibility.", - example: "Types + variants = precise, exhaustive data modeling!", - }, - ~tags=["interface", "type", "record", "data-modeling"], - ~relatedPatterns=["class-to-record", "enum-to-variant"], - ~learningObjectives=["Record types", "Nominal typing"], - ~commonMistakes=["Expecting structural compatibility"], - ~bestPractices=["Model domain data with records and variants"], -) - -let nestedObjectToVariant = makePattern( - ~id="nested-object-to-variant", - ~name="Nested Object to Variant", - ~category=DataModeling, - ~difficulty=Advanced, - ~jsPattern=`\\{\\s*type\\s*:\\s*['\"]\\w+['\"]`, - ~confidence=0.7, - ~jsExample=`const response = { - type: 'success', - data: { users: [...] } -}; -// OR -const response = { - type: 'error', - message: 'Not found' -};`, - ~rescriptExample=`type response = - | Success({users: array}) - | Error({message: string}) - -switch response { -| Success({users}) => // use users -| Error({message}) => // handle error -}`, - ~narrative={ - celebrate: "Smart! Discriminated unions with a 'type' field - you're thinking in types!", - minimize: "The type field is just a string - easy to misspell or forget to check...", - better: "ReScript variants with inline records are this exact pattern, but type-safe!", - safety: "Each variant arm has its own typed payload - no casting needed.", - example: "Variants with inline records: the best of both worlds!", - }, - ~tags=["discriminated-union", "variant", "data-modeling"], - ~relatedPatterns=["union-to-variant", "interface-to-type"], - ~learningObjectives=["Inline records in variants", "Discriminated unions"], - ~commonMistakes=["Forgetting to destructure variant payload"], - ~bestPractices=["Use inline records for variant data"], -) - -// ADDITIONAL MODULE PATTERNS -let importToOpen = makePattern( - ~id="import-to-open", - ~name="Import to Open", - ~category=Modules, - ~difficulty=Beginner, - ~jsPattern=`import\\s*\\{[^}]+\\}\\s*from`, - ~confidence=0.85, - ~jsExample=`import { map, filter, reduce } from './utils'; -import { useState, useEffect } from 'react';`, - ~rescriptExample=`// Just use the module name directly - no imports needed! -// Utils.map, Utils.filter, Utils.reduce - -// Or open the module to use unqualified names: -open Utils -map(data, fn)`, - ~narrative={ - celebrate: "Good! Named imports keep your namespace clean!", - minimize: "Import lists can get long and need updating when APIs change...", - better: "ReScript modules are always available - no imports needed!", - safety: "The compiler resolves module references automatically - no stale imports.", - example: "No import statements, no circular dependency headaches!", - }, - ~tags=["import", "module", "open"], - ~relatedPatterns=["namespace-to-module"], - ~learningObjectives=["Module auto-resolution", "open keyword"], - ~commonMistakes=["Overusing open (namespace pollution)"], - ~bestPractices=["Prefer qualified access, use open sparingly"], -) - -// ADDITIONAL OOP TO FP PATTERNS -let thisToModule = makePattern( - ~id="this-to-module", - ~name="This Binding to Module", - ~category=OopToFp, - ~difficulty=Intermediate, - ~jsPattern=`this\\.\\w+`, - ~confidence=0.5, - ~jsExample=`class Counter { - constructor() { this.count = 0; } - increment() { this.count++; return this; } - getCount() { return this.count; } -}`, - ~rescriptExample=`type counter = {count: int} - -let make = () => {count: 0} -let increment = c => {count: c.count + 1} -let getCount = c => c.count - -// Usage: make()->increment->increment->getCount`, - ~narrative={ - celebrate: "You understand method chaining with 'this' - fluent interface!", - minimize: "'this' binding is one of JavaScript's most confusing features...", - better: "ReScript passes data explicitly - no 'this', no binding confusion!", - safety: "Explicit data flow means no lost 'this' context in callbacks.", - example: "Pipe operator replaces method chaining without 'this'!", - }, - ~tags=["this", "binding", "oop", "module"], - ~relatedPatterns=["class-method-to-function", "chain-to-pipe"], - ~learningObjectives=["Eliminating this", "Explicit data passing"], - ~commonMistakes=["Looking for 'this' in ReScript"], - ~bestPractices=["Pass state as first argument, use pipe for chaining"], -) - -// ADDITIONAL CLASSES TO RECORDS PATTERNS -let getterToField = makePattern( - ~id="getter-to-field", - ~name="Getter/Setter to Record Field", - ~category=ClassesToRecords, - ~difficulty=Beginner, - ~jsPattern=`get\\s+\\w+\\s*\\(\\)|set\\s+\\w+\\s*\\(`, - ~confidence=0.8, - ~jsExample=`class Person { - #name; - get name() { return this.#name; } - set name(value) { this.#name = value; } -}`, - ~rescriptExample=`type person = {name: string} - -// Read: just access the field -let getName = p => p.name - -// "Update": create a new record -let setName = (p, name) => {...p, name}`, - ~narrative={ - celebrate: "Encapsulation with getters/setters - you're protecting your data!", - minimize: "Getters and setters add boilerplate for what's essentially field access...", - better: "ReScript records have direct field access and immutable updates!", - safety: "Record fields are typed - no need for validation in getters/setters.", - example: "Direct field access + immutable updates = simpler code!", - }, - ~tags=["getter", "setter", "record", "field"], - ~relatedPatterns=["class-to-record"], - ~learningObjectives=["Record field access", "Immutable record update"], - ~commonMistakes=["Trying to mutate record fields"], - ~bestPractices=["Use record update syntax for changes"], -) - -// ADDITIONAL INHERITANCE TO COMPOSITION PATTERNS -let mixinToModule = makePattern( - ~id="mixin-to-module", - ~name="Mixin to Module Include", - ~category=InheritanceToComposition, - ~difficulty=Advanced, - ~jsPattern=`Object\\.assign\\s*\\(|\\.\\.\\.[^,]+prototype`, - ~confidence=0.6, - ~jsExample=`const Serializable = { - serialize() { return JSON.stringify(this); } -}; -const Loggable = { - log() { console.log(this.toString()); } -}; -Object.assign(MyClass.prototype, Serializable, Loggable);`, - ~rescriptExample=`// Serializable.res -let serialize = data => data->JSON.stringifyAny - -// Loggable.res -let log = data => Console.log(data) - -// Usage: compose by calling module functions -Serializable.serialize(myData) -Loggable.log(myData)`, - ~narrative={ - celebrate: "Mixins! You're composing behaviour from multiple sources!", - minimize: "Runtime mixins can conflict and have no compile-time checks...", - better: "ReScript modules compose statically - just call functions from different modules!", - safety: "No runtime prototype manipulation, no method name conflicts.", - example: "Module composition: explicit, type-safe, no surprises!", - }, - ~tags=["mixin", "composition", "module", "prototype"], - ~relatedPatterns=["inheritance-to-composition", "namespace-to-module"], - ~learningObjectives=["Module composition", "Avoiding mixins"], - ~commonMistakes=["Trying to merge modules at runtime"], - ~bestPractices=["Compose by calling functions from multiple modules"], -) - -// ADDITIONAL STATE MACHINE PATTERNS -let reducerToVariant = makePattern( - ~id="reducer-to-variant", - ~name="Reducer to Variant Actions", - ~category=StateMachines, - ~difficulty=Intermediate, - ~jsPattern=`(?:case|action\\.type\\s*===?)\\s*['\"]\\w+['\"]`, - ~confidence=0.65, - ~jsExample=`function reducer(state, action) { - switch (action.type) { - case 'ADD_TODO': return {...state, todos: [...state.todos, action.payload]}; - case 'REMOVE_TODO': return {...state, todos: state.todos.filter(t => t.id !== action.id)}; - case 'TOGGLE_TODO': return {...state, todos: state.todos.map(t => - t.id === action.id ? {...t, done: !t.done} : t)}; - default: return state; - } -}`, - ~rescriptExample=`type action = - | AddTodo(todo) - | RemoveTodo(int) - | ToggleTodo(int) - -let reducer = (state, action) => - switch action { - | AddTodo(todo) => {...state, todos: state.todos->Array.concat([todo])} - | RemoveTodo(id) => {...state, todos: state.todos->Array.filter(t => t.id != id)} - | ToggleTodo(id) => {...state, todos: state.todos->Array.map(t => - t.id == id ? {...t, done: !t.done} : t)} - }`, - ~narrative={ - celebrate: "Redux-style reducer! You understand unidirectional data flow!", - minimize: "String action types are easy to typo and payloads aren't type-checked...", - better: "ReScript variant actions carry typed payloads - no string matching!", - safety: "Add a new action and the compiler shows every reducer that needs updating.", - example: "Variant-based reducers: the pattern Redux wishes it had!", - }, - ~tags=["reducer", "state-machine", "variant", "action"], - ~relatedPatterns=["state-machine", "switch-to-match"], - ~learningObjectives=["Variant-based actions", "Type-safe reducers"], - ~commonMistakes=["Using strings for action types"], - ~bestPractices=["Model every action as a variant with typed payload"], -) - -// ADDITIONAL VARIANT PATTERNS -let typeGuardToVariant = makePattern( - ~id="type-guard-to-variant", - ~name="Type Guard to Variant", - ~category=Variants, - ~difficulty=Advanced, - ~jsPattern=`(?:typeof|instanceof)\\s+\\w+\\s*===?`, - ~confidence=0.6, - ~jsExample=`function formatValue(value) { - if (typeof value === 'string') return value.toUpperCase(); - if (typeof value === 'number') return value.toFixed(2); - if (Array.isArray(value)) return value.join(', '); - return String(value); -}`, - ~rescriptExample=`type value = String(string) | Number(float) | List(array) - -let formatValue = v => - switch v { - | String(s) => String.toUpperCase(s) - | Number(n) => Float.toFixed(n, ~digits=2) - | List(items) => items->Array.join(", ") - }`, - ~narrative={ - celebrate: "Type guards! You're narrowing types at runtime - smart!", - minimize: "Runtime type checks can miss cases and aren't enforced by the compiler...", - better: "ReScript variants carry the type WITH the data - no runtime checks needed!", - safety: "Exhaustive matching means you handle every type, guaranteed.", - example: "Variants replace typeof/instanceof with compile-time safety!", - }, - ~tags=["type-guard", "typeof", "instanceof", "variant"], - ~relatedPatterns=["enum-to-variant", "union-to-variant"], - ~learningObjectives=["Replacing runtime type checks", "Variant-based dispatch"], - ~commonMistakes=["Using typeof in ReScript"], - ~bestPractices=["Model data shapes as variants from the start"], -) - -// ADDITIONAL TYPE SAFETY PATTERNS -let typeGuardFnToVariant = makePattern( - ~id="type-guard-fn-to-variant", - ~name="Type Guard Function to Pattern Match", - ~category=TypeSafety, - ~difficulty=Advanced, - ~jsPattern=`function\\s+is\\w+\\s*\\(|:\\s*\\w+\\s+is\\s+\\w+`, - ~confidence=0.65, - ~jsExample=`function isError(result): result is Error { - return result instanceof Error; -} -if (isError(value)) { - console.error(value.message); -}`, - ~rescriptExample=`type result<'a> = Ok('a) | Error(string) - -switch value { -| Ok(data) => Console.log(data) -| Error(msg) => Console.error(msg) -}`, - ~narrative={ - celebrate: "Type guard functions! You're bringing type safety to runtime checks!", - minimize: "Type predicates are powerful but rely on you writing them correctly...", - better: "ReScript's pattern matching IS the type narrowing - no guard functions needed!", - safety: "Pattern matching narrows types automatically and exhaustively.", - example: "Every switch arm narrows the type - no manual type guards!", - }, - ~tags=["type-guard", "predicate", "pattern-matching", "type-safety"], - ~relatedPatterns=["type-assertion-to-type", "switch-to-match"], - ~learningObjectives=["Automatic type narrowing", "Pattern-based dispatch"], - ~commonMistakes=["Writing manual type predicates"], - ~bestPractices=["Let pattern matching narrow types automatically"], -) - -// ADDITIONAL FUNCTIONAL PATTERNS -let compose = makePattern( - ~id="function-compose", - ~name="Function Composition", - ~category=Functional, - ~difficulty=Intermediate, - ~jsPattern=`compose\\s*\\(|(?:const|let)\\s+\\w+\\s*=\\s*\\(\\.\\.\\.[^)]*\\)\\s*=>\\s*\\w+\\.reduce`, - ~confidence=0.55, - ~jsExample=`const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x); -const transform = compose(capitalize, trim, toLowerCase); -const result = transform(input);`, - ~rescriptExample=`// No compose needed - pipe operator IS composition! -let result = input -->String.toLowerCase -->String.trim -->String.capitalize`, - ~narrative={ - celebrate: "Function composition! You're building complex operations from simple ones!", - minimize: "Compose utilities work but read right-to-left which is unnatural...", - better: "ReScript's pipe operator composes left-to-right - no utility needed!", - safety: "Each step in the pipe is type-checked individually.", - example: "Pipe operator: built-in composition that reads like English!", - }, - ~tags=["compose", "functional", "pipe"], - ~relatedPatterns=["chain-to-pipe", "nested-calls-to-pipe", "higher-order-function"], - ~learningObjectives=["Pipe as composition", "Left-to-right data flow"], - ~commonMistakes=["Creating compose utilities in ReScript"], - ~bestPractices=["Use pipe operator instead of compose"], -) - -// ADDITIONAL PIPE OPERATOR PATTERNS -let callbackToPipe = makePattern( - ~id="callback-to-pipe", - ~name="Callback to Pipe", - ~category=PipeOperator, - ~difficulty=Intermediate, - ~jsPattern=`\\w+\\s*\\(\\s*function|\\w+\\s*\\(\\s*\\([^)]*\\)\\s*=>\\s*\\{[^}]*\\}\\s*\\)`, - ~confidence=0.5, - ~jsExample=`addEventListener('click', function(event) { - const target = getTarget(event); - const data = extractData(target); - const result = processData(data); - updateUI(result); -});`, - ~rescriptExample=`addEventListener("click", event => { - event - ->getTarget - ->extractData - ->processData - ->updateUI -})`, - ~narrative={ - celebrate: "Callbacks for event handling - the foundation of interactive code!", - minimize: "Deeply nested operations inside callbacks can be hard to follow...", - better: "ReScript's pipe operator flattens callback internals into readable chains!", - safety: "Each function in the pipe is type-checked against the previous return type.", - example: "Pipe operator turns nested callback logic into linear flow!", - }, - ~tags=["callback", "pipe", "event", "readability"], - ~relatedPatterns=["chain-to-pipe", "nested-calls-to-pipe"], - ~learningObjectives=["Pipe in callbacks", "Readable event handling"], - ~commonMistakes=["Deeply nested callback logic"], - ~bestPractices=["Use pipe to flatten callback internals"], -) - -// ADDITIONAL IMMUTABILITY PATTERNS -let freezeToRecord = makePattern( - ~id="freeze-to-record", - ~name="Object.freeze to Record", - ~category=Immutability, - ~difficulty=Beginner, - ~jsPattern=`Object\\.freeze\\s*\\(`, - ~confidence=0.9, - ~jsExample=`const config = Object.freeze({ - apiUrl: 'https://api.example.com', - timeout: 5000, - retries: 3, -});`, - ~rescriptExample=`type config = { - apiUrl: string, - timeout: int, - retries: int, -} - -let config = { - apiUrl: "https://api.example.com", - timeout: 5000, - retries: 3, -} -// Already immutable - no freeze needed!`, - ~narrative={ - celebrate: "Object.freeze for immutability - you value data integrity!", - minimize: "Object.freeze is shallow and only checked at runtime...", - better: "ReScript records are immutable by default - deeply and at compile time!", - safety: "Immutability is enforced by the compiler, not a runtime function.", - example: "Records: immutable by default, no Object.freeze needed!", - }, - ~tags=["freeze", "immutable", "record", "const"], - ~relatedPatterns=["const-to-let", "spread-to-update"], - ~learningObjectives=["Default immutability", "Compile-time guarantees"], - ~commonMistakes=["Trying to freeze ReScript records (already immutable)"], - ~bestPractices=["Trust the type system - records are always immutable"], -) - -// ADDITIONAL ARRAY OPERATION PATTERNS -let forEachToIter = makePattern( - ~id="foreach-to-iter", - ~name="ForEach to Array.forEach", - ~category=ArrayOperations, - ~difficulty=Beginner, - ~jsPattern=`\\.forEach\\s*\\(\\s*(?:\\w+|\\([^)]*\\))\\s*=>`, - ~confidence=0.9, - ~jsExample=`users.forEach(user => { - console.log(user.name); - sendEmail(user.email); -});`, - ~rescriptExample=`users->Array.forEach(user => { - Console.log(user.name) - sendEmail(user.email) -})`, - ~narrative={ - celebrate: "forEach for side effects - you know when NOT to use map!", - minimize: "Works great, ReScript's version is nearly identical...", - better: "ReScript's forEach with pipe operator reads cleanly left-to-right!", - safety: "forEach returns unit - the compiler prevents accidentally using a 'result'.", - example: "forEach returns unit, not undefined - type-safe side effects!", - }, - ~tags=["foreach", "array", "side-effect"], - ~relatedPatterns=["array-map", "array-filter"], - ~learningObjectives=["Array.forEach", "Side effects vs transforms"], - ~commonMistakes=["Using map for side effects"], - ~bestPractices=["Use forEach for side effects, map for transforms"], -) - -// ADDITIONAL CONDITIONAL PATTERNS -let guardClause = makePattern( - ~id="guard-clause", - ~name="Guard Clause / Early Return", - ~category=Conditionals, - ~difficulty=Intermediate, - ~jsPattern=`if\\s*\\([^)]+\\)\\s*(?:return|throw)`, - ~confidence=0.7, - ~jsExample=`function processUser(user) { - if (!user) return null; - if (!user.active) return null; - if (!user.email) throw new Error('No email'); - return sendWelcome(user.email); -}`, - ~rescriptExample=`let processUser = user => - switch user { - | None => None - | Some({active: false}) => None - | Some({email: None}) => Error("No email") - | Some({email: Some(email), active: true}) => Ok(sendWelcome(email)) - }`, - ~narrative={ - celebrate: "Guard clauses! Early returns keep your code flat and readable!", - minimize: "Multiple early returns can make the happy path hard to spot...", - better: "ReScript pattern matching handles all guards in one clear expression!", - safety: "Pattern matching ensures you've considered every case explicitly.", - example: "Pattern matching replaces guard clauses with exhaustive case analysis!", - }, - ~tags=["guard", "early-return", "pattern-matching"], - ~relatedPatterns=["if-else-chain", "switch-to-match"], - ~learningObjectives=["Pattern matching for guards", "Exhaustive case analysis"], - ~commonMistakes=["Forgetting a guard case"], - ~bestPractices=["Use pattern matching to make all cases explicit"], -) - -// PATTERN LIBRARY -let patternLibrary: array = [ - // Null Safety (3) - nullCheckBasic, - nullCheckTernary, - optionalChaining, - // Async (2) - asyncAwaitBasic, - promiseThen, - // Error Handling (2) - tryCatchBasic, - errorResultType, - // Array Operations (4) - arrayMap, - arrayFilter, - arrayReduce, - arrayFind, - // Conditionals (2) - switchStatement, - ifElseChain, - // Functional (2) - pureFunction, - higherOrderFunction, - // Destructuring (2) - objectDestructuring, - arrayDestructuring, - // Defaults (2) - defaultParams, - nullishCoalescing, - // Templates (2) - templateLiterals, - taggedTemplates, - // Arrow Functions (2) - arrowFunction, - arrowImplicitReturn, - // Variants (2) - enumToVariant, - unionToVariant, - // Modules (2) - namespaceToModule, - importToOpen, - // Type Safety (3) - typeAssertionToType, - anyToTyped, - typeGuardFnToVariant, - // Immutability (3) - constToLet, - spreadToUpdate, - freezeToRecord, - // Pattern Matching (2) - switchToMatch, - nestedTernaryToMatch, - // Pipe Operator (3) - chainToPipe, - nestedCallsToPipe, - callbackToPipe, - // OOP to FP (2) - classMethodToFunction, - thisToModule, - // Classes to Records (2) - classToRecord, - getterToField, - // Inheritance to Composition (2) - inheritanceToComposition, - mixinToModule, - // State Machines (2) - stateMachine, - reducerToVariant, - // Data Modeling (2) - interfaceToType, - nestedObjectToVariant, - // Additional Variants (1) - typeGuardToVariant, - // Additional Functional (1) - compose, - // Additional Array Operations (1) - forEachToIter, - // Additional Conditionals (1) - guardClause, -] - -// Get pattern by ID -let getPatternById = (id: string): option => { - patternLibrary->Array.find(p => p.id === id) -} - -// Get patterns by category -let getPatternsByCategory = (category: patternCategory): array => { - patternLibrary->Array.filter(p => p.category === category) -} - -// Get patterns by difficulty -let getPatternsByDifficulty = (difficulty: difficultyLevel): array => { - patternLibrary->Array.filter(p => p.difficulty === difficulty) -} - -// Create regex from pattern string -@val external createRegex: (string, string) => Nullable.t = "RegExp" - -// Detect patterns in code -let detectPatterns = (code: string): array => { - patternLibrary - ->Array.filter(pattern => { - switch createRegex(pattern.jsPattern, "")->Nullable.toOption { - | Some(regex) => regex->RegExp.test(code) - | None => false - } - }) - ->Array.toSorted((a, b) => b.confidence -. a.confidence) -} - -// Get total pattern count -let getPatternCount = (): int => { - patternLibrary->Array.length -} - -// Get pattern statistics -let getPatternStats = (): patternStats => { - let byCategory = Dict.make() - let byDifficulty = Dict.make() - - patternLibrary->Array.forEach(pattern => { - let catStr = categoryToString(pattern.category) - let diffStr = difficultyToString(pattern.difficulty) - - let catCount = byCategory->Dict.get(catStr)->Option.getOr(0) - byCategory->Dict.set(catStr, catCount + 1) - - let diffCount = byDifficulty->Dict.get(diffStr)->Option.getOr(0) - byDifficulty->Dict.set(diffStr, diffCount + 1) - }) - - { - total: patternLibrary->Array.length, - byCategory, - byDifficulty, - } -} diff --git a/src/Scanner.res b/src/Scanner.res deleted file mode 100644 index bbd6584..0000000 --- a/src/Scanner.res +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Scanner: Matches JavaScript code against pattern library using regex detection -// Returns detailed patternMatch results with line numbers and confidence scores - -open Types - -// Create regex from pattern string (reuse from Patterns) -@val external createRegex: (string, string) => Nullable.t = "RegExp" - -// Split code into lines for line-number tracking -let splitLines = (code: string): array => { - code->String.split("\n") -} - -// Find the line number where a match occurs in the source code -let findMatchLine = (code: string, matchedText: string): int => { - let lines = splitLines(code) - let foundLine = ref(1) - lines->Array.forEachWithIndex((line, idx) => { - if String.includes(line, String.slice(matchedText, ~start=0, ~end=Math.Int.min(40, String.length(matchedText)))) { - foundLine := idx + 1 - } - }) - foundLine.contents -} - -// Count the number of lines in a matched code fragment -let countMatchLines = (matchedText: string): int => { - splitLines(matchedText)->Array.length -} - -// Scan a block of code against a single pattern, returning matches -let scanPattern = (code: string, pattern: pattern): array => { - switch createRegex(pattern.jsPattern, "gm")->Nullable.toOption { - | None => [] - | Some(regex) => - let matches = ref([]) - let safetyCounter = ref(0) - let continue_ = ref(true) - - while continue_.contents && safetyCounter.contents < 1000 { - safetyCounter := safetyCounter.contents + 1 - switch regex->RegExp.exec(code) { - | None => continue_ := false - | Some(result) => - let matchedText = result->RegExp.Result.input - let startLine = findMatchLine(code, matchedText) - let endLine = startLine + countMatchLines(matchedText) - 1 - - matches := - matches.contents->Array.concat([ - { - pattern, - code: matchedText, - startLine, - endLine, - confidence: pattern.confidence, - // Use flagship target's code as the offered transformation; - // falls back to whatever target the pattern does support. - transformation: Some(patternCodeFor(pattern, flagshipTarget)), - }, - ]) - } - } - - matches.contents - } -} - -// Scan code against all patterns in the library -let scanCode = (code: string, patterns: array): array => { - patterns - ->Array.flatMap(pattern => scanPattern(code, pattern)) - ->Array.toSorted((a, b) => { - // Sort by confidence descending, then by line number ascending - let confDiff = b.confidence -. a.confidence - if confDiff != 0.0 { - confDiff - } else { - Int.toFloat(a.startLine - b.startLine) - } - }) -} - -// Scan code against the full pattern library -let scanAll = (code: string): array => { - scanCode(code, Patterns.patternLibrary) -} - -// Get unique patterns matched (deduplicated by pattern ID) -let uniquePatterns = (matches: array): array => { - let seen = Dict.make() - matches->Array.filter(m => { - switch seen->Dict.get(m.pattern.id) { - | Some(_) => false - | None => - seen->Dict.set(m.pattern.id, true) - true - } - })->Array.map(m => m.pattern) -} - -// Count matches per category -let matchesByCategory = (matches: array): Dict.t => { - let counts = Dict.make() - matches->Array.forEach(m => { - let cat = categoryToString(m.pattern.category) - let current = counts->Dict.get(cat)->Option.getOr(0) - counts->Dict.set(cat, current + 1) - }) - counts -} diff --git a/src/Scanner.resi b/src/Scanner.resi deleted file mode 100644 index 50467f6..0000000 --- a/src/Scanner.resi +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Scanner: public interface for JavaScript code pattern scanning - -// Scan a block of code against a specific set of patterns -let scanCode: (string, array) => array - -// Scan code against the full built-in pattern library -let scanAll: (string) => array - -// Deduplicate matches by pattern ID, returning unique patterns -let uniquePatterns: (array) => array - -// Count matches grouped by pattern category -let matchesByCategory: (array) => Dict.t diff --git a/src/Types.res b/src/Types.res deleted file mode 100644 index 1d930af..0000000 --- a/src/Types.res +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Core type definitions for Nextgen Languages Evangeliser -// Philosophy: "Celebrate good, minimize bad, show better" - -type viewLayer = RAW | FOLDED | GLYPHED | WYSIWYG - -type difficultyLevel = Beginner | Intermediate | Advanced - -type patternCategory = - | NullSafety - | Async - | ErrorHandling - | ArrayOperations - | Conditionals - | Destructuring - | Defaults - | Functional - | Templates - | ArrowFunctions - | Variants - | Modules - | TypeSafety - | Immutability - | PatternMatching - | PipeOperator - | OopToFp - | ClassesToRecords - | InheritanceToComposition - | StateMachines - | DataModeling - -type semanticCategory = - | Transformation - | Safety - | Flow - | Structure - | State - | Data - -// Target languages the evangeliser can emit examples for. -// AffineScript is the flagship (Phase 2 onward); ReScript is the legacy -// catalogue preserved during migration. Others are planned (Phase 5+). -type targetLang = - | AffineScript - | ReScript - | Rust - | Gleam - | Zig - -// Flagship target — consumed by Cli/Output when --target is not specified. -let flagshipTarget: targetLang = AffineScript - -// A Makaton-inspired glyph that represents semantic meaning beyond syntax -type glyph = { - symbol: string, - name: string, - meaning: string, - semanticCategory: semanticCategory, - usageExample: string, -} - -// Encouraging narrative following "Celebrate, Minimize, Better" philosophy -type narrative = { - celebrate: string, - minimize: string, - better: string, - safety: string, - example: string, -} - -// A single target-language example for a pattern. -// In Phase 1b, only `code` varies per target; narrative is shared at -// pattern level. In Phase 2 this record can gain an optional per-target -// narrative override so AffineScript's affine-safety pitch differs from -// ReScript's Option-type pitch. -type targetExample = { - language: targetLang, - code: string, -} - -// A transformation pattern from JavaScript to one or more target languages. -type pattern = { - id: string, - name: string, - category: patternCategory, - difficulty: difficultyLevel, - jsPattern: string, - confidence: float, - jsExample: string, - // Multi-target: each entry pairs a target language with its example code. - // Patterns must have at least one target; the first target is used as - // fallback if --target requests a language this pattern has not been - // ported to yet. - targets: array, - narrative: narrative, - glyphs: array, - tags: array, - relatedPatterns: array, - learningObjectives: array, - commonMistakes: array, - bestPractices: array, -} - -// A matched pattern in user's code -type patternMatch = { - pattern: pattern, - code: string, - startLine: int, - endLine: int, - confidence: float, - transformation: option, -} - -// Analysis result for a file or selection -type analysisResult = { - matches: array, - totalPatterns: int, - coveragePercentage: float, - difficulty: difficultyLevel, - suggestedNextPatterns: array, - analysisTime: float, - memoryUsed: int, -} - -// User's learning progress -type rec learningProgress = { - patternsCompleted: Set.t, - currentDifficulty: difficultyLevel, - totalTransformations: int, - favoritePatterns: array, - achievements: array, - startDate: Date.t, - lastActive: Date.t, -} -and achievement = { - achievementId: string, - achievementName: string, - description: string, - icon: string, - unlockedAt: option, -} - -// Extension configuration -type extensionConfig = { - defaultView: viewLayer, - showNarratives: bool, - autoDetectPatterns: bool, - difficultyLevel: option, - enableTelemetry: bool, - performanceMode: bool, - customPatternPaths: array, -} - -// Pattern statistics -type patternStats = { - total: int, - byCategory: Dict.t, - byDifficulty: Dict.t, -} - -// Helper functions for category string conversion -let categoryToString = (category: patternCategory): string => { - switch category { - | NullSafety => "null-safety" - | Async => "async" - | ErrorHandling => "error-handling" - | ArrayOperations => "array-operations" - | Conditionals => "conditionals" - | Destructuring => "destructuring" - | Defaults => "defaults" - | Functional => "functional" - | Templates => "templates" - | ArrowFunctions => "arrow-functions" - | Variants => "variants" - | Modules => "modules" - | TypeSafety => "type-safety" - | Immutability => "immutability" - | PatternMatching => "pattern-matching" - | PipeOperator => "pipe-operator" - | OopToFp => "oop-to-fp" - | ClassesToRecords => "classes-to-records" - | InheritanceToComposition => "inheritance-to-composition" - | StateMachines => "state-machines" - | DataModeling => "data-modeling" - } -} - -let difficultyToString = (difficulty: difficultyLevel): string => { - switch difficulty { - | Beginner => "beginner" - | Intermediate => "intermediate" - | Advanced => "advanced" - } -} - -let viewLayerToString = (view: viewLayer): string => { - switch view { - | RAW => "RAW" - | FOLDED => "FOLDED" - | GLYPHED => "GLYPHED" - | WYSIWYG => "WYSIWYG" - } -} - -// Canonical string name for a target language (lowercase, used in CLI args). -let targetLangToString = (t: targetLang): string => { - switch t { - | AffineScript => "affinescript" - | ReScript => "rescript" - | Rust => "rust" - | Gleam => "gleam" - | Zig => "zig" - } -} - -// Display-friendly label (capitalisation for rendering). -let targetLangLabel = (t: targetLang): string => { - switch t { - | AffineScript => "AffineScript" - | ReScript => "ReScript" - | Rust => "Rust" - | Gleam => "Gleam" - | Zig => "Zig" - } -} - -// Markdown / Rouge syntax-highlighter tag for a target language. -let targetLangSyntaxTag = (t: targetLang): string => { - switch t { - | AffineScript => "affinescript" - | ReScript => "rescript" - | Rust => "rust" - | Gleam => "gleam" - | Zig => "zig" - } -} - -let stringToTargetLang = (s: string): option => { - switch String.toLowerCase(s) { - | "affinescript" | "affine" | "as" => Some(AffineScript) - | "rescript" | "res" => Some(ReScript) - | "rust" | "rs" => Some(Rust) - | "gleam" => Some(Gleam) - | "zig" => Some(Zig) - | _ => None - } -} - -// Find the example code for a specific target language in a pattern. -// Returns None if the pattern has no entry for that target. -let patternExampleFor = (p: pattern, lang: targetLang): option => { - p.targets->Array.find(t => t.language == lang) -} - -// Get the code string for a specific target, falling back to the first -// available target if the requested one is not present. Empty string if -// the pattern has no targets at all (should never happen in practice). -let patternCodeFor = (p: pattern, lang: targetLang): string => { - switch patternExampleFor(p, lang) { - | Some(t) => t.code - | None => - switch p.targets->Array.get(0) { - | Some(t) => t.code - | None => "" - } - } -} - -// The effective target for a pattern given a requested target: the -// requested one if supported, else the pattern's first target (fallback). -let patternEffectiveTarget = (p: pattern, requested: targetLang): targetLang => { - switch patternExampleFor(p, requested) { - | Some(_) => requested - | None => - switch p.targets->Array.get(0) { - | Some(t) => t.language - | None => requested - } - } -} diff --git a/test/Analyser_test.res b/test/Analyser_test.res deleted file mode 100644 index efce952..0000000 --- a/test/Analyser_test.res +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Tests for Analyser module - end-to-end analysis pipeline - -open Types - -let assert_ = (condition, message) => { - if !condition { - JsError.throwWithMessage(`FAIL: ${message}`) - } -} - -let testAnalyseEmptyCode = () => { - let result = Analyser.analyse("") - assert_(result.matches->Array.length === 0, "Empty code produces no matches") - assert_(result.coveragePercentage === 0.0, "Empty code has 0% coverage") - assert_(result.difficulty === Beginner, "Empty code defaults to Beginner difficulty") - Console.log("PASS: analyse('') returns empty result with Beginner difficulty") -} - -let testAnalyseSimpleJS = () => { - let code = `const doubled = numbers.map(n => n * 2); -const evens = numbers.filter(n => n % 2 === 0);` - let result = Analyser.analyse(code) - assert_(result.matches->Array.length > 0, "Simple JS code produces matches") - assert_(result.totalPatterns > 0, "totalPatterns is populated") - assert_(result.analysisTime >= 0.0, "analysisTime is non-negative") - Console.log("PASS: analyse with simple JS produces matches") -} - -let testAnalyseComplexJS = () => { - let code = ` -async function fetchUsers() { - try { - const response = await fetch('/api/users'); - const users = await response.json(); - return users.map(u => u.name).filter(n => n !== null); - } catch (error) { - console.error(error); - return []; - } -} - -if (user !== null && user !== undefined) { - const { name, email } = user; - const displayName = name ?? 'Anonymous'; -} -` - let result = Analyser.analyse(code) - assert_(result.matches->Array.length >= 3, "Complex JS matches multiple patterns") - assert_(result.coveragePercentage > 0.0, "Complex JS has non-zero coverage") - Console.log(`PASS: Complex JS analysis found ${Int.toString(result.matches->Array.length)} matches, ${Float.toFixed(result.coveragePercentage, ~digits=1)}% coverage`) -} - -let testDifficultyAssessment = () => { - // Beginner-only code - let beginnerCode = `const doubled = numbers.map(n => n * 2);` - let beginnerResult = Analyser.analyse(beginnerCode) - assert_(beginnerResult.difficulty === Beginner, "Simple map code is Beginner") - - // Code with advanced patterns - let advancedCode = `class Dog extends Animal { speak() { return 'Woof!'; } }` - let advancedResult = Analyser.analyse(advancedCode) - assert_(advancedResult.difficulty === Advanced, "Inheritance code is Advanced") - - Console.log("PASS: Difficulty assessment - Beginner and Advanced correctly identified") -} - -let testSuggestedNextPatterns = () => { - let code = `const doubled = numbers.map(n => n * 2);` - let result = Analyser.analyse(code) - assert_(result.suggestedNextPatterns->Array.length > 0, "Suggestions provided") - assert_(result.suggestedNextPatterns->Array.length <= 5, "At most 5 suggestions") - Console.log("PASS: suggestedNextPatterns returns 1-5 suggestions") -} - -let testAnalyseWithPatterns = () => { - let code = `const doubled = numbers.map(n => n * 2);` - let onlyArrayPatterns = Patterns.getPatternsByCategory(ArrayOperations) - let result = Analyser.analyseWithPatterns(code, onlyArrayPatterns) - assert_(result.totalPatterns === onlyArrayPatterns->Array.length, "totalPatterns matches custom set") - Console.log("PASS: analyseWithPatterns uses custom pattern set") -} - -let testSummarise = () => { - let emptyResult = Analyser.analyse("") - let emptySummary = Analyser.summarise(emptyResult) - assert_(String.includes(emptySummary, "No JavaScript patterns"), "Empty summary has helpful message") - - let code = `const doubled = numbers.map(n => n * 2);` - let result = Analyser.analyse(code) - let summary = Analyser.summarise(result) - assert_(String.includes(summary, "match"), "Summary mentions matches") - assert_(String.includes(summary, "coverage"), "Summary mentions coverage") - Console.log("PASS: summarise - empty and non-empty results produce appropriate messages") -} - -let testCoverageCalculation = () => { - // Code that touches many categories - let code = ` -const doubled = numbers.map(n => n * 2); -async function f() { await fetch('/api'); } -try { x(); } catch(e) { console.log(e); } -if (x > 10) { return 'large'; } else if (x > 5) { return 'medium'; } else { return 'small'; } -const { name } = user; -const updated = { ...user, name: 'New' }; -` - let result = Analyser.analyse(code) - // Should have coverage > 0 and < 100 (we won't match all 21 categories) - assert_(result.coveragePercentage > 0.0, "Multi-category code has positive coverage") - assert_(result.coveragePercentage <= 100.0, "Coverage is at most 100%") - Console.log(`PASS: Coverage calculation - ${Float.toFixed(result.coveragePercentage, ~digits=1)}% for multi-category code`) -} - -let runAll = () => { - Console.log("=== Analyser Tests ===") - testAnalyseEmptyCode() - testAnalyseSimpleJS() - testAnalyseComplexJS() - testDifficultyAssessment() - testSuggestedNextPatterns() - testAnalyseWithPatterns() - testSummarise() - testCoverageCalculation() - Console.log("=== All Analyser tests passed ===\n") -} - -runAll() diff --git a/test/Glyphs_test.res b/test/Glyphs_test.res deleted file mode 100644 index 9d829e4..0000000 --- a/test/Glyphs_test.res +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Tests for Glyphs module - -open Types - -let assert_ = (condition, message) => { - if !condition { - JsError.throwWithMessage(`FAIL: ${message}`) - } -} - -let testGetAllGlyphs = () => { - let all = Glyphs.getAllGlyphs() - assert_(all->Array.length === 21, `getAllGlyphs returns 21 glyphs, got ${Int.toString(all->Array.length)}`) - Console.log("PASS: getAllGlyphs returns 21 glyphs (11 core + 10 extended)") -} - -let testGetGlyphBySymbol = () => { - switch Glyphs.getGlyphBySymbol(`🔄`) { - | Some(g) => assert_(g.name === "Transform", "Transform glyph found by symbol") - | None => JsError.throwWithMessage("FAIL: Transform glyph not found by symbol") - } - - switch Glyphs.getGlyphBySymbol(`🛡️`) { - | Some(g) => assert_(g.name === "Shield", "Shield glyph found by symbol") - | None => JsError.throwWithMessage("FAIL: Shield glyph not found by symbol") - } - - switch Glyphs.getGlyphBySymbol("NONEXISTENT") { - | Some(_) => JsError.throwWithMessage("FAIL: Nonexistent glyph should return None") - | None => () - } - Console.log("PASS: getGlyphBySymbol - found and not-found cases") -} - -let testGetGlyphsByCategory = () => { - let safetyGlyphs = Glyphs.getGlyphsByCategory(Safety) - assert_(safetyGlyphs->Array.length > 0, "Safety category has glyphs") - - let transformGlyphs = Glyphs.getGlyphsByCategory(Transformation) - assert_(transformGlyphs->Array.length > 0, "Transformation category has glyphs") - - let flowGlyphs = Glyphs.getGlyphsByCategory(Flow) - assert_(flowGlyphs->Array.length > 0, "Flow category has glyphs") - - let structureGlyphs = Glyphs.getGlyphsByCategory(Structure) - assert_(structureGlyphs->Array.length > 0, "Structure category has glyphs") - - let dataGlyphs = Glyphs.getGlyphsByCategory(Data) - assert_(dataGlyphs->Array.length > 0, "Data category has glyphs") - - Console.log("PASS: getGlyphsByCategory - all semantic categories have glyphs") -} - -let testGetGlyphsForPattern = () => { - // Every pattern category should map to exactly 3 glyphs - let categories: array = [ - NullSafety, Async, ErrorHandling, ArrayOperations, Conditionals, - Destructuring, Defaults, Functional, Templates, ArrowFunctions, - Variants, Modules, TypeSafety, Immutability, PatternMatching, - PipeOperator, OopToFp, ClassesToRecords, InheritanceToComposition, - StateMachines, DataModeling, - ] - - categories->Array.forEach(cat => { - let glyphs = Glyphs.getGlyphsForPattern(cat) - assert_( - glyphs->Array.length === 3, - `${categoryToString(cat)} should have 3 glyphs, got ${Int.toString(glyphs->Array.length)}`, - ) - }) - Console.log("PASS: getGlyphsForPattern - all 21 categories map to 3 glyphs each") -} - -let testAnnotateWithGlyphs = () => { - let result = Glyphs.annotateWithGlyphs("some code", [`🔄`, `🛡️`]) - assert_(String.includes(result, `🔄`), "annotated code includes first glyph") - assert_(String.includes(result, "some code"), "annotated code includes original code") - Console.log("PASS: annotateWithGlyphs - glyphs prepended to code") -} - -let testCreateGlyphLegend = () => { - let legend = Glyphs.createGlyphLegend() - assert_(String.includes(legend, "Glyph Legend"), "legend has header") - assert_(String.includes(legend, "Safety"), "legend includes Safety category") - assert_(String.includes(legend, "Transformation"), "legend includes Transformation category") - assert_(String.includes(legend, "Flow"), "legend includes Flow category") - assert_(String.includes(legend, "Structure"), "legend includes Structure category") - assert_(String.includes(legend, "Data"), "legend includes Data category") - Console.log("PASS: createGlyphLegend - contains all category sections") -} - -let runAll = () => { - Console.log("=== Glyphs Tests ===") - testGetAllGlyphs() - testGetGlyphBySymbol() - testGetGlyphsByCategory() - testGetGlyphsForPattern() - testAnnotateWithGlyphs() - testCreateGlyphLegend() - Console.log("=== All Glyphs tests passed ===\n") -} - -runAll() diff --git a/test/Narrative_test.res b/test/Narrative_test.res deleted file mode 100644 index 75d1879..0000000 --- a/test/Narrative_test.res +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Tests for Narrative module - -open Types - -let assert_ = (condition, message) => { - if !condition { - JsError.throwWithMessage(`FAIL: ${message}`) - } -} - -let testGetTemplateForCategory = () => { - // Categories with explicit templates - let categoriesWithTemplates = [ - NullSafety, Async, ErrorHandling, ArrayOperations, Conditionals, Functional, - ] - - categoriesWithTemplates->Array.forEach(cat => { - let _template = Narrative.getTemplateForCategory(cat) - // If it returned without error, the template exists - () - }) - - // Categories without templates should fall back to default - let _defaultTemplate = Narrative.getTemplateForCategory(Destructuring) - Console.log("PASS: getTemplateForCategory - 6 explicit + fallback to default") -} - -let testGenerateCategoryNarrative = () => { - let narrative = Narrative.generateCategoryNarrative(NullSafety, "Test Pattern") - assert_(String.length(narrative.celebrate) > 0, "celebrate is non-empty") - assert_(String.length(narrative.minimize) > 0, "minimize is non-empty") - assert_(String.length(narrative.better) > 0, "better is non-empty") - assert_(String.length(narrative.safety) > 0, "safety is non-empty") - assert_(String.includes(narrative.example, "Test Pattern"), "example mentions pattern name") - Console.log("PASS: generateCategoryNarrative - all narrative fields populated") -} - -let testFormatNarrative = () => { - let narrative: narrative = { - celebrate: "Great job!", - minimize: "Just a small thing...", - better: "ReScript makes it better!", - safety: "Type-safe guaranteed.", - example: "See example here.", - } - - let plain = Narrative.formatNarrative(narrative, "plain") - assert_(String.includes(plain, "Great job!"), "plain format includes celebrate") - assert_(String.includes(plain, "Safety:"), "plain format includes safety label") - - let html = Narrative.formatNarrative(narrative, "html") - assert_(String.includes(html, "
      "), "html format has narrative div") - assert_(String.includes(html, "celebrate"), "html format has celebrate class") - - let markdown = Narrative.formatNarrative(narrative, "markdown") - assert_(String.includes(markdown, "**You were close!**"), "markdown format has bold header") - assert_(String.includes(markdown, "**Safety:**"), "markdown format has safety header") - - Console.log("PASS: formatNarrative - plain, html, markdown all formatted correctly") -} - -let testGenerateSuccessMessage = () => { - let msg = Narrative.generateSuccessMessage("Array.map", "beginner") - assert_(String.includes(msg, "Array.map"), "success message mentions pattern name") - assert_(String.length(msg) > 10, "success message is non-trivial") - Console.log("PASS: generateSuccessMessage - includes pattern name") -} - -let testGenerateHint = () => { - let mockPattern: pattern = { - id: "test", - name: "Test", - category: NullSafety, - difficulty: Beginner, - jsPattern: "", - confidence: 0.9, - jsExample: "", - targets: [{language: ReScript, code: ""}], - narrative: { - celebrate: "", - minimize: "", - better: "", - safety: "", - example: "", - }, - glyphs: [], - tags: [], - relatedPatterns: [], - learningObjectives: ["Learn Option type"], - commonMistakes: ["Forgetting None"], - bestPractices: ["Always handle None"], - } - - let hint = Narrative.generateHint(mockPattern) - assert_(String.length(hint) > 0, "hint is non-empty") - Console.log("PASS: generateHint - returns non-empty hint") -} - -let runAll = () => { - Console.log("=== Narrative Tests ===") - testGetTemplateForCategory() - testGenerateCategoryNarrative() - testFormatNarrative() - testGenerateSuccessMessage() - testGenerateHint() - Console.log("=== All Narrative tests passed ===\n") -} - -runAll() diff --git a/test/Patterns_test.res b/test/Patterns_test.res deleted file mode 100644 index ca332c5..0000000 --- a/test/Patterns_test.res +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Tests for Patterns module - pattern registry completeness - -open Types - -let assert_ = (condition, message) => { - if !condition { - JsError.throwWithMessage(`FAIL: ${message}`) - } -} - -let testPatternCount = () => { - let count = Patterns.getPatternCount() - assert_(count >= 50, `Expected 50+ patterns, got ${Int.toString(count)}`) - Console.log(`PASS: Pattern count is ${Int.toString(count)} (target: 50+)`) -} - -let testAllCategoriesHavePatterns = () => { - let categories: array = [ - NullSafety, Async, ErrorHandling, ArrayOperations, Conditionals, - Destructuring, Defaults, Functional, Templates, ArrowFunctions, - Variants, Modules, TypeSafety, Immutability, PatternMatching, - PipeOperator, OopToFp, ClassesToRecords, InheritanceToComposition, - StateMachines, DataModeling, - ] - - let missingCategories = categories->Array.filter(cat => { - Patterns.getPatternsByCategory(cat)->Array.length === 0 - }) - - if missingCategories->Array.length > 0 { - let missing = - missingCategories - ->Array.map(cat => categoryToString(cat)) - ->Array.join(", ") - JsError.throwWithMessage(`FAIL: Categories with no patterns: ${missing}`) - } - Console.log("PASS: All 21 categories have at least one pattern") -} - -let testPatternIdsAreUnique = () => { - let seen = Dict.make() - let duplicates = ref([]) - - Patterns.patternLibrary->Array.forEach(p => { - switch seen->Dict.get(p.id) { - | Some(_) => duplicates := duplicates.contents->Array.concat([p.id]) - | None => seen->Dict.set(p.id, true) - } - }) - - if duplicates.contents->Array.length > 0 { - JsError.throwWithMessage(`FAIL: Duplicate pattern IDs: ${duplicates.contents->Array.join(", ")}`) - } - Console.log("PASS: All pattern IDs are unique") -} - -let testPatternsHaveRequiredFields = () => { - Patterns.patternLibrary->Array.forEach(p => { - assert_(String.length(p.id) > 0, `Pattern missing id`) - assert_(String.length(p.name) > 0, `Pattern ${p.id} missing name`) - assert_(String.length(p.jsPattern) > 0, `Pattern ${p.id} missing jsPattern`) - assert_(p.confidence > 0.0 && p.confidence <= 1.0, `Pattern ${p.id} confidence out of range`) - assert_(String.length(p.jsExample) > 0, `Pattern ${p.id} missing jsExample`) - assert_(p.targets->Array.length > 0, `Pattern ${p.id} has no target examples`) - p.targets->Array.forEach(t => { - assert_(String.length(t.code) > 0, `Pattern ${p.id} has empty code for target ${targetLangLabel(t.language)}`) - }) - assert_(String.length(p.narrative.celebrate) > 0, `Pattern ${p.id} missing narrative.celebrate`) - assert_(String.length(p.narrative.minimize) > 0, `Pattern ${p.id} missing narrative.minimize`) - assert_(String.length(p.narrative.better) > 0, `Pattern ${p.id} missing narrative.better`) - assert_(String.length(p.narrative.safety) > 0, `Pattern ${p.id} missing narrative.safety`) - assert_(p.tags->Array.length > 0, `Pattern ${p.id} missing tags`) - assert_(p.learningObjectives->Array.length > 0, `Pattern ${p.id} missing learningObjectives`) - assert_(p.glyphs->Array.length === 3, `Pattern ${p.id} should have 3 glyphs`) - }) - Console.log("PASS: All patterns have required fields populated") -} - -let testGetPatternById = () => { - switch Patterns.getPatternById("null-check-basic") { - | Some(p) => assert_(p.name === "Basic Null Check", "Found null-check-basic by ID") - | None => JsError.throwWithMessage("FAIL: null-check-basic not found") - } - - switch Patterns.getPatternById("nonexistent") { - | Some(_) => JsError.throwWithMessage("FAIL: nonexistent pattern should return None") - | None => () - } - Console.log("PASS: getPatternById - found and not-found cases") -} - -let testGetPatternsByDifficulty = () => { - let beginnerPatterns = Patterns.getPatternsByDifficulty(Beginner) - let intermediatePatterns = Patterns.getPatternsByDifficulty(Intermediate) - let advancedPatterns = Patterns.getPatternsByDifficulty(Advanced) - - assert_(beginnerPatterns->Array.length > 0, "Has beginner patterns") - assert_(intermediatePatterns->Array.length > 0, "Has intermediate patterns") - assert_(advancedPatterns->Array.length > 0, "Has advanced patterns") - - let total = - beginnerPatterns->Array.length + - intermediatePatterns->Array.length + - advancedPatterns->Array.length - assert_( - total === Patterns.getPatternCount(), - "Difficulty counts sum to total", - ) - Console.log("PASS: getPatternsByDifficulty - all difficulty levels populated, sum matches total") -} - -let testGetPatternStats = () => { - let stats = Patterns.getPatternStats() - assert_(stats.total === Patterns.getPatternCount(), "stats.total matches getPatternCount") - - let catTotal = ref(0) - stats.byCategory->Dict.toArray->Array.forEach(((_key, count)) => { - catTotal := catTotal.contents + count - }) - assert_(catTotal.contents === stats.total, "category counts sum to total") - - let diffTotal = ref(0) - stats.byDifficulty->Dict.toArray->Array.forEach(((_key, count)) => { - diffTotal := diffTotal.contents + count - }) - assert_(diffTotal.contents === stats.total, "difficulty counts sum to total") - Console.log("PASS: getPatternStats - totals are consistent") -} - -let runAll = () => { - Console.log("=== Patterns Tests ===") - testPatternCount() - testAllCategoriesHavePatterns() - testPatternIdsAreUnique() - testPatternsHaveRequiredFields() - testGetPatternById() - testGetPatternsByDifficulty() - testGetPatternStats() - Console.log("=== All Patterns tests passed ===\n") -} - -runAll() diff --git a/test/Scanner_test.res b/test/Scanner_test.res deleted file mode 100644 index 8d5d7f1..0000000 --- a/test/Scanner_test.res +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Tests for Scanner module - regex matching against known JS snippets - -open Types - -let assert_ = (condition, message) => { - if !condition { - JsError.throwWithMessage(`FAIL: ${message}`) - } -} - -let testScanDetectsNullChecks = () => { - let code = `if (user !== null && user !== undefined) { - console.log(user.name); -}` - let matches = Scanner.scanAll(code) - let hasNullSafety = matches->Array.some(m => m.pattern.category === NullSafety) - assert_(hasNullSafety, "Detects null safety pattern in null check code") - Console.log("PASS: Detects null check patterns") -} - -let testScanDetectsAsyncAwait = () => { - let code = `async function fetchData() { - const response = await fetch('/api/data'); - return await response.json(); -}` - let matches = Scanner.scanAll(code) - let hasAsync = matches->Array.some(m => m.pattern.category === Async) - assert_(hasAsync, "Detects async pattern in async/await code") - Console.log("PASS: Detects async/await patterns") -} - -let testScanDetectsArrayOps = () => { - let code = `const doubled = numbers.map(n => n * 2); -const evens = numbers.filter(n => n % 2 === 0); -const sum = numbers.reduce((acc, n) => acc + n, 0);` - let matches = Scanner.scanAll(code) - let arrayMatches = matches->Array.filter(m => m.pattern.category === ArrayOperations) - assert_(arrayMatches->Array.length >= 3, `Detects at least 3 array operations, got ${Int.toString(arrayMatches->Array.length)}`) - Console.log("PASS: Detects map, filter, reduce array operations") -} - -let testScanDetectsTryCatch = () => { - let code = `try { - const result = riskyOperation(); - return result; -} catch (error) { - console.error(error); -}` - let matches = Scanner.scanAll(code) - let hasError = matches->Array.some(m => m.pattern.category === ErrorHandling) - assert_(hasError, "Detects error handling in try/catch code") - Console.log("PASS: Detects try/catch error handling") -} - -let testScanDetectsSwitch = () => { - let code = `switch (status) { - case 'loading': return 'Loading...'; - case 'success': return data; - default: return null; -}` - let matches = Scanner.scanAll(code) - let hasConditional = matches->Array.some(m => m.pattern.category === Conditionals) - assert_(hasConditional, "Detects conditional pattern in switch code") - Console.log("PASS: Detects switch statement patterns") -} - -let testScanDetectsPromiseThen = () => { - let code = `fetch('/api').then(res => res.json()).then(data => console.log(data));` - let matches = Scanner.scanAll(code) - let hasAsync = matches->Array.some(m => m.pattern.id === "promise-then") - assert_(hasAsync, "Detects Promise.then pattern") - Console.log("PASS: Detects Promise.then chains") -} - -let testScanDetectsSpreadOperator = () => { - let code = `const updated = { ...user, name: 'New Name' };` - let matches = Scanner.scanAll(code) - let hasSpread = matches->Array.some(m => m.pattern.id === "spread-to-update") - assert_(hasSpread, "Detects spread operator") - Console.log("PASS: Detects spread operator for immutable updates") -} - -let testScanDetectsClassInheritance = () => { - let code = `class Dog extends Animal { speak() { return 'Woof!'; } }` - let matches = Scanner.scanAll(code) - let hasInheritance = matches->Array.some(m => m.pattern.category === InheritanceToComposition) - assert_(hasInheritance, "Detects class inheritance") - Console.log("PASS: Detects class inheritance pattern") -} - -let testScanReturnsEmptyForCleanCode = () => { - let code = `// just a comment` - let matches = Scanner.scanAll(code) - assert_(matches->Array.length === 0, "No matches for comment-only code") - Console.log("PASS: Returns empty for code with no patterns") -} - -let testScanSortsByConfidence = () => { - let code = ` -const doubled = numbers.map(n => n * 2); -const city = user?.address?.city; -` - let matches = Scanner.scanAll(code) - if matches->Array.length >= 2 { - let first = matches->Array.getUnsafe(0) - let second = matches->Array.getUnsafe(1) - assert_(first.confidence >= second.confidence, "Results sorted by confidence descending") - } - Console.log("PASS: Results sorted by confidence descending") -} - -let testUniquePatterns = () => { - let code = ` -const a = numbers.map(x => x + 1); -const b = items.map(x => x.name); -` - let matches = Scanner.scanAll(code) - let unique = Scanner.uniquePatterns(matches) - // Multiple map matches should deduplicate to one unique pattern - let mapCount = unique->Array.filter(p => p.id === "array-map")->Array.length - assert_(mapCount <= 1, "Unique patterns deduplicates array-map") - Console.log("PASS: uniquePatterns deduplicates correctly") -} - -let testMatchesByCategory = () => { - let code = ` -const doubled = numbers.map(n => n * 2); -async function fetchData() { await fetch('/api'); } -` - let matches = Scanner.scanAll(code) - let byCat = Scanner.matchesByCategory(matches) - let arrayCount = byCat->Dict.get("array-operations")->Option.getOr(0) - assert_(arrayCount > 0, "matchesByCategory counts array operations") - Console.log("PASS: matchesByCategory groups correctly") -} - -let runAll = () => { - Console.log("=== Scanner Tests ===") - testScanDetectsNullChecks() - testScanDetectsAsyncAwait() - testScanDetectsArrayOps() - testScanDetectsTryCatch() - testScanDetectsSwitch() - testScanDetectsPromiseThen() - testScanDetectsSpreadOperator() - testScanDetectsClassInheritance() - testScanReturnsEmptyForCleanCode() - testScanSortsByConfidence() - testUniquePatterns() - testMatchesByCategory() - Console.log("=== All Scanner tests passed ===\n") -} - -runAll() diff --git a/test/Types_test.res b/test/Types_test.res deleted file mode 100644 index a638323..0000000 --- a/test/Types_test.res +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Tests for Types module helper functions - -open Types - -// Simple test helper -let assert_ = (condition, message) => { - if !condition { - JsError.throwWithMessage(`FAIL: ${message}`) - } -} - -let testCategoryToString = () => { - assert_(categoryToString(NullSafety) === "null-safety", "NullSafety -> null-safety") - assert_(categoryToString(Async) === "async", "Async -> async") - assert_(categoryToString(ErrorHandling) === "error-handling", "ErrorHandling -> error-handling") - assert_(categoryToString(ArrayOperations) === "array-operations", "ArrayOperations -> array-operations") - assert_(categoryToString(Conditionals) === "conditionals", "Conditionals -> conditionals") - assert_(categoryToString(Destructuring) === "destructuring", "Destructuring -> destructuring") - assert_(categoryToString(Defaults) === "defaults", "Defaults -> defaults") - assert_(categoryToString(Functional) === "functional", "Functional -> functional") - assert_(categoryToString(Templates) === "templates", "Templates -> templates") - assert_(categoryToString(ArrowFunctions) === "arrow-functions", "ArrowFunctions -> arrow-functions") - assert_(categoryToString(Variants) === "variants", "Variants -> variants") - assert_(categoryToString(Modules) === "modules", "Modules -> modules") - assert_(categoryToString(TypeSafety) === "type-safety", "TypeSafety -> type-safety") - assert_(categoryToString(Immutability) === "immutability", "Immutability -> immutability") - assert_(categoryToString(PatternMatching) === "pattern-matching", "PatternMatching -> pattern-matching") - assert_(categoryToString(PipeOperator) === "pipe-operator", "PipeOperator -> pipe-operator") - assert_(categoryToString(OopToFp) === "oop-to-fp", "OopToFp -> oop-to-fp") - assert_(categoryToString(ClassesToRecords) === "classes-to-records", "ClassesToRecords -> classes-to-records") - assert_( - categoryToString(InheritanceToComposition) === "inheritance-to-composition", - "InheritanceToComposition -> inheritance-to-composition", - ) - assert_(categoryToString(StateMachines) === "state-machines", "StateMachines -> state-machines") - assert_(categoryToString(DataModeling) === "data-modeling", "DataModeling -> data-modeling") - Console.log("PASS: categoryToString - all 21 categories") -} - -let testDifficultyToString = () => { - assert_(difficultyToString(Beginner) === "beginner", "Beginner -> beginner") - assert_(difficultyToString(Intermediate) === "intermediate", "Intermediate -> intermediate") - assert_(difficultyToString(Advanced) === "advanced", "Advanced -> advanced") - Console.log("PASS: difficultyToString - all 3 levels") -} - -let testViewLayerToString = () => { - assert_(viewLayerToString(RAW) === "RAW", "RAW -> RAW") - assert_(viewLayerToString(FOLDED) === "FOLDED", "FOLDED -> FOLDED") - assert_(viewLayerToString(GLYPHED) === "GLYPHED", "GLYPHED -> GLYPHED") - assert_(viewLayerToString(WYSIWYG) === "WYSIWYG", "WYSIWYG -> WYSIWYG") - Console.log("PASS: viewLayerToString - all 4 layers") -} - -let runAll = () => { - Console.log("=== Types Tests ===") - testCategoryToString() - testDifficultyToString() - testViewLayerToString() - Console.log("=== All Types tests passed ===\n") -} - -runAll() diff --git a/test/run_all.js b/test/run_all.js index 3089731..2e8a898 100644 --- a/test/run_all.js +++ b/test/run_all.js @@ -1,14 +1,119 @@ -#!/usr/bin/env -S deno run --allow-read --allow-env +#!/usr/bin/env -S deno run --allow-read // SPDX-License-Identifier: MPL-2.0 -// Test runner: imports all compiled test modules sequentially - -import "./Types_test.res.js" -import "./Glyphs_test.res.js" -import "./Narrative_test.res.js" -import "./Patterns_test.res.js" -import "./Scanner_test.res.js" -import "./Analyser_test.res.js" - -console.log("========================================") -console.log("All test suites passed!") -console.log("========================================") +// Engine-invariant tests over the correspondence cartridges (Deno-only — no +// ReScript). Schema conformance is covered by scripts/validate-cartridges.js; +// this asserts the *semantic* invariants the JSON Schema cannot express, and +// that mirror the Idris2 ABI (Abi.Correspondence) and the browser workspace. +// +// deno run --allow-read test/run_all.js +// deno task test / just test + +const KINDS = [ + "cognate", + "false-friend", + "antonym", + "alien-realization", + "novel", + "vanished", +] +const STRATA = ["surface", "structure", "intention", "trope", "invariant"] +const RESIDUE_SHAPES = ["none", "inverted", "lossy", "absent-source", "absent-target"] + +let passed = 0 +let failed = 0 +function check(label, cond) { + if (cond) { + passed++ + } else { + failed++ + console.error(` ✗ ${label}`) + } +} + +async function loadCartridges(dirUrl) { + const out = [] + async function walk(d) { + let entries + try { + entries = Deno.readDir(d) + } catch { + return + } + for await (const e of entries) { + const child = new URL(e.name + (e.isDirectory ? "/" : ""), d) + if (e.isDirectory) { + await walk(child) + continue + } + if (!e.name.endsWith(".cartridge.json")) continue + out.push(JSON.parse(await Deno.readTextFile(child))) + } + } + await walk(dirUrl) + return out +} + +function verdict(strata, name) { + const v = (strata ?? []).find((s) => s.stratum === name) + return v ? v.holds : null +} + +const root = new URL("../cartridges/", import.meta.url) +const cartridges = await loadCartridges(root) + +console.log("Nextgen Languages Evangeliser — cartridge invariant tests\n") + +// 1. cartridges load; the reference cartridge is present +check("at least one cartridge loads", cartridges.length >= 1) +const ref = cartridges.find((c) => c.name === "worked-examples") +check("reference cartridge 'worked-examples' present", !!ref) + +const all = cartridges.flatMap((c) => c.transitions ?? []) +check("at least one correspondence", all.length >= 1) + +// 2. every correspondence is well-formed and classified +for (const t of all) { + const id = `${t.concept ?? "?"} (${t.from?.language}->${t.to?.language})` + check(`${id}: kind is one of the six`, KINDS.includes(t.kind)) + check(`${id}: has from.language+surface`, !!t.from?.language && t.from?.surface != null) + check(`${id}: has to.language+surface`, !!t.to?.language && t.to?.surface != null) + check( + `${id}: residue shape valid`, + t.residue == null || RESIDUE_SHAPES.includes(t.residue.shape), + ) + for (const s of t.strata ?? []) { + check(`${id}: stratum '${s.stratum}' valid`, STRATA.includes(s.stratum)) + check(`${id}: stratum '${s.stratum}' holds is boolean`, typeof s.holds === "boolean") + } +} + +// 3. false-friend signature (mirrors Abi.Correspondence.isFalseFriendShape): +// surface corresponds AND intention diverges. +for (const ff of all.filter((t) => t.kind === "false-friend")) { + check(`false-friend '${ff.concept}': surface holds`, verdict(ff.strata, "surface") === true) + check( + `false-friend '${ff.concept}': intention diverges`, + verdict(ff.strata, "intention") === false, + ) +} + +// 4. novel/vanished residue direction (per CORRESPONDENCE-MODEL.adoc) +for (const nv of all.filter((t) => t.kind === "novel")) { + check(`novel '${nv.concept}': residue absent-source`, nv.residue?.shape === "absent-source") +} +for (const vn of all.filter((t) => t.kind === "vanished")) { + check(`vanished '${vn.concept}': residue absent-target`, vn.residue?.shape === "absent-target") +} + +// 5. the reference cartridge demonstrates all six kinds +if (ref) { + const kinds = new Set((ref.transitions ?? []).map((t) => t.kind)) + for (const k of KINDS) check(`reference demonstrates '${k}'`, kinds.has(k)) +} + +console.log(`\n${passed} passed, ${failed} failed`) +if (failed) { + console.error("\n❌ cartridge invariants violated") + Deno.exit(1) +} +console.log("✅ all cartridge invariants hold") From 48bf632ec191a44910414a585be756438e52b1d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 04:59:15 +0000 Subject: [PATCH 11/17] Dedup cartridge loader into src/cartridges.js (SonarCloud duplication gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SonarCloud flagged 7.1% duplication on new code: the cartridge-walk loader was repeated across bin/evangeliser.js, test/run_all.js, gui/server.js, and scripts/validate-cartridges.js. Extract one shared module (src/cartridges.js: eachCartridgeFile / loadCartridges / verdict) and route all four readers through it. Verified (Deno 2.8.3): deno fmt --check, deno lint, deno task test (59/59), deno task build (1/1 cartridges), CLI + GUI /api/cartridges live probe (6 transitions, all six kinds) — all green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .claude/CLAUDE.md | 1 + bin/evangeliser.js | 40 +++--------------------------- deno.json | 2 +- gui/server.js | 38 ++++++---------------------- scripts/validate-cartridges.js | 30 +++++++++-------------- src/cartridges.js | 45 ++++++++++++++++++++++++++++++++++ test/run_all.js | 30 ++--------------------- 7 files changed, 71 insertions(+), 115 deletions(-) create mode 100644 src/cartridges.js diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index f9e2a17..4e63725 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -80,6 +80,7 @@ JavaScript is allowed only when it is Deno/browser runtime glue or a launcher br | `bin/evangeliser.js` | 1 | Deno CLI reading correspondence cartridges (offline fallback) | Replace when AffineScript host CLI is runnable directly | | `gui/server.js` | 1 | Local Deno HTTP bridge serving the workspace + cartridge API | Replace when AffineScript-to-Deno/webview bridge is available | | `gui/app.js` | 1 | Browser-side event/render bridge for the GUI shell | Replace when AffineScript DOM/TEA bridge can drive this UI directly | +| `src/cartridges.js` | 1 | Shared Deno cartridge loader (CLI, GUI server, validator, tests) | Replace when AffineScript can host the loader | | `scripts/validate-cartridges.js` | 1 | Deno cartridge-schema validator (ajv) | Replace when AffineScript can host the validator | | `test/run_all.js` | 1 | Deno cartridge invariant tests | Replace when AffineScript test runner is runnable directly | diff --git a/bin/evangeliser.js b/bin/evangeliser.js index 8674964..a766433 100644 --- a/bin/evangeliser.js +++ b/bin/evangeliser.js @@ -13,6 +13,8 @@ // The browser workspace ('just gui') is the primary surface; this CLI is the // offline-first fallback. +import { loadCartridges, verdict } from "../src/cartridges.js" + const KINDS = { "cognate": { glyph: "🤝", pedagogy: "transfer directly" }, "false-friend": { glyph: "🎭", pedagogy: "flag the trap" }, @@ -54,42 +56,6 @@ function parseArgs(argv) { return opts } -async function loadCartridges(dirUrl) { - const out = [] - async function walk(d) { - let entries - try { - entries = Deno.readDir(d) - } catch { - return - } - for await (const e of entries) { - const child = new URL(e.name + (e.isDirectory ? "/" : ""), d) - if (e.isDirectory) { - await walk(child) - continue - } - if (!e.name.endsWith(".cartridge.json")) continue - try { - const c = JSON.parse(await Deno.readTextFile(child)) - out.push({ - cartridge: c.name ?? e.name, - transitions: Array.isArray(c.transitions) ? c.transitions : [], - }) - } catch (err) { - console.error(`skip ${e.name}: ${err.message}`) - } - } - } - await walk(dirUrl) - return out -} - -function verdict(strata, name) { - const v = (strata ?? []).find((s) => s.stratum === name) - return v ? v.holds : null -} - function render(t) { const k = KINDS[t.kind] ?? { glyph: "?", pedagogy: "" } const residue = t.residue?.shape ? ` · residue: ${t.residue.shape}` : "" @@ -119,7 +85,7 @@ async function main() { const cartridges = await loadCartridges(root) let items = cartridges.flatMap((c) => - c.transitions.map((t) => ({ ...t, cartridge: c.cartridge })) + (c.transitions ?? []).map((t) => ({ ...t, cartridge: c.name })) ) if (opts.kind) items = items.filter((t) => t.kind === opts.kind) diff --git a/deno.json b/deno.json index 01adcda..3e4e1fd 100644 --- a/deno.json +++ b/deno.json @@ -20,7 +20,7 @@ "semiColons": false, "singleQuote": false, "proseWrap": "preserve", - "include": ["bin/", "gui/", "scripts/", "test/run_all.js", "deno.json"] + "include": ["bin/", "gui/", "scripts/", "src/cartridges.js", "test/run_all.js", "deno.json"] }, "lint": { "rules": { diff --git a/gui/server.js b/gui/server.js index d0d5489..96abb40 100644 --- a/gui/server.js +++ b/gui/server.js @@ -9,6 +9,8 @@ // deno run --allow-read --allow-net gui/server.js [--port N] [--host H] [--open] // (add --allow-env to honour EVANGELISER_GUI_PORT; --allow-run for --open) +import { loadCartridges as loadAll } from "../src/cartridges.js" + const root = new URL("../", import.meta.url) function argValue(name, fallback) { @@ -26,36 +28,12 @@ function envPort() { async function loadCartridges() { const dir = new URL("cartridges/", root) - const out = [] - async function walk(d) { - let entries - try { - entries = Deno.readDir(d) - } catch { - return - } - for await (const e of entries) { - const child = new URL(e.name + (e.isDirectory ? "/" : ""), d) - if (e.isDirectory) { - await walk(child) - continue - } - if (!e.name.endsWith(".cartridge.json")) continue - try { - const c = JSON.parse(await Deno.readTextFile(child)) - out.push({ - cartridge: c.name ?? e.name, - description: c.description ?? "", - languages: c.languages ?? [], - transitions: Array.isArray(c.transitions) ? c.transitions : [], - }) - } catch (err) { - console.error(`skip ${e.name}: ${err.message}`) - } - } - } - await walk(dir) - return out + return (await loadAll(dir)).map((c) => ({ + cartridge: c.name ?? "(unnamed)", + description: c.description ?? "", + languages: c.languages ?? [], + transitions: Array.isArray(c.transitions) ? c.transitions : [], + })) } const contentTypes = { diff --git a/scripts/validate-cartridges.js b/scripts/validate-cartridges.js index 113e198..fb3faed 100644 --- a/scripts/validate-cartridges.js +++ b/scripts/validate-cartridges.js @@ -8,6 +8,7 @@ // Exits non-zero if any cartridge fails — suitable for CI. import Ajv2020 from "npm:ajv@8/dist/2020.js" +import { eachCartridgeFile } from "../src/cartridges.js" const dir = new URL("../cartridges/", import.meta.url) const schema = JSON.parse( @@ -20,26 +21,17 @@ const validate = new AjvCtor({ allErrors: true, strict: false }).compile(schema) let count = 0 let bad = 0 -async function walk(d) { - for await (const entry of Deno.readDir(d)) { - const child = new URL(entry.name + (entry.isDirectory ? "/" : ""), d) - if (entry.isDirectory) { - await walk(child) - continue - } - if (!entry.name.endsWith(".cartridge.json")) continue - count++ - const data = JSON.parse(await Deno.readTextFile(child)) - if (validate(data)) { - console.log(`VALID ${entry.name} (${data.transitions.length} transitions)`) - } else { - bad++ - console.error(`INVALID ${entry.name}`) - console.error(JSON.stringify(validate.errors, null, 2)) - } +await eachCartridgeFile(dir, async (url, name) => { + count++ + const data = JSON.parse(await Deno.readTextFile(url)) + if (validate(data)) { + console.log(`VALID ${name} (${data.transitions.length} transitions)`) + } else { + bad++ + console.error(`INVALID ${name}`) + console.error(JSON.stringify(validate.errors, null, 2)) } -} +}) -await walk(dir) console.log(`\n${count - bad}/${count} cartridges valid.`) Deno.exit(bad ? 1 : 0) diff --git a/src/cartridges.js b/src/cartridges.js new file mode 100644 index 0000000..9ccee51 --- /dev/null +++ b/src/cartridges.js @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MPL-2.0 +// Shared cartridge loader (Deno glue) for the CLI, the GUI server, the schema +// validator, and the invariant tests. One walk of cartridges/**/*.cartridge.json +// so the readers do not each re-implement it. + +// Invoke `fn(fileUrl, fileName)` for every *.cartridge.json under `rootUrl`. +export async function eachCartridgeFile(rootUrl, fn) { + async function walk(d) { + let entries + try { + entries = Deno.readDir(d) + } catch { + return + } + for await (const e of entries) { + const child = new URL(e.name + (e.isDirectory ? "/" : ""), d) + if (e.isDirectory) { + await walk(child) + continue + } + if (!e.name.endsWith(".cartridge.json")) continue + await fn(child, e.name) + } + } + await walk(rootUrl) +} + +// Parse every cartridge under `rootUrl`; returns the raw cartridge objects. +export async function loadCartridges(rootUrl) { + const out = [] + await eachCartridgeFile(rootUrl, async (url, name) => { + try { + out.push(JSON.parse(await Deno.readTextFile(url))) + } catch (err) { + console.error(`skip ${name}: ${err.message}`) + } + }) + return out +} + +// Whether a correspondence holds at the named stratum (true / false / null). +export function verdict(strata, name) { + const v = (strata ?? []).find((s) => s.stratum === name) + return v ? v.holds : null +} diff --git a/test/run_all.js b/test/run_all.js index 2e8a898..701d950 100644 --- a/test/run_all.js +++ b/test/run_all.js @@ -8,6 +8,8 @@ // deno run --allow-read test/run_all.js // deno task test / just test +import { loadCartridges, verdict } from "../src/cartridges.js" + const KINDS = [ "cognate", "false-friend", @@ -30,34 +32,6 @@ function check(label, cond) { } } -async function loadCartridges(dirUrl) { - const out = [] - async function walk(d) { - let entries - try { - entries = Deno.readDir(d) - } catch { - return - } - for await (const e of entries) { - const child = new URL(e.name + (e.isDirectory ? "/" : ""), d) - if (e.isDirectory) { - await walk(child) - continue - } - if (!e.name.endsWith(".cartridge.json")) continue - out.push(JSON.parse(await Deno.readTextFile(child))) - } - } - await walk(dirUrl) - return out -} - -function verdict(strata, name) { - const v = (strata ?? []).find((s) => s.stratum === name) - return v ? v.holds : null -} - const root = new URL("../cartridges/", import.meta.url) const cartridges = await loadCartridges(root) From e3a2a8a89bd4a9999a01277d1e8bf8b4bb22141a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 05:07:10 +0000 Subject: [PATCH 12/17] Revert cosmetic GUI reformatting; keep only the shared-loader change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit's deno-fmt pass rewrote the already-merged GUI files (gui/app.html, gui/app.js, gui/server.js) wholesale, which SonarCloud then counted as "new code" — and app.js's kind/strata constants duplicated the new CLI/test, leaving duplication at 4.4% (> 3%). Restore gui/app.html and gui/app.js to their merged (main) state, and keep only the functional shared-loader change in gui/server.js and scripts/validate-cartridges.js (route both through src/cartridges.js so the cartridge walk lives in exactly one place). Narrow deno.json fmt.include to the files this PR actually authors, so the merged GUI shell is no longer reformatted. Verified (Deno 2.8.3): deno fmt --check, deno lint, deno task test (59/59), deno task build (1/1), GUI /api/cartridges live probe — all green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- deno.json | 2 +- gui/app.html | 499 +++++++-------------------------- gui/app.js | 365 ++++++++---------------- gui/server.js | 72 ++--- scripts/validate-cartridges.js | 34 +-- 5 files changed, 274 insertions(+), 698 deletions(-) diff --git a/deno.json b/deno.json index 3e4e1fd..4c6a34b 100644 --- a/deno.json +++ b/deno.json @@ -20,7 +20,7 @@ "semiColons": false, "singleQuote": false, "proseWrap": "preserve", - "include": ["bin/", "gui/", "scripts/", "src/cartridges.js", "test/run_all.js", "deno.json"] + "include": ["bin/", "src/cartridges.js", "test/run_all.js", "deno.json"] }, "lint": { "rules": { diff --git a/gui/app.html b/gui/app.html index 8973482..a335022 100644 --- a/gui/app.html +++ b/gui/app.html @@ -28,450 +28,154 @@ } @media (prefers-color-scheme: dark) { :root { - --bg: #0e1418; - --panel: #161e25; - --ink: #e7eef5; - --muted: #9fb0bd; - --line: #2a353e; - --accent: #36b3ad; - --accent-ink: #06211f; - --code: #0a1016; - --code-ink: #dce6ef; - --focus: #4fd0c9; - --k-cognate: #58c98a; - --k-false: #ef8a7e; - --k-antonym: #74b6e6; - --k-alien: #b79ce6; - --k-novel: #e0b85a; - --k-vanished: #aab6c2; + --bg: #0e1418; --panel: #161e25; --ink: #e7eef5; --muted: #9fb0bd; + --line: #2a353e; --accent: #36b3ad; --accent-ink: #06211f; + --code: #0a1016; --code-ink: #dce6ef; --focus: #4fd0c9; + --k-cognate: #58c98a; --k-false: #ef8a7e; --k-antonym: #74b6e6; + --k-alien: #b79ce6; --k-novel: #e0b85a; --k-vanished: #aab6c2; } } - * { - box-sizing: border-box; - } + * { box-sizing: border-box; } body { - margin: 0; - min-height: 100vh; - background: var(--bg); - color: var(--ink); + margin: 0; min-height: 100vh; background: var(--bg); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } - button, select, input { - font: inherit; - color: var(--ink); - } - :focus-visible { - outline: 3px solid var(--focus); - outline-offset: 2px; - } - .app { - min-height: 100vh; - display: grid; - grid-template-rows: auto 1fr auto; - } + button, select, input { font: inherit; color: var(--ink); } + :focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } + .app { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } header.topbar { - display: flex; - flex-wrap: wrap; - gap: 14px 18px; - align-items: center; - justify-content: space-between; - padding: 12px 18px; - border-bottom: 1px solid var(--line); - background: var(--panel); - } - .brand { - display: flex; - gap: 12px; - align-items: center; - min-width: 0; + display: flex; flex-wrap: wrap; gap: 14px 18px; align-items: center; + justify-content: space-between; padding: 12px 18px; + border-bottom: 1px solid var(--line); background: var(--panel); } + .brand { display: flex; gap: 12px; align-items: center; min-width: 0; } .mark { - width: 40px; - height: 40px; - border-radius: 9px; - display: grid; - place-items: center; + width: 40px; height: 40px; border-radius: 9px; display: grid; place-items: center; background: linear-gradient(135deg, var(--accent), var(--k-alien)); - color: #fff; - font-weight: 800; - font-size: 17px; - } - h1 { - margin: 0; - font-size: 17px; - line-height: 1.15; - } - .motto { - margin-top: 2px; - color: var(--muted); - font-style: italic; - } - .controls { - display: flex; - flex-wrap: wrap; - gap: 10px 14px; - align-items: center; - } - .field { - display: flex; - gap: 6px; - align-items: center; - } - .field > label { - color: var(--muted); - font-size: 12px; + color: #fff; font-weight: 800; font-size: 17px; } + h1 { margin: 0; font-size: 17px; line-height: 1.15; } + .motto { margin-top: 2px; color: var(--muted); font-style: italic; } + .controls { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; } + .field { display: flex; gap: 6px; align-items: center; } + .field > label { color: var(--muted); font-size: 12px; } select, input[type="search"] { - min-height: 34px; - padding: 0 10px; - border: 1px solid var(--line); - border-radius: 7px; - background: var(--panel); + min-height: 34px; padding: 0 10px; border: 1px solid var(--line); + border-radius: 7px; background: var(--panel); } fieldset.views { - display: flex; - gap: 2px; - margin: 0; - padding: 3px; - border: 1px solid var(--line); - border-radius: 9px; - background: var(--panel); - } - fieldset.views legend { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip: rect(0 0 0 0); + display: flex; gap: 2px; margin: 0; padding: 3px; border: 1px solid var(--line); + border-radius: 9px; background: var(--panel); } + fieldset.views legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } .views label { - padding: 6px 10px; - border-radius: 7px; - cursor: pointer; - color: var(--muted); - font-size: 13px; - white-space: nowrap; - } - .views input { - position: absolute; - opacity: 0; - pointer-events: none; - } - .views input:checked + span { - color: var(--accent-ink); - } - .views label:has(input:checked) { - background: var(--accent); - color: var(--accent-ink); - } - .views label:has(input:focus-visible) { - outline: 3px solid var(--focus); - outline-offset: 1px; + padding: 6px 10px; border-radius: 7px; cursor: pointer; color: var(--muted); + font-size: 13px; white-space: nowrap; } + .views input { position: absolute; opacity: 0; pointer-events: none; } + .views input:checked + span { color: var(--accent-ink); } + .views label:has(input:checked) { background: var(--accent); color: var(--accent-ink); } + .views label:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 1px; } main.workspace { - min-height: 0; - display: grid; - gap: 14px; - padding: 14px; + min-height: 0; display: grid; gap: 14px; padding: 14px; grid-template-columns: minmax(240px, 0.85fr) minmax(360px, 1.4fr) minmax(280px, 1fr); } .pane { - min-width: 0; - min-height: 0; - border: 1px solid var(--line); - border-radius: 10px; - background: var(--panel); - display: grid; - grid-template-rows: auto 1fr; - overflow: hidden; + min-width: 0; min-height: 0; border: 1px solid var(--line); border-radius: 10px; + background: var(--panel); display: grid; grid-template-rows: auto 1fr; overflow: hidden; } .pane-header { - display: flex; - gap: 10px; - align-items: baseline; - justify-content: space-between; - padding: 11px 13px; - border-bottom: 1px solid var(--line); - } - .pane-title { - font-weight: 700; - } - .pane-hint { - color: var(--muted); - font-size: 12px; - } - .pane-body { - min-height: 0; - overflow: auto; - padding: 12px; + display: flex; gap: 10px; align-items: baseline; justify-content: space-between; + padding: 11px 13px; border-bottom: 1px solid var(--line); } + .pane-title { font-weight: 700; } + .pane-hint { color: var(--muted); font-size: 12px; } + .pane-body { min-height: 0; overflow: auto; padding: 12px; } /* index pane */ - .index { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: 6px; - } + .index { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } .index li { - border: 1px solid var(--line); - border-radius: 8px; - padding: 9px 11px; - cursor: pointer; - display: grid; - gap: 4px; - background: var(--panel); - } - .index li[aria-selected="true"] { - border-color: var(--accent); - box-shadow: inset 0 0 0 1px var(--accent); - } - .index .concept { - font-weight: 650; - } - .index .pair { - color: var(--muted); - font-size: 12px; + border: 1px solid var(--line); border-radius: 8px; padding: 9px 11px; cursor: pointer; + display: grid; gap: 4px; background: var(--panel); } + .index li[aria-selected="true"] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); } + .index .concept { font-weight: 650; } + .index .pair { color: var(--muted); font-size: 12px; } .badge { - display: inline-flex; - align-items: center; - gap: 5px; - border-radius: 999px; - padding: 2px 9px; - font-size: 12px; - font-weight: 650; - border: 1px solid currentColor; - } - .k-cognate { - color: var(--k-cognate); - } - .k-false { - color: var(--k-false); - } - .k-antonym { - color: var(--k-antonym); - } - .k-alien { - color: var(--k-alien); - } - .k-novel { - color: var(--k-novel); - } - .k-vanished { - color: var(--k-vanished); + display: inline-flex; align-items: center; gap: 5px; border-radius: 999px; + padding: 2px 9px; font-size: 12px; font-weight: 650; border: 1px solid currentColor; } + .k-cognate { color: var(--k-cognate); } .k-false { color: var(--k-false); } + .k-antonym { color: var(--k-antonym); } .k-alien { color: var(--k-alien); } + .k-novel { color: var(--k-novel); } .k-vanished { color: var(--k-vanished); } /* forms pane / view-layers */ - .concept-head { - margin: 0 0 12px; - font-size: 16px; - } - .sxs { - display: grid; - grid-template-columns: 1fr auto 1fr; - gap: 12px; - align-items: center; - } - .form-card { - min-width: 0; - border: 1px solid var(--line); - border-radius: 9px; - overflow: hidden; - } + .concept-head { margin: 0 0 12px; font-size: 16px; } + .sxs { display: grid; grid-template-columns: 1fr auto 1fr; gap: 12px; align-items: center; } + .form-card { min-width: 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; } .form-lang { - padding: 6px 10px; - font-size: 12px; - font-weight: 650; - color: var(--muted); - background: color-mix(in srgb, var(--accent) 8%, transparent); - border-bottom: 1px solid var(--line); + padding: 6px 10px; font-size: 12px; font-weight: 650; color: var(--muted); + background: color-mix(in srgb, var(--accent) 8%, transparent); border-bottom: 1px solid var(--line); } pre.surface { - margin: 0; - padding: 12px; - overflow: auto; - background: var(--code); - color: var(--code-ink); + margin: 0; padding: 12px; overflow: auto; background: var(--code); color: var(--code-ink); font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } - .cross { - font-size: 22px; - text-align: center; - } - .cross small { - display: block; - color: var(--muted); - font-size: 11px; - } - .focus-form { - font-size: 15px; - } - .glyph-view { - text-align: center; - padding: 18px 8px; - } - .glyph-view .big { - font-size: 56px; - line-height: 1; - } - .glyph-view .row { - display: flex; - justify-content: center; - gap: 18px; - margin-top: 14px; - flex-wrap: wrap; - } - .blockly { - display: grid; - gap: 10px; - justify-items: center; - } + .cross { font-size: 22px; text-align: center; } + .cross small { display: block; color: var(--muted); font-size: 11px; } + .focus-form { font-size: 15px; } + .glyph-view { text-align: center; padding: 18px 8px; } + .glyph-view .big { font-size: 56px; line-height: 1; } + .glyph-view .row { display: flex; justify-content: center; gap: 18px; margin-top: 14px; flex-wrap: wrap; } + .blockly { display: grid; gap: 10px; justify-items: center; } .block { - width: 100%; - max-width: 460px; - border: 2px solid var(--accent); - border-radius: 12px; - padding: 10px 14px; - background: color-mix(in srgb, var(--accent) 7%, var(--panel)); - } - .block .blang { - font-size: 12px; - color: var(--muted); - font-weight: 650; - } - .block code { - font: 13px/1.4 ui-monospace, Menlo, Consolas, monospace; - } - .connector { - font-size: 20px; - color: var(--muted); + width: 100%; max-width: 460px; border: 2px solid var(--accent); border-radius: 12px; + padding: 10px 14px; background: color-mix(in srgb, var(--accent) 7%, var(--panel)); } + .block .blang { font-size: 12px; color: var(--muted); font-weight: 650; } + .block code { font: 13px/1.4 ui-monospace, Menlo, Consolas, monospace; } + .connector { font-size: 20px; color: var(--muted); } /* classification pane */ - .kind-hero { - display: flex; - gap: 12px; - align-items: center; - margin-bottom: 12px; - } - .kind-hero .glyph { - font-size: 34px; - } - .kind-hero .pedagogy { - font-weight: 700; - } - .kind-hero .blurb { - color: var(--muted); - font-size: 13px; - } - .section { - margin: 14px 0; - } - .section h3 { - margin: 0 0 7px; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.03em; - color: var(--muted); - } - .residue { - display: flex; - gap: 8px; - align-items: baseline; - } - .residue .shape { - font-weight: 650; - } - .strata { - display: flex; - flex-wrap: wrap; - gap: 6px; - } + .kind-hero { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; } + .kind-hero .glyph { font-size: 34px; } + .kind-hero .pedagogy { font-weight: 700; } + .kind-hero .blurb { color: var(--muted); font-size: 13px; } + .section { margin: 14px 0; } + .section h3 { margin: 0 0 7px; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); } + .residue { display: flex; gap: 8px; align-items: baseline; } + .residue .shape { font-weight: 650; } + .strata { display: flex; flex-wrap: wrap; gap: 6px; } .stratum { - display: inline-flex; - gap: 5px; - align-items: center; - border: 1px solid var(--line); - border-radius: 999px; - padding: 2px 9px; - font-size: 12px; - } - .stratum.holds { - color: var(--k-cognate); - border-color: currentColor; - } - .stratum.diverges { - color: var(--k-false); - border-color: currentColor; + display: inline-flex; gap: 5px; align-items: center; border: 1px solid var(--line); + border-radius: 999px; padding: 2px 9px; font-size: 12px; } + .stratum.holds { color: var(--k-cognate); border-color: currentColor; } + .stratum.diverges { color: var(--k-false); border-color: currentColor; } .ff-flag { - margin-top: 8px; - padding: 8px 10px; - border-radius: 8px; - font-size: 13px; - color: var(--k-false); - border: 1px solid currentColor; + margin-top: 8px; padding: 8px 10px; border-radius: 8px; font-size: 13px; + color: var(--k-false); border: 1px solid currentColor; background: color-mix(in srgb, var(--k-false) 8%, transparent); } - .narrative { - display: grid; - gap: 8px; - } - .narrative .n { - border-left: 3px solid var(--accent); - padding: 6px 10px; - border-radius: 0 8px 8px 0; - background: color-mix(in srgb, var(--accent) 6%, transparent); - } - .narrative .n b { - display: block; - font-size: 11px; - text-transform: uppercase; - color: var(--muted); - } - .residue-note { - color: var(--muted); - } + .narrative { display: grid; gap: 8px; } + .narrative .n { border-left: 3px solid var(--accent); padding: 6px 10px; border-radius: 0 8px 8px 0; + background: color-mix(in srgb, var(--accent) 6%, transparent); } + .narrative .n b { display: block; font-size: 11px; text-transform: uppercase; color: var(--muted); } + .residue-note { color: var(--muted); } - .empty { - padding: 24px; - text-align: center; - color: var(--muted); - border: 1px dashed var(--line); - border-radius: 8px; - } + .empty { padding: 24px; text-align: center; color: var(--muted); border: 1px dashed var(--line); border-radius: 8px; } footer.statusbar { - padding: 8px 18px; - border-top: 1px solid var(--line); - background: var(--panel); - color: var(--muted); - font-size: 13px; - display: flex; - justify-content: space-between; - gap: 10px; + padding: 8px 18px; border-top: 1px solid var(--line); background: var(--panel); + color: var(--muted); font-size: 13px; display: flex; justify-content: space-between; gap: 10px; } - @media (prefers-reduced-motion: reduce) { - * { - transition: none !important; - animation: none !important; - } - } + @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } } @media (max-width: 1000px) { - main.workspace { - grid-template-columns: 1fr; - } - .sxs { - grid-template-columns: 1fr; - } - .cross { - transform: rotate(90deg); - } + main.workspace { grid-template-columns: 1fr; } + .sxs { grid-template-columns: 1fr; } .cross { transform: rotate(90deg); } } @@ -500,17 +204,11 @@

      Nextgen Languages Evangeliser

      - +
      View layer - + @@ -526,8 +224,7 @@

      Nextgen Languages Evangeliser

      -
        -
      +
        diff --git a/gui/app.js b/gui/app.js index bc551f8..0ddd9cb 100644 --- a/gui/app.js +++ b/gui/app.js @@ -8,152 +8,103 @@ // per-stratum verdicts, and a no-shame narrative. const KIND = { - "cognate": { - glyph: "🤝", - label: "Cognate", - pedagogy: "Transfer directly", - cls: "k-cognate", - blurb: "Same idea — your intuition carries over.", - }, - "false-friend": { - glyph: "🎭", - label: "False friend", - pedagogy: "Flag the trap", - cls: "k-false", - blurb: "Looks familiar; behaves differently.", - }, - "antonym": { - glyph: "🔄", - label: "Antonym", - pedagogy: "Remap the intuition", - cls: "k-antonym", - blurb: "Related but inverted — flip your expectation.", - }, - "alien-realization": { - glyph: "🛸", - label: "Alien realization", - pedagogy: "Bridge with effort", - cls: "k-alien", - blurb: "Same goal, unfamiliar machinery.", - }, - "novel": { - glyph: "✨", - label: "Novel", - pedagogy: "Teach de novo", - cls: "k-novel", - blurb: "No prior anchor — learn it fresh.", - }, - "vanished": { - glyph: "👻", - label: "Vanished", - pedagogy: "Un-learn / re-route", - cls: "k-vanished", - blurb: "A habit to drop — it's gone here.", - }, -} + "cognate": { glyph: "🤝", label: "Cognate", pedagogy: "Transfer directly", cls: "k-cognate", blurb: "Same idea — your intuition carries over." }, + "false-friend": { glyph: "🎭", label: "False friend", pedagogy: "Flag the trap", cls: "k-false", blurb: "Looks familiar; behaves differently." }, + "antonym": { glyph: "🔄", label: "Antonym", pedagogy: "Remap the intuition", cls: "k-antonym", blurb: "Related but inverted — flip your expectation." }, + "alien-realization": { glyph: "🛸", label: "Alien realization", pedagogy: "Bridge with effort", cls: "k-alien", blurb: "Same goal, unfamiliar machinery." }, + "novel": { glyph: "✨", label: "Novel", pedagogy: "Teach de novo", cls: "k-novel", blurb: "No prior anchor — learn it fresh." }, + "vanished": { glyph: "👻", label: "Vanished", pedagogy: "Un-learn / re-route", cls: "k-vanished", blurb: "A habit to drop — it's gone here." }, +}; const RESIDUE = { - "none": { glyph: "∅", label: "none (true isomorphism)" }, - "inverted": { glyph: "🔃", label: "inverted" }, - "lossy": { glyph: "〜", label: "lossy" }, + "none": { glyph: "∅", label: "none (true isomorphism)" }, + "inverted": { glyph: "🔃", label: "inverted" }, + "lossy": { glyph: "〜", label: "lossy" }, "absent-source": { glyph: "⌀→", label: "absent source (novel)" }, "absent-target": { glyph: "→⌀", label: "absent target (vanished)" }, -} -const STRATA = ["surface", "structure", "intention", "trope", "invariant"] +}; +const STRATA = ["surface", "structure", "intention", "trope", "invariant"]; const NARR = [ - ["celebrate", "Celebrate"], - ["minimise", "Minimise"], - ["better", "Better"], - ["safety", "Safety"], - ["example", "Example"], -] + ["celebrate", "Celebrate"], ["minimise", "Minimise"], ["better", "Better"], + ["safety", "Safety"], ["example", "Example"], +]; -const state = { items: [], view: "side-by-side", kind: "all", query: "", selected: 0 } +const state = { items: [], view: "side-by-side", kind: "all", query: "", selected: 0 }; -const $ = (id) => document.getElementById(id) +const $ = (id) => document.getElementById(id); function node(tag, opts = {}, kids = []) { - const el = document.createElement(tag) + const el = document.createElement(tag); for (const [k, v] of Object.entries(opts)) { - if (v == null) continue - if (k === "class") el.className = v - else if (k === "text") el.textContent = v - else if (k === "html") el.innerHTML = v - else el.setAttribute(k, v) + if (v == null) continue; + if (k === "class") el.className = v; + else if (k === "text") el.textContent = v; + else if (k === "html") el.innerHTML = v; + else el.setAttribute(k, v); } - for (const kid of [].concat(kids)) if (kid != null) el.append(kid) - return el + for (const kid of [].concat(kids)) if (kid != null) el.append(kid); + return el; } function kindBadge(kind) { - const k = KIND[kind] ?? { glyph: "?", label: kind, cls: "" } - return node("span", { class: `badge ${k.cls}` }, [`${k.glyph} ${k.label}`]) + const k = KIND[kind] ?? { glyph: "?", label: kind, cls: "" }; + return node("span", { class: `badge ${k.cls}` }, [`${k.glyph} ${k.label}`]); } function verdictAt(strata, name) { - const v = (strata ?? []).find((s) => s.stratum === name) - return v ? v.holds : null + const v = (strata ?? []).find((s) => s.stratum === name); + return v ? v.holds : null; } // Mirrors Abi.Correspondence.isFalseFriendShape: surface holds AND intention diverges. function isFalseFriendShape(strata) { - return verdictAt(strata, "surface") === true && verdictAt(strata, "intention") === false + return verdictAt(strata, "surface") === true && verdictAt(strata, "intention") === false; } function filtered() { - const q = state.query.trim().toLowerCase() + const q = state.query.trim().toLowerCase(); return state.items.filter((it) => { - if (state.kind !== "all" && it.kind !== state.kind) return false - if (!q) return true + if (state.kind !== "all" && it.kind !== state.kind) return false; + if (!q) return true; return [it.concept, it.from?.language, it.to?.language, it.from?.surface, it.to?.surface] - .filter(Boolean).some((s) => String(s).toLowerCase().includes(q)) - }) + .filter(Boolean).some((s) => String(s).toLowerCase().includes(q)); + }); } function renderIndex() { - const list = $("index") - const items = filtered() - $("indexCount").textContent = `${items.length} of ${state.items.length}` - list.replaceChildren() + const list = $("index"); + const items = filtered(); + $("indexCount").textContent = `${items.length} of ${state.items.length}`; + list.replaceChildren(); if (items.length === 0) { - list.append( - node("li", { class: "empty", role: "presentation", text: "No correspondences match." }), - ) - renderDetail(null) - return + list.append(node("li", { class: "empty", role: "presentation", text: "No correspondences match." })); + renderDetail(null); + return; } - if (state.selected >= items.length) state.selected = 0 + if (state.selected >= items.length) state.selected = 0; items.forEach((it, i) => { const li = node("li", { - role: "option", - id: `corr-${i}`, - tabindex: "-1", + role: "option", id: `corr-${i}`, tabindex: "-1", "aria-selected": String(i === state.selected), }, [ node("div", { class: "concept", text: it.concept }), - node("div", { - class: "pair", - text: `${it.from?.language ?? "?"} → ${it.to?.language ?? "?"}`, - }), + node("div", { class: "pair", text: `${it.from?.language ?? "?"} → ${it.to?.language ?? "?"}` }), kindBadge(it.kind), - ]) - li.addEventListener("click", () => { - state.selected = i - syncSelection() - }) - list.append(li) - }) - renderDetail(items[state.selected]) - list.setAttribute("aria-activedescendant", `corr-${state.selected}`) + ]); + li.addEventListener("click", () => { state.selected = i; syncSelection(); }); + list.append(li); + }); + renderDetail(items[state.selected]); + list.setAttribute("aria-activedescendant", `corr-${state.selected}`); } function syncSelection() { - const items = filtered() + const items = filtered(); document.querySelectorAll("#index li[role=option]").forEach((li, i) => { - li.setAttribute("aria-selected", String(i === state.selected)) - }) - $("index").setAttribute("aria-activedescendant", `corr-${state.selected}`) - const el = $(`corr-${state.selected}`) - if (el) el.scrollIntoView({ block: "nearest" }) - renderDetail(items[state.selected]) + li.setAttribute("aria-selected", String(i === state.selected)); + }); + $("index").setAttribute("aria-activedescendant", `corr-${state.selected}`); + const el = $(`corr-${state.selected}`); + if (el) el.scrollIntoView({ block: "nearest" }); + renderDetail(items[state.selected]); } // ---- view-layers ------------------------------------------------------------ @@ -162,86 +113,62 @@ function formCard(form) { return node("div", { class: "form-card" }, [ node("div", { class: "form-lang", text: form?.language ?? "?" }), node("pre", { class: "surface" }, [node("code", { text: form?.surface ?? "" })]), - ]) + ]); } function renderForms(it) { - const host = $("paneForms") - host.replaceChildren() - if (!it) { - host.append(node("div", { class: "empty", text: "Select a correspondence." })) - return - } - host.append(node("h2", { class: "concept-head", text: it.concept })) - const k = KIND[it.kind] ?? {} + const host = $("paneForms"); + host.replaceChildren(); + if (!it) { host.append(node("div", { class: "empty", text: "Select a correspondence." })); return; } + host.append(node("h2", { class: "concept-head", text: it.concept })); + const k = KIND[it.kind] ?? {}; if (state.view === "raw") { host.append(node("pre", { class: "surface" }, [node("code", { text: `${it.from?.language}: ${it.from?.surface}\n${it.to?.language}: ${it.to?.surface}`, - })])) - return + })])); + return; } if (state.view === "focus") { host.append(node("div", { class: "focus-form" }, [ - node("p", { - class: "pane-hint", - text: `from your ${it.from?.language}: ${it.from?.surface}`, - }), + node("p", { class: "pane-hint", text: `from your ${it.from?.language}: ${it.from?.surface}` }), formCard(it.to), - ])) - return + ])); + return; } if (state.view === "glyph") { host.append(node("div", { class: "glyph-view" }, [ node("div", { class: "big", text: k.glyph ?? "?" }), node("div", { text: k.label ?? it.kind }), node("div", { class: "row" }, [ - node("div", {}, [ - node("div", { class: "form-lang", text: it.from?.language }), - node("code", { text: it.from?.surface }), - ]), + node("div", {}, [node("div", { class: "form-lang", text: it.from?.language }), node("code", { text: it.from?.surface })]), node("div", { class: "cross", text: "→" }), - node("div", {}, [ - node("div", { class: "form-lang", text: it.to?.language }), - node("code", { text: it.to?.surface }), - ]), + node("div", {}, [node("div", { class: "form-lang", text: it.to?.language }), node("code", { text: it.to?.surface })]), ]), - ])) - return + ])); + return; } if (state.view === "blockly") { host.append(node("div", { class: "blockly" }, [ - node("div", { class: "block" }, [ - node("div", { class: "blang", text: it.from?.language }), - node("code", { text: it.from?.surface }), - ]), + node("div", { class: "block" }, [node("div", { class: "blang", text: it.from?.language }), node("code", { text: it.from?.surface })]), node("div", { class: "connector", text: `▼ ${k.glyph ?? ""} ${k.label ?? it.kind}` }), - node("div", { class: "block" }, [ - node("div", { class: "blang", text: it.to?.language }), - node("code", { text: it.to?.surface }), - ]), - ])) - return + node("div", { class: "block" }, [node("div", { class: "blang", text: it.to?.language }), node("code", { text: it.to?.surface })]), + ])); + return; } // side-by-side (default) host.append(node("div", { class: "sxs" }, [ formCard(it.from), - node("div", { class: "cross" }, [ - document.createTextNode(k.glyph ?? "→"), - node("small", { text: "becomes" }), - ]), + node("div", { class: "cross" }, [document.createTextNode(k.glyph ?? "→"), node("small", { text: "becomes" })]), formCard(it.to), - ])) + ])); } function renderClassify(it) { - const host = $("paneClassify") - host.replaceChildren() - if (!it) { - host.append(node("div", { class: "empty", text: "—" })) - return - } - const k = KIND[it.kind] ?? { glyph: "?", label: it.kind, pedagogy: "", blurb: "", cls: "" } + const host = $("paneClassify"); + host.replaceChildren(); + if (!it) { host.append(node("div", { class: "empty", text: "—" })); return; } + const k = KIND[it.kind] ?? { glyph: "?", label: it.kind, pedagogy: "", blurb: "", cls: "" }; host.append(node("div", { class: "kind-hero" }, [ node("div", { class: `glyph ${k.cls}`, text: k.glyph }), @@ -250,9 +177,9 @@ function renderClassify(it) { node("div", { class: "pedagogy", text: k.pedagogy }), node("div", { class: "blurb", text: k.blurb }), ]), - ])) + ])); - const res = RESIDUE[it.residue?.shape] ?? { glyph: "?", label: it.residue?.shape ?? "—" } + const res = RESIDUE[it.residue?.shape] ?? { glyph: "?", label: it.residue?.shape ?? "—" }; host.append(node("div", { class: "section" }, [ node("h3", { text: "Residue" }), node("div", { class: "residue" }, [ @@ -260,120 +187,72 @@ function renderClassify(it) { node("span", { class: "shape", text: res.label }), ]), it.residue?.note ? node("div", { class: "residue-note", text: it.residue.note }) : null, - ])) + ])); - const strataRow = node( - "div", - { class: "strata" }, - STRATA.map((s) => { - const v = verdictAt(it.strata, s) - const cls = v === true ? "holds" : v === false ? "diverges" : "" - const mark = v === true ? "✓" : v === false ? "✗" : "—" - return node("span", { class: `stratum ${cls}`, text: `${s} ${mark}` }) - }), - ) - const strataSection = node("div", { class: "section" }, [ - node("h3", { text: "Strata (levels of objects)" }), - strataRow, - ]) + const strataRow = node("div", { class: "strata" }, STRATA.map((s) => { + const v = verdictAt(it.strata, s); + const cls = v === true ? "holds" : v === false ? "diverges" : ""; + const mark = v === true ? "✓" : v === false ? "✗" : "—"; + return node("span", { class: `stratum ${cls}`, text: `${s} ${mark}` }); + })); + const strataSection = node("div", { class: "section" }, [node("h3", { text: "Strata (levels of objects)" }), strataRow]); if (isFalseFriendShape(it.strata)) { - strataSection.append( - node("div", { - class: "ff-flag", - text: "⚠ False-friend signature: corresponds at the surface, diverges at intention.", - }), - ) + strataSection.append(node("div", { class: "ff-flag", text: "⚠ False-friend signature: corresponds at the surface, diverges at intention." })); } - host.append(strataSection) + host.append(strataSection); if (it.narrative) { const blocks = NARR.filter(([key]) => it.narrative[key]) - .map(([key, label]) => - node("div", { class: "n" }, [ - node("b", { text: label }), - document.createTextNode(it.narrative[key]), - ]) - ) + .map(([key, label]) => node("div", { class: "n" }, [node("b", { text: label }), document.createTextNode(it.narrative[key])])); if (blocks.length) { - host.append( - node("div", { class: "section" }, [ - node("h3", { text: "Narrative (no shame)" }), - node("div", { class: "narrative" }, blocks), - ]), - ) + host.append(node("div", { class: "section" }, [node("h3", { text: "Narrative (no shame)" }), node("div", { class: "narrative" }, blocks)])); } } if (it.witness) { - host.append( - node("div", { class: "section" }, [ - node("h3", { text: "Witness" }), - node("code", { text: it.witness }), - ]), - ) + host.append(node("div", { class: "section" }, [node("h3", { text: "Witness" }), node("code", { text: it.witness })])); } } -function renderDetail(it) { - renderForms(it) - renderClassify(it) -} +function renderDetail(it) { renderForms(it); renderClassify(it); } // ---- events ----------------------------------------------------------------- function onKeyNav(e) { - const items = filtered() - if (!items.length) return + const items = filtered(); + if (!items.length) return; if (e.key === "ArrowDown" || e.key === "ArrowUp") { - e.preventDefault() - const d = e.key === "ArrowDown" ? 1 : -1 - state.selected = (state.selected + d + items.length) % items.length - syncSelection() - } else if (e.key === "Home") { - e.preventDefault() - state.selected = 0 - syncSelection() - } else if (e.key === "End") { - e.preventDefault() - state.selected = items.length - 1 - syncSelection() - } + e.preventDefault(); + const d = e.key === "ArrowDown" ? 1 : -1; + state.selected = (state.selected + d + items.length) % items.length; + syncSelection(); + } else if (e.key === "Home") { e.preventDefault(); state.selected = 0; syncSelection(); } + else if (e.key === "End") { e.preventDefault(); state.selected = items.length - 1; syncSelection(); } } async function init() { - $("filterKind").addEventListener("change", (e) => { - state.kind = e.target.value - state.selected = 0 - renderIndex() - }) - $("search").addEventListener("input", (e) => { - state.query = e.target.value - state.selected = 0 - renderIndex() - }) - $("index").addEventListener("keydown", onKeyNav) + $("filterKind").addEventListener("change", (e) => { state.kind = e.target.value; state.selected = 0; renderIndex(); }); + $("search").addEventListener("input", (e) => { state.query = e.target.value; state.selected = 0; renderIndex(); }); + $("index").addEventListener("keydown", onKeyNav); document.querySelectorAll("input[name=view]").forEach((r) => r.addEventListener("change", (e) => { - state.view = e.target.value - $("viewHint").textContent = e.target.value - renderForms(filtered()[state.selected]) - }) - ) + state.view = e.target.value; $("viewHint").textContent = e.target.value; + renderForms(filtered()[state.selected]); + })); try { - const res = await fetch("/api/cartridges") - const body = await res.json() - if (!res.ok) throw new Error(body.error || "load failed") + const res = await fetch("/api/cartridges"); + const body = await res.json(); + if (!res.ok) throw new Error(body.error || "load failed"); state.items = (body.cartridges || []).flatMap((c) => - (c.transitions || []).map((t) => ({ ...t, cartridge: c.cartridge })) - ) + (c.transitions || []).map((t) => ({ ...t, cartridge: c.cartridge }))); $("status").textContent = state.items.length ? `${state.items.length} correspondences across ${body.cartridges.length} cartridge(s)` - : "No cartridges found." - renderIndex() + : "No cartridges found."; + renderIndex(); } catch (err) { - $("status").textContent = `Error: ${err.message}` - $("index").append(node("li", { class: "empty", text: err.message })) + $("status").textContent = `Error: ${err.message}`; + $("index").append(node("li", { class: "empty", text: err.message })); } } -init() +init(); diff --git a/gui/server.js b/gui/server.js index 96abb40..b8f32ce 100644 --- a/gui/server.js +++ b/gui/server.js @@ -9,103 +9,103 @@ // deno run --allow-read --allow-net gui/server.js [--port N] [--host H] [--open] // (add --allow-env to honour EVANGELISER_GUI_PORT; --allow-run for --open) -import { loadCartridges as loadAll } from "../src/cartridges.js" +import { loadCartridges as loadAll } from "../src/cartridges.js"; -const root = new URL("../", import.meta.url) +const root = new URL("../", import.meta.url); function argValue(name, fallback) { - const i = Deno.args.indexOf(name) - return i === -1 || i + 1 >= Deno.args.length ? fallback : Deno.args[i + 1] + const i = Deno.args.indexOf(name); + return i === -1 || i + 1 >= Deno.args.length ? fallback : Deno.args[i + 1]; } function envPort() { try { - return Deno.env.get("EVANGELISER_GUI_PORT") + return Deno.env.get("EVANGELISER_GUI_PORT"); } catch { - return undefined // --allow-env not granted; fine + return undefined; // --allow-env not granted; fine } } async function loadCartridges() { - const dir = new URL("cartridges/", root) + const dir = new URL("cartridges/", root); return (await loadAll(dir)).map((c) => ({ cartridge: c.name ?? "(unnamed)", description: c.description ?? "", languages: c.languages ?? [], transitions: Array.isArray(c.transitions) ? c.transitions : [], - })) + })); } const contentTypes = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", -} +}; async function staticFile(name) { - const body = await Deno.readFile(new URL(name, import.meta.url)) - const ext = name.slice(name.lastIndexOf(".")) + const body = await Deno.readFile(new URL(name, import.meta.url)); + const ext = name.slice(name.lastIndexOf(".")); return new Response(body, { headers: { "content-type": contentTypes[ext] ?? "application/octet-stream" }, - }) + }); } function json(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" }, - }) + }); } async function handler(req) { - const url = new URL(req.url) + const url = new URL(req.url); try { if (req.method === "GET") { - if (url.pathname === "/") return await staticFile("app.html") - if (url.pathname === "/app.js") return await staticFile("app.js") + if (url.pathname === "/") return await staticFile("app.html"); + if (url.pathname === "/app.js") return await staticFile("app.js"); if (url.pathname === "/api/cartridges") { - return json({ cartridges: await loadCartridges() }) + return json({ cartridges: await loadCartridges() }); } } - return json({ error: "Not found" }, 404) + return json({ error: "Not found" }, 404); } catch (err) { - console.error(err) - return json({ error: err.message ?? String(err) }, 500) + console.error(err); + return json({ error: err.message ?? String(err) }, 500); } } -const requestedPort = Number(argValue("--port", envPort() ?? "8765")) -const hostname = argValue("--host", "127.0.0.1") -let server -let port = requestedPort +const requestedPort = Number(argValue("--port", envPort() ?? "8765")); +const hostname = argValue("--host", "127.0.0.1"); +let server; +let port = requestedPort; for (let attempt = 0; attempt < 20; attempt += 1) { try { - port = requestedPort + attempt - server = Deno.serve({ hostname, port, onListen: () => {} }, handler) - break + port = requestedPort + attempt; + server = Deno.serve({ hostname, port, onListen: () => {} }, handler); + break; } catch (err) { - if (String(err.message ?? err).includes("Address already in use")) continue - throw err + if (String(err.message ?? err).includes("Address already in use")) continue; + throw err; } } if (!server) { - throw new Error(`No free port from ${requestedPort} to ${requestedPort + 19}`) + throw new Error(`No free port from ${requestedPort} to ${requestedPort + 19}`); } -const addr = `http://${hostname}:${port}/` -console.log(`Correspondence workspace: ${addr}`) +const addr = `http://${hostname}:${port}/`; +console.log(`Correspondence workspace: ${addr}`); if (Deno.args.includes("--open")) { setTimeout(() => { for (const cmd of [["sensible-browser", addr], ["gio", "open", addr], ["xdg-open", addr]]) { try { - new Deno.Command(cmd[0], { args: cmd.slice(1), stdout: "null", stderr: "null" }).spawn() - return + new Deno.Command(cmd[0], { args: cmd.slice(1), stdout: "null", stderr: "null" }).spawn(); + return; } catch { // try the next opener } } - }, 250) + }, 250); } -await server.finished +await server.finished; diff --git a/scripts/validate-cartridges.js b/scripts/validate-cartridges.js index fb3faed..e8bfbbe 100644 --- a/scripts/validate-cartridges.js +++ b/scripts/validate-cartridges.js @@ -7,31 +7,31 @@ // // Exits non-zero if any cartridge fails — suitable for CI. -import Ajv2020 from "npm:ajv@8/dist/2020.js" -import { eachCartridgeFile } from "../src/cartridges.js" +import Ajv2020 from "npm:ajv@8/dist/2020.js"; +import { eachCartridgeFile } from "../src/cartridges.js"; -const dir = new URL("../cartridges/", import.meta.url) +const dir = new URL("../cartridges/", import.meta.url); const schema = JSON.parse( await Deno.readTextFile(new URL("correspondence-cartridge.schema.json", dir)), -) +); -const AjvCtor = Ajv2020.default ?? Ajv2020 -const validate = new AjvCtor({ allErrors: true, strict: false }).compile(schema) +const AjvCtor = Ajv2020.default ?? Ajv2020; +const validate = new AjvCtor({ allErrors: true, strict: false }).compile(schema); -let count = 0 -let bad = 0 +let count = 0; +let bad = 0; await eachCartridgeFile(dir, async (url, name) => { - count++ - const data = JSON.parse(await Deno.readTextFile(url)) + count++; + const data = JSON.parse(await Deno.readTextFile(url)); if (validate(data)) { - console.log(`VALID ${name} (${data.transitions.length} transitions)`) + console.log(`VALID ${name} (${data.transitions.length} transitions)`); } else { - bad++ - console.error(`INVALID ${name}`) - console.error(JSON.stringify(validate.errors, null, 2)) + bad++; + console.error(`INVALID ${name}`); + console.error(JSON.stringify(validate.errors, null, 2)); } -}) +}); -console.log(`\n${count - bad}/${count} cartridges valid.`) -Deno.exit(bad ? 1 : 0) +console.log(`\n${count - bad}/${count} cartridges valid.`); +Deno.exit(bad ? 1 : 0); From 7dd94fe8fa6aa46294daf683ecb8614175a2c176 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:05:55 +0000 Subject: [PATCH 13/17] =?UTF-8?q?Add=20HANDOFF.adoc=20=E2=80=94=20orientat?= =?UTF-8?q?ion=20for=20resuming=20(e.g.=20in=20the=20desktop=20app)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Short "read me first" for the next session: current state (ReScript host removed; Deno + cartridges runtime; Idris2/Zig/AffineScript spine under src/interface/), how to run, toolchain notes, next steps from the roadmap, the guardrails, and the open owner-only LICENSE SPDX item. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- HANDOFF.adoc | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 HANDOFF.adoc diff --git a/HANDOFF.adoc b/HANDOFF.adoc new file mode 100644 index 0000000..2b64a93 --- /dev/null +++ b/HANDOFF.adoc @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Session handoff — read me first +:toc: macro + +Quick orientation for the next working session (e.g. resuming in the Claude Code +desktop app). The long-term plan lives in link:ROADMAP.adoc[ROADMAP.adoc]; the +canonical design in link:docs/theory/CORRESPONDENCE-MODEL.adoc[the spec]; the +live state in `.machine_readable/STATE.a2ml`. + +== Where things stand (2026-06) + +* *Identity:* Duolingo / Rosetta Stone *for programming languages* — a + comprehension + transfer engine that *classifies* cross-language + correspondences. It is *not* an IDE, a linter, a universal translator, or a + shame-the-JS matcher. Classify, don't translate. +* *The legacy ReScript host has been removed* (merged). ReScript survives only + as a legacy _target_ language in the correspondence content (the + vanished-`return` example, the cartridge target, the Form examples). +* *Runtime surface = Deno + cartridge data.* No compile step. +* *Engine spine* (authored; the host application is still to be built): + `src/interface/Abi/` (Idris2 ABI — typechecks), `src/interface/ffi/` (Zig + C-ABI mirror), `src/interface/host/Correspondence.affine` (AffineScript + binding, compiler-gated). + +== Run it + +[source,bash] +---- +just gui # browser multi-pane workspace (primary surface) +just test # cartridge invariant tests (Deno) — 59 checks +just build # validate cartridge data (no compile step) +deno run --allow-read bin/evangeliser.js --kind false-friend # offline CLI +---- + +== Toolchain + +* *Required:* https://deno.land[Deno] (latest stable) — runs everything above. +* *Optional, now locally buildable* (these were "author-now / verify-in-CI" in + the cloud env because it could not fetch them; on a desktop you can actually + build/verify): *Idris2* (`idris2 --typecheck abi.ipkg`), *Zig* (the FFI mirror + under `src/interface/ffi/`). + +== Next steps (pick up from ROADMAP M2/M4/M5) + +. Port the legacy pattern catalogue into *cartridge facts* + (Concept/Form/Transition + CorrespondenceKind + residue + strata). +. Author a *second language pack* to prove the engine is language-agnostic + (JTV is the canonical alien-realization / novel exemplar). +. Begin the *AffineScript host application* against the Idris2 ABI / Zig FFI + seams (needs the AffineScript compiler). +. Optional cleanup: reframe the old-model contributor prose + (`CONTRIBUTING.md`, `MAINTAINERS.md`, `docs/TPCF.md`, the duplicate + `CHANGELOG.md`) — still carries "JS→ReScript transformation" language. + +== Guardrails (non-negotiable) + +* *Deno*, not npm/bun. *Justfile*, not Makefile. *Zig* for FFI, *Idris2* for + ABI/proofs. +* Keep the *no-shame* voice: celebrate / minimise / better / safety / example. +* *Never* reintroduce `.res` host code — the host path is AffineScript + (`.affine`). +* *Licence files and SPDX headers are owner-only.* Never relicense or + bulk-sweep SPDX; licence-label drift is FLAG-ONLY to the owner. + +== Known open item (owner decision) + +The `governance / Licence consistency` check fails because `LICENSE` has no +`SPDX-License-Identifier:` header. This is *pre-existing* and *owner-domain* — +left untouched on purpose. It is entangled with label drift (the `LICENSE` body +is the Palimpsest text; `config.ncl` says `MIT OR Palimpsest-0.8`; the README +says "Palimpsest License (MPL-2.0)"; the manifests say `MPL-2.0`). Resolve only +with an explicit owner decision on the exact identifier. From 6930568f97b16f38958896f59f86043e508e9607 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:34:52 +0000 Subject: [PATCH 14/17] Purge stale TS/VS Code-era docs; finish .adoc migration where unambiguous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several docs still described the long-dead TypeScript VS Code extension (npm, Jest, Babel, "JS→ReScript patterns") — two host-generations out of date and actively misleading. Rewritten to current reality (correspondence engine; Deno + cartridges runtime; Idris2 ABI + Zig FFI + AffineScript host spine; cartridge authoring as the contribution model; no-shame voice): - docs/RSR_COMPLIANCE.md -> docs/RSR_COMPLIANCE.adoc (accurate + honest; neutral licensing section that defers to LICENSE) - docs/TPCF.md -> docs/TPCF.adoc (perimeter model reframed for cartridges) - CONTRIBUTING.md rewritten in place (kept .md — GitHub-required file) Removed (superseded by a clean .adoc twin): - MAINTAINERS.md (MAINTAINERS.adoc is canonical) References repointed: README badges + links, CODE_OF_CONDUCT.md, SECURITY.md, .github/workflows/ci.yml + .gitlab-ci.yml doc checks, Justfile validate-docs. deno fmt/lint/test/build all green; no dangling links remain. Out of scope (flagged, deliberately untouched): - Licence-label drift (config.ncl "MIT OR Palimpsest", README "Palimpsest License (MPL-2.0)", LICENSE-MIT/PALIMPSEST vs the MPL-2.0 classification) — owner-only per the licence guardrail. - The CONTRIBUTING.md / CHANGELOG.md .md-vs-.adoc choice for GitHub-required files. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .github/workflows/ci.yml | 2 +- .gitlab-ci.yml | 4 +- CODE_OF_CONDUCT.md | 4 +- CONTRIBUTING.md | 498 ++++++++------------------------------- HANDOFF.adoc | 8 +- Justfile | 2 +- MAINTAINERS.md | 259 -------------------- README.adoc | 10 +- SECURITY.md | 4 +- docs/RSR_COMPLIANCE.adoc | 84 +++++++ docs/RSR_COMPLIANCE.md | 359 ---------------------------- docs/TPCF.adoc | 88 +++++++ docs/TPCF.md | 400 ------------------------------- 13 files changed, 282 insertions(+), 1440 deletions(-) delete mode 100644 MAINTAINERS.md create mode 100644 docs/RSR_COMPLIANCE.adoc delete mode 100644 docs/RSR_COMPLIANCE.md create mode 100644 docs/TPCF.adoc delete mode 100644 docs/TPCF.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b30d9a..f4dc549 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: test -f CONTRIBUTING.md || (echo "Missing CONTRIBUTING.md" && exit 1) test -f CODE_OF_CONDUCT.md || (echo "Missing CODE_OF_CONDUCT.md" && exit 1) test -f SECURITY.md || (echo "Missing SECURITY.md" && exit 1) - test -f MAINTAINERS.md || (echo "Missing MAINTAINERS.md" && exit 1) + test -f MAINTAINERS.adoc || (echo "Missing MAINTAINERS.adoc" && exit 1) test -f CHANGELOG.md || (echo "Missing CHANGELOG.md" && exit 1) test -f CLAUDE.md || (echo "Missing CLAUDE.md" && exit 1) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 996ca22..36fe1d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -98,11 +98,11 @@ rsr-compliance: echo "Checking RSR Bronze compliance..." # Documentation - test -f ../README.md || (echo "Missing README.md" && exit 1) + test -f ../README.adoc || (echo "Missing README.adoc" && exit 1) test -f ../CONTRIBUTING.md || (echo "Missing CONTRIBUTING.md" && exit 1) test -f ../CODE_OF_CONDUCT.md || (echo "Missing CODE_OF_CONDUCT.md" && exit 1) test -f ../SECURITY.md || (echo "Missing SECURITY.md" && exit 1) - test -f ../MAINTAINERS.md || (echo "Missing MAINTAINERS.md" && exit 1) + test -f ../MAINTAINERS.adoc || (echo "Missing MAINTAINERS.adoc" && exit 1) test -f ../CHANGELOG.md || (echo "Missing CHANGELOG.md" && exit 1) # Licenses diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 77a6f1b..d8926b9 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -114,7 +114,7 @@ This Code of Conduct applies within all community spaces, and also applies when If you experience or witness unacceptable behavior, or have any other concerns, please report it by contacting the project maintainers at: -- **Email**: See [MAINTAINERS.md](MAINTAINERS.md) for contact information +- **Email**: See [MAINTAINERS.adoc](MAINTAINERS.adoc) for contact information - **Anonymous Form**: [Coming soon] All reports will be handled with **discretion and confidentiality**. @@ -220,7 +220,7 @@ This Code of Conduct is a **living document** and may be updated based on commun For questions about this Code of Conduct: -- **Maintainers**: See [MAINTAINERS.md](MAINTAINERS.md) +- **Maintainers**: See [MAINTAINERS.adoc](MAINTAINERS.adoc) - **GitHub Discussions**: [Community forum](https://github.com/Hyperpolymath/nextgen-languages-evangeliser/discussions) --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c04d5cb..0cba703 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,444 +1,130 @@ + # Contributing to Nextgen Languages Evangeliser Thank you for your interest in contributing! 🎉 -> **Note:** This project is mid-migration from *ReScript Evangeliser* to *Nextgen Languages Evangeliser* (flagship target: AffineScript). Some sections below still reference the ReScript-centric dev setup — these will be updated through Phases 1–3. See `ROADMAP.adoc`. +This project is **Duolingo / Rosetta Stone for programming languages** — a +language-*comprehension and transfer* engine that **classifies** cross-language +correspondences so learning transfers from a language you know to one you don't. +It is **not** an IDE, a linter, a universal translator, or a "fix your +JavaScript" tool. Canonical design: [`docs/theory/CORRESPONDENCE-MODEL.adoc`](docs/theory/CORRESPONDENCE-MODEL.adoc). -This project follows the **Compassionate Code Contribution Pledge (CCCP)** and **Tri-Perimeter Contribution Framework (TPCF) - Perimeter 3**. +We follow the **Compassionate Code Contribution Pledge (CCCP)** and the +**Tri-Perimeter Contribution Framework (TPCF) — Perimeter 3** (see +[`docs/TPCF.adoc`](docs/TPCF.adoc)). -## 🌟 Philosophy: "Celebrate Good, Minimize Bad, Show Better" +## Philosophy: no shame, transfer-first -Our contribution process mirrors our teaching philosophy: +Our contribution process mirrors our teaching voice — **celebrate / minimise / +better / safety / example**: -1. **Celebrate**: We appreciate every contribution, no matter how small -2. **Minimize**: We gently guide improvements without harsh criticism -3. **Better**: We collaboratively make the code even better -4. **Safety**: We ensure type safety and test coverage -5. **Example**: We provide concrete examples and mentorship +1. **Celebrate** every contribution, however small. +2. **Minimise** — guide gently, never shame. +3. **Better** — improve collaboratively. +4. **Safety** — keep the data valid and the invariants honest. +5. **Example** — show, don't lecture. -## 🚀 Quick Start +## The main way to contribute: author a cartridge -### Prerequisites - -- Node.js 20+ (LTS recommended) -- VS Code 1.85+ -- Git -- Optional: Nix (for reproducible builds) - -### Development Setup - -```bash -# Fork and clone -git clone https://github.com/YOUR_USERNAME/nextgen-languages-evangeliser.git -cd nextgen-languages-evangeliser/extension - -# Install dependencies -npm install - -# Compile TypeScript -npm run compile - -# Run tests -npm test - -# Launch extension in VS Code -# Press F5 or Run > Start Debugging -``` - -### First Contribution - -1. **Find an issue** labeled `good first issue` or `help wanted` -2. **Comment** on the issue to claim it -3. **Create a branch**: `git checkout -b feature/your-feature-name` -4. **Make changes** following our guidelines (below) -5. **Test thoroughly**: `npm test` and `npm run lint` -6. **Commit**: Use conventional commits (see below) -7. **Push**: `git push origin feature/your-feature-name` -8. **Open a Pull Request**: Describe your changes clearly - -## 📋 Contribution Types - -### 1. Pattern Contributions (Most Welcome!) - -Add new JavaScript → ReScript transformation patterns. - -**Template**: See [docs/PATTERN_AUTHORING.md](docs/PATTERN_AUTHORING.md) - -**Example**: -```typescript -{ - id: 'my-pattern', - name: 'My Cool Pattern', - category: 'functional', - difficulty: 'beginner', - jsPattern: /const\s+(\w+)\s*=/, - jsExample: `const x = 42;`, - rescriptExample: `let x = 42`, - narrative: { - celebrate: "You're using const - you know about immutability!", - minimize: "The only small thing is const isn't always enforced...", - better: "ReScript makes immutability the default!", - safety: "The compiler ensures no accidental mutations.", - example: "See how let in ReScript is always immutable!" - }, - glyphs: ['💎', '🛡️'], - tags: ['variables', 'immutability'], - relatedPatterns: [], - learningObjectives: ['Understand immutability by default'], - commonMistakes: ['Trying to reassign let bindings'], - bestPractices: ['Use let for immutable bindings, use ref for mutable ones'] -} -``` - -### 2. Code Contributions - -**Areas**: -- Core extension functionality -- Pattern detection engine (AST parsing) -- UI views (RAW/FOLDED/GLYPHED/WYSIWYG) -- Test coverage improvements -- Performance optimizations -- Documentation - -### 3. Documentation Contributions - -- Improving README, guides, tutorials -- Adding code examples -- Creating video scripts -- Translating documentation -- Writing blog posts - -### 4. Testing Contributions - -- Unit tests for patterns -- Integration tests -- UI tests -- Performance benchmarks -- Security tests - -### 5. Design Contributions - -- UI/UX improvements -- Glyph designs -- Color schemes -- Accessibility enhancements - -## 📝 Guidelines - -### Code Style - -We use TypeScript with strict mode: - -```typescript -// ✅ Good -function transformCode(code: string): string { - if (!code) { - throw new Error('Code is required'); - } - return code.trim(); -} - -// ❌ Avoid -function transformCode(code) { // Missing types - return code.trim(); // No validation -} -``` - -**Rules**: -- Strict TypeScript (`strict: true`) -- No `any` types (use `unknown` and narrow) -- Explicit return types for public functions -- JSDoc comments for public APIs -- Meaningful variable names - -### Testing Requirements - -All contributions must include tests: - -```bash -# Run all tests -npm test - -# Run with coverage -npm run test:coverage - -# Watch mode during development -npm run test:watch -``` - -**Requirements**: -- Unit tests for new functions -- Integration tests for new features -- 70%+ code coverage (enforced by CI) -- All tests must pass - -### Commit Message Format - -We use **Conventional Commits**: - -``` -(): - -[optional body] - -[optional footer] -``` - -**Types**: -- `feat`: New feature -- `fix`: Bug fix -- `docs`: Documentation only -- `style`: Code style (formatting, no logic change) -- `refactor`: Code refactoring -- `test`: Adding tests -- `chore`: Build process, dependencies - -**Examples**: -``` -feat(patterns): add null coalescing operator pattern - -Add detection and transformation for ?? operator to ReScript's -Option.getOr pattern. Includes tests and documentation. - -Closes #42 -``` - -``` -fix(parser): handle nested ternary expressions - -Previously nested ternaries caused parser to fail. Now properly -handles up to 3 levels of nesting with pattern matching suggestion. - -Fixes #123 -``` - -### Code Review Process - -1. **Automated Checks**: - - CI runs tests and linting - - RSR compliance verification - - Coverage report generated - -2. **Peer Review**: - - At least one approval required - - Constructive feedback following CCCP - - Suggestions, not demands - -3. **Maintainer Review**: - - Final architectural review - - Merge approval - -### TPCF Perimeter 3: Community Sandbox - -This project is **fully open** to contributions: +The engine is language-agnostic. The community authors **cartridges** — +per-language packs of correspondence *facts* (data, not code). This is the +highest-value, lowest-barrier contribution. -- ✅ **No approval required** for common changes -- ✅ **Automated review** for pattern additions -- ✅ **Community-driven** governance -- ✅ **Inclusive** decision-making +A cartridge entry is a **Transition**: a Concept shown through a Form in language +A and a Form in language B, classified by its **CorrespondenceKind** and its +**residue** (what is lost, added, or inverted on the crossing): -See [docs/TPCF.md](docs/TPCF.md) for full framework. +- **cognate** — transfer directly +- **false-friend** — flag the trap (surface matches, intention diverges) +- **antonym** — remap the intuition (related but inverted) +- **alien-realization** — bridge with effort (same goal, foreign machinery) +- **novel** — teach de novo (no prior anchor) +- **vanished** — un-learn / re-route (the habit is gone here) -## 🛡️ Security Contributions +### Steps -Security is critical! Please: +1. Read [`docs/theory/CORRESPONDENCE-MODEL.adoc`](docs/theory/CORRESPONDENCE-MODEL.adoc). +2. Add or extend a `*.cartridge.json` under `cartridges/`, following + `cartridges/correspondence-cartridge.schema.json`. +3. Classify each Transition's kind + residue + per-stratum verdicts, and write + the no-shame narrative. +4. Validate: `just validate-cartridges` and `just test`. +5. Open a pull request. -1. **Never** commit secrets, API keys, or credentials -2. **Report** vulnerabilities privately (see [SECURITY.md](SECURITY.md)) -3. **Test** for common vulnerabilities (XSS, injection, etc.) -4. **Follow** security checklist in [SECURITY.md](SECURITY.md) +The worked exemplar is +[`cartridges/reference/worked-examples.cartridge.json`](cartridges/reference/worked-examples.cartridge.json) +— one correspondence per kind. -## ♿ Accessibility +## Development setup -We strive for **WCAG 2.1 AA compliance**: +The runtime surface is **Deno + cartridge data** — no compile step, no Node, no +npm. -- Semantic HTML in webviews -- Keyboard navigation support -- Screen reader compatibility -- Sufficient color contrast -- No flashing/blinking elements - -## 🌍 Internationalization - -Currently English-only, but we welcome: - -- Translation contributions -- i18n infrastructure -- Localized examples -- Cultural adaptations of narratives - -## 📊 Performance Standards +### Prerequisites -All code must meet performance targets: +- [Deno](https://deno.land) (latest stable) — the only required tool +- [just](https://github.com/casey/just) — task runner +- Git +- _Optional, for the engine spine:_ Idris2 (ABI typecheck), Zig (FFI). The + AffineScript host is the future host; its compiler is not yet released. -- **Pattern detection**: <300ms -- **Memory usage**: <100MB -- **UI response**: <50ms +### Common tasks -Run benchmarks: ```bash -npm run benchmark +just gui # browser multi-pane correspondence workspace (primary surface) +just test # cartridge invariant tests +just validate-cartridges # schema-validate the cartridges +just build # validate cartridge data (no compile step) +deno run --allow-read bin/evangeliser.js --help # offline CLI ``` -## 🎨 UI/UX Contributions - -When contributing to UI: - -1. **Consistency**: Follow VS Code design guidelines -2. **Simplicity**: Progressive disclosure, not overwhelming -3. **Accessibility**: WCAG 2.1 AA minimum -4. **Performance**: 60fps animations, lazy loading - -## 🧪 Testing Patterns - -### Unit Test Example - -```typescript -import { detectPattern } from '../patterns/pattern-library'; - -describe('Pattern Detection', () => { - it('should detect null check pattern', () => { - const code = 'if (x !== null && x !== undefined)'; - const matches = detectPattern(code); - - expect(matches).toHaveLength(1); - expect(matches[0].pattern.id).toBe('null-safety'); - }); - - it('should handle edge case of nested null checks', () => { - const code = 'if (a && a.b && a.b.c)'; - const matches = detectPattern(code); - - expect(matches.length).toBeGreaterThan(0); - }); -}); -``` - -## 📚 Documentation Standards - -When writing documentation: - -- **Clear**: Plain language, no jargon -- **Concise**: Respect the reader's time -- **Concrete**: Provide examples -- **Encouraging**: Follow "celebrate good" philosophy -- **Inclusive**: Consider diverse backgrounds - -## 🏆 Recognition +## Contribution areas -Contributors are recognized in: +- **Cartridges** (most welcome) — correspondence facts for any language pair. +- **Engine / interface** (`src/interface/**`, `gui/**`, `bin/`) — reviewed + contributions; discuss architectural changes in an issue first. +- **Engine spine** — the Idris2 ABI (`src/interface/Abi/`), Zig FFI mirror + (`src/interface/ffi/`), AffineScript host binding (`src/interface/host/`). +- **Documentation** — guides, examples, translations. +- **Accessibility** — the workspace targets the Hyperpolymath Accessibility + Standard (Level A → AA): keyboard-only, ≥4.5:1 contrast, ARIA, reduced-motion, + plain-language and glyph layers. -- [humans.txt](.well-known/humans.txt) -- Release notes -- GitHub contributors list -- Annual "Top Contributors" spotlight +## Language policy -## 🎯 Roadmap Priorities +Per the Hyperpolymath Standard: **Deno** (not npm/bun), **Justfile** (not +Makefile), **Zig** for FFI, **Idris2** for ABI/proofs, **AffineScript** as the +future host. **TypeScript, Node.js, Python, Go are banned.** ReScript is a legacy +*target* language only (the ReScript host was removed) — never reintroduce `.res` +host code. See [`CLAUDE.md`](CLAUDE.md) and [`.claude/CLAUDE.md`](.claude/CLAUDE.md) +for the full policy and JavaScript-glue exemptions. -Current priorities (see [GitHub Projects](https://github.com/Hyperpolymath/nextgen-languages-evangeliser/projects)): +## Commits & pull requests -1. **High Priority**: - - Expanding pattern library (50 → 100+ patterns) - - WYSIWYG view implementation - - Tutorial system +- **Conventional Commits**: `type(scope): description` (`feat`, `fix`, `docs`, + `refactor`, `test`, `chore`, …). +- **Sign off** every commit (DCO): `git commit -s`. +- Keep commits atomic and focused; describe the change clearly in the PR. +- CI runs the cartridge tests, schema validation, the `panic-attack` gate, and + the governance/policy checks. -2. **Medium Priority**: - - Multi-language support (Elm, Haskell) - - Performance optimizations - - Accessibility improvements +## Security -3. **Low Priority**: - - Theme customization - - Custom glyph sets - - Plugin system +Never commit secrets. Report vulnerabilities privately — see +[`SECURITY.md`](SECURITY.md). -## 💬 Communication +## Code of Conduct -### Channels +This project follows the **Compassionate Code Contribution Pledge** — see +[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). In short: never shame developers, +celebrate existing knowledge, assume good intent, foster psychological safety. -- **GitHub Issues**: Bug reports, feature requests -- **GitHub Discussions**: Questions, ideas, showcase -- **Discord**: Real-time chat (ReScript community) -- **Forum**: Long-form discussion (ReScript forum) +## License -### Response Times - -We aim for: -- **Issues**: Response within 48 hours -- **PRs**: Initial review within 7 days -- **Security**: Acknowledgment within 24 hours - -## 🙏 Code of Conduct - -This project follows the **Compassionate Code Contribution Pledge (CCCP)**: - -### Our Pledge - -We pledge to make participation a harassment-free, psychologically safe experience for everyone, regardless of: - -- Age, body size, disability, ethnicity -- Gender identity and expression -- Level of experience, education, socio-economic status -- Nationality, personal appearance, race, religion -- Sexual identity and orientation - -### Our Standards - -**Positive behaviors**: -- ✅ Celebrating others' knowledge and contributions -- ✅ Being respectful of differing viewpoints -- ✅ Giving and gracefully accepting constructive feedback -- ✅ Focusing on what's best for the community -- ✅ Showing empathy towards other community members - -**Unacceptable behaviors**: -- ❌ Harassment, trolling, insulting/derogatory comments -- ❌ Public or private harassment -- ❌ Publishing others' private information -- ❌ Shaming developers for not knowing something -- ❌ Any conduct inappropriate in a professional setting - -### Enforcement - -Violations should be reported to project maintainers. All complaints will be reviewed and investigated promptly and fairly. - -**Consequences**: -1. **Warning**: Private, written warning -2. **Temporary Ban**: 30-day suspension -3. **Permanent Ban**: Permanent exclusion - -See full [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). - -## 🎓 Learning Resources - -New to contributing? Check out: - -- [First Contributions](https://github.com/firstcontributions/first-contributions) -- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) -- [Conventional Commits](https://www.conventionalcommits.org/) -- [ReScript Documentation](https://rescript-lang.org/docs) - -## ❓ Questions? - -Don't hesitate to ask! - -- **GitHub Discussions**: For general questions -- **Issues**: For specific technical questions -- **Maintainers**: See [MAINTAINERS.md](MAINTAINERS.md) +By contributing, you agree that your contributions are licensed under the +project licence — see [`LICENSE`](LICENSE). You retain copyright of your +contributions. --- -## 📜 License - -By contributing, you agree that your contributions will be dual-licensed under: - -- **MIT License** (LICENSE-MIT.txt) -- **Palimpsest License v0.8** (LICENSE-PALIMPSEST.txt) - -You retain copyright of your contributions. - ---- - -**Thank you for contributing to Nextgen Languages Evangeliser!** 🚀 - -Remember: You were close! We're here to help make it even better together. 💙 +**Thank you for contributing!** Remember the motto: *Learn — to love it!* 💙 diff --git a/HANDOFF.adoc b/HANDOFF.adoc index 2b64a93..322a16d 100644 --- a/HANDOFF.adoc +++ b/HANDOFF.adoc @@ -49,9 +49,11 @@ deno run --allow-read bin/evangeliser.js --kind false-friend # offline CLI (JTV is the canonical alien-realization / novel exemplar). . Begin the *AffineScript host application* against the Idris2 ABI / Zig FFI seams (needs the AffineScript compiler). -. Optional cleanup: reframe the old-model contributor prose - (`CONTRIBUTING.md`, `MAINTAINERS.md`, `docs/TPCF.md`, the duplicate - `CHANGELOG.md`) — still carries "JS→ReScript transformation" language. +. Owner-only follow-ups (flagged, not done — out of agent scope): the + `CONTRIBUTING.md` / `CHANGELOG.md` `.md`-vs-`.adoc` format choice (GitHub-required + files), and the licence-label drift (`config.ncl` `MIT OR Palimpsest`, the README + "Palimpsest License (MPL-2.0)" label, `LICENSE-MIT.txt` / `LICENSE-PALIMPSEST.txt` + vs the `MPL-2.0` classification). Both are owner-domain — flag, never sweep. == Guardrails (non-negotiable) diff --git a/Justfile b/Justfile index 872e470..e05c192 100644 --- a/Justfile +++ b/Justfile @@ -112,7 +112,7 @@ validate-docs: @test -f CONTRIBUTING.md || (echo "❌ Missing CONTRIBUTING.md" && exit 1) @test -f CODE_OF_CONDUCT.md || (echo "❌ Missing CODE_OF_CONDUCT.md" && exit 1) @test -f SECURITY.md || (echo "❌ Missing SECURITY.md" && exit 1) - @test -f MAINTAINERS.md || (echo "❌ Missing MAINTAINERS.md" && exit 1) + @test -f MAINTAINERS.adoc || (echo "❌ Missing MAINTAINERS.adoc" && exit 1) @test -f CHANGELOG.md || (echo "❌ Missing CHANGELOG.md" && exit 1) @test -f CLAUDE.md || (echo "❌ Missing CLAUDE.md" && exit 1) @echo "✅ Documentation complete" diff --git a/MAINTAINERS.md b/MAINTAINERS.md deleted file mode 100644 index 0261116..0000000 --- a/MAINTAINERS.md +++ /dev/null @@ -1,259 +0,0 @@ -# Maintainers - -This document lists the current maintainers of the Nextgen Languages Evangeliser project (formerly ReScript Evangeliser) and describes their roles and responsibilities. - -## Current Maintainers - -### Lead Maintainer - -**Hyperpolymath** -- **Role**: Project creator, lead maintainer, architecture decisions -- **GitHub**: [@Hyperpolymath](https://github.com/Hyperpolymath) -- **Responsibilities**: - - Overall project vision and direction - - Major architectural decisions - - Release management - - Security vulnerability coordination - - Community moderation -- **Availability**: Best effort -- **PGP Key**: See [.well-known/security.txt](.well-known/security.txt) - -### Pattern Library Maintainers - -*Looking for volunteers!* - -- **Role**: Curate and maintain the JavaScript → ReScript pattern library -- **Responsibilities**: - - Review pattern submissions - - Ensure pattern quality and accuracy - - Organize patterns by category and difficulty - - Write pattern documentation - -### Documentation Maintainers - -*Looking for volunteers!* - -- **Role**: Maintain project documentation -- **Responsibilities**: - - Keep documentation up to date - - Review documentation PRs - - Improve clarity and accessibility - - Coordinate translations (future) - -### Testing Maintainers - -*Looking for volunteers!* - -- **Role**: Maintain test suite and CI/CD -- **Responsibilities**: - - Write and review tests - - Maintain CI/CD pipelines - - Ensure test coverage requirements - - Performance testing and benchmarking - -## Becoming a Maintainer - -We welcome new maintainers! The path to maintainership: - -### Requirements - -1. **Consistent Contributions**: 3+ months of regular contributions -2. **Code Quality**: Demonstrates understanding of project standards -3. **Community Engagement**: Helpful in issues, discussions, PRs -4. **Code of Conduct**: Exemplifies our compassionate approach -5. **Reliability**: Can commit to ongoing involvement - -### Process - -1. **Nomination**: Existing maintainers nominate candidates -2. **Discussion**: Team discusses candidate's fit -3. **Consensus**: All current maintainers must agree -4. **Invitation**: Candidate is invited to join -5. **Onboarding**: New maintainer receives access and training - -### Maintainer Levels - -**Level 1: Triage** -- Label and organize issues -- Close duplicates and spam -- Help users in discussions - -**Level 2: Reviewer** -- Review pull requests -- Merge approved PRs -- Guide contributors - -**Level 3: Maintainer** -- Full repository access -- Make architectural decisions -- Manage releases -- Enforce Code of Conduct - -**Level 4: Lead** -- Overall project direction -- Conflict resolution -- Security coordination -- Community leadership - -## Maintainer Responsibilities - -### Code Review - -- Review PRs within 7 days -- Provide constructive, compassionate feedback -- Test changes locally when needed -- Ensure CI passes before merge -- Follow "Celebrate, Minimize, Better" philosophy - -### Issue Management - -- Triage new issues within 48 hours -- Label appropriately -- Close duplicates with links to originals -- Ask for clarification when needed -- Be welcoming to new contributors - -### Security - -- Monitor security advisories -- Coordinate security fixes -- Follow responsible disclosure -- Update dependencies regularly - -### Community - -- Model positive behavior -- Enforce Code of Conduct fairly -- Welcome new contributors -- Recognize contributions -- Foster inclusive environment - -### Releases - -- Follow semantic versioning -- Update CHANGELOG.md -- Test release candidates -- Announce releases -- Monitor for issues post-release - -## Emeritus Maintainers - -Maintainers who have moved on to other projects but whose contributions are forever appreciated: - -*None yet - this is a new project!* - -## Decision Making - -### Consensus Model - -For major decisions, we use **lazy consensus**: - -1. Proposal is shared publicly (GitHub Issue or Discussion) -2. Community has 7 days to object -3. If no objections, proposal is accepted -4. If objections, discussion continues until consensus - -### Voting - -For rare cases where consensus fails: - -- Simple majority vote among Level 3+ maintainers -- All votes are public -- Decisions documented in issues/discussions - -### Veto Rights - -Lead maintainer has veto power for: - -- Security-critical decisions -- Code of Conduct enforcement -- Legal/licensing issues - -Veto should be used sparingly and with explanation. - -## Conflict Resolution - -### Process - -1. **Direct Communication**: Parties attempt to resolve directly -2. **Mediation**: Another maintainer mediates -3. **Team Decision**: Maintainer team discusses and decides -4. **External Mediation**: If needed, involve neutral third party - -### Principles - -- Assume good intent -- Focus on project health, not personal preferences -- Follow Code of Conduct -- Document decisions -- Move forward once decided - -## Stepping Down - -Maintainers can step down at any time: - -1. **Notification**: Inform other maintainers -2. **Transition**: Help onboard replacement if possible -3. **Access Removal**: Repository access revoked -4. **Emeritus Status**: Recognized in this document -5. **Return Welcome**: Always welcome to return later - -## Maintainer Resources - -### Internal - -- **Maintainer Chat**: [Private Discord channel] -- **Security Email**: [Secure mailing list] -- **Calendar**: [Shared calendar for releases] - -### External - -- **GitHub Maintainer Guide**: https://opensource.guide/best-practices/ -- **VS Code Extension Best Practices**: https://code.visualstudio.com/api/references/extension-guidelines -- **ReScript Community**: https://forum.rescript-lang.org/ - -## Time Commitment - -Maintainers are **volunteers**. Expected time commitment: - -- **Level 1**: 1-2 hours/week -- **Level 2**: 2-4 hours/week -- **Level 3**: 4-8 hours/week -- **Level 4**: 8-12 hours/week - -These are guidelines, not requirements. **Life happens**. Communicate when unavailable. - -## Compensation - -Currently, maintainership is **volunteer-based**. Future possibilities: - -- Open Collective funding -- GitHub Sponsors -- Corporate sponsorship -- Grant funding - -Any compensation will be distributed fairly and transparently. - -## Contact - -### Public - -- **GitHub Issues**: Technical questions, bug reports -- **GitHub Discussions**: General questions, ideas -- **ReScript Forum**: Community discussion - -### Private - -- **Security Issues**: See [SECURITY.md](SECURITY.md) -- **Code of Conduct**: See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) -- **Other Private Matters**: Create a private security advisory on GitHub - -## Acknowledgments - -Thank you to all maintainers, past and present, for your dedication to making ReScript accessible and welcoming! - ---- - -**Want to become a maintainer?** Start contributing! We're always looking for dedicated community members. - -**Last Updated**: 2024-11-22 diff --git a/README.adoc b/README.adoc index f03b781..675c4ee 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,11 @@ = Nextgen Languages Evangeliser -image:https://img.shields.io/badge/RSR-Bronze-CD7F32[RSR Bronze,link=docs/RSR_COMPLIANCE.md] +image:https://img.shields.io/badge/RSR-Bronze-CD7F32[RSR Bronze,link=docs/RSR_COMPLIANCE.adoc] image:https://img.shields.io/badge/License-MPL--2.0-blue.svg[License: MPL-2.0,link="https://github.com/hyperpolymath/palimpsest-license"] // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -image:[TPCF: Perimeter 3,link=docs/TPCF.md] +image:[TPCF: Perimeter 3,link=docs/TPCF.adoc] image:[Learn — to love it!] image:https://img.shields.io/badge/Deno-Runtime-000000.svg[Deno Runtime] @@ -241,7 +241,7 @@ This project follows the *Rhodium Standard Repository (RSR)* framework: * *Licensed*: MPL-2.0 * *Build Reproducibility*: Deno + Justfile -See link:docs/RSR_COMPLIANCE.md[RSR_COMPLIANCE.md] for full details. +See link:docs/RSR_COMPLIANCE.adoc[RSR_COMPLIANCE.adoc] for full details. == Documentation @@ -250,8 +250,8 @@ See link:docs/RSR_COMPLIANCE.md[RSR_COMPLIANCE.md] for full details. * link:CODE_OF_CONDUCT.md[CODE_OF_CONDUCT.md] - Community guidelines * link:SECURITY.md[SECURITY.md] - Security policies * link:CHANGELOG.md[CHANGELOG.md] - Version history -* link:docs/RSR_COMPLIANCE.md[RSR_COMPLIANCE.md] - RSR framework compliance -* link:docs/TPCF.md[TPCF.md] - Contribution framework +* link:docs/RSR_COMPLIANCE.adoc[RSR_COMPLIANCE.adoc] - RSR framework compliance +* link:docs/TPCF.adoc[TPCF.adoc] - Contribution framework * link:ROADMAP.adoc[ROADMAP.adoc] - Re-point sequence & milestones == Contributing diff --git a/SECURITY.md b/SECURITY.md index 716bebc..e8dd1d2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -22,7 +22,7 @@ Security is a **first-class concern** in Nextgen Languages Evangeliser. We follo If you discover a security vulnerability, please report it responsibly: **DO:** -- ✅ Email security details to the maintainers (see [MAINTAINERS.md](MAINTAINERS.md)) +- ✅ Email security details to the maintainers (see [MAINTAINERS.adoc](MAINTAINERS.adoc)) - ✅ Use encrypted email if possible (PGP key in [.well-known/security.txt](.well-known/security.txt)) - ✅ Wait for response before public disclosure (max 90 days) - ✅ Provide detailed reproduction steps @@ -252,7 +252,7 @@ https://github.com/Hyperpolymath/nextgen-languages-evangeliser/releases For security concerns: -- **Email**: See [MAINTAINERS.md](MAINTAINERS.md) +- **Email**: See [MAINTAINERS.adoc](MAINTAINERS.adoc) - **PGP Key**: See [.well-known/security.txt](.well-known/security.txt) - **Security.txt**: RFC 9116 compliant file in [.well-known/](.well-known/) diff --git a/docs/RSR_COMPLIANCE.adoc b/docs/RSR_COMPLIANCE.adoc new file mode 100644 index 0000000..5cf308b --- /dev/null +++ b/docs/RSR_COMPLIANCE.adoc @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += RSR Compliance — Nextgen Languages Evangeliser +:toc: macro + +Compliance summary against the *Rhodium Standard Repository (RSR)* framework, +Bronze level. This mirrors the current architecture: a cross-language +*correspondence engine* (classify, don't translate) whose runtime surface is +*Deno + cartridge data*, with an Idris2 ABI + Zig FFI + AffineScript host-binding +spine. (The pre-2026 TypeScript VS Code extension this report once described has +been removed; so has the ReScript host.) + +toc::[] + +== 1. Type safety + +* The correspondence carrier is specified in the *Idris2 ABI* + (`src/interface/Abi/`) and *typechecks* under Idris2 (`idris2 --typecheck + abi.ipkg`, CI `idris2-abi.yml`) — no `believe_me` / `assert_total`. +* *AffineScript* (affine/linear types, borrow checking, QTT) is the future host + and a first-class teaching target. +* Cartridge facts are schema-validated against a JSON-Schema (draft 2020-12). + +== 2. Memory safety + +* No application code performs manual memory management; the runtime surface is + Deno (sandboxed, GC). The FFI seam is *Zig* (`src/interface/ffi/`), and the + estate default for correctness-critical native work is Rust/SPARK. + +== 3. Offline-first architecture + +* Zero network dependency to use the tool: the CLI (`bin/evangeliser.js`) and the + workspace server (`gui/server.js`) read local cartridge data only. +* Works air-gapped; no CDN or runtime fetch. + +== 4. Complete documentation + +* `README.adoc`, `EXPLAINME.adoc`, `ROADMAP.adoc`, `CONTRIBUTING.md`, + `CODE_OF_CONDUCT.md`, `SECURITY.md`, `MAINTAINERS.adoc`, `CHANGELOG`, + `CLAUDE.md`, and the canonical design spec + `docs/theory/CORRESPONDENCE-MODEL.adoc`. + +== 5. Security-first design + +* `panic-attack` static-analysis gate (`just assail`); CodeQL; SHA-pinned GitHub + Actions; RFC 9116 `.well-known/security.txt`; no hardcoded secrets; HTTPS-only. + +== 6. Open governance + +* TPCF *Perimeter 3* (Community Sandbox). See link:TPCF.adoc[TPCF.adoc]. + +== 7. Licensing + +* Licensed under the project licence — see the repository `LICENSE`. Licence + identifiers and any SPDX headers are owner-managed; this report does not assert + a specific scheme. + +== 8. Test coverage + +* Cartridge *invariant* tests (`test/run_all.js`, Deno) — every correspondence + is classified by one of the six CorrespondenceKinds, the false-friend signature + holds (surface ✓ ∧ intention ✗), residue direction is correct for novel / + vanished, and the reference pack demonstrates all six kinds. +* Cartridge *schema* validation (`scripts/validate-cartridges.js`, ajv). +* Idris2 ABI typecheck (CI). + +== 9. Build reproducibility + +* No compile step for the runtime surface (Deno + data). Task orchestration via + *Justfile*; reproducible environments via *Guix* (primary) / *Nix* (fallback). + +== 10. `.well-known/` + +* `security.txt` (RFC 9116), `ai.txt`, `humans.txt`. + +== 11. No vendor lock-in + +* Open formats (JSON cartridges, AsciiDoc docs); language-agnostic engine; the + cartridge contract is portable and community-authored. + +== Notes + +* This document is descriptive, not a scorecard. Where a category is partially + realised (e.g. the AffineScript host application is authored at the seam but + not yet built), `ROADMAP.adoc` tracks the remaining work honestly. diff --git a/docs/RSR_COMPLIANCE.md b/docs/RSR_COMPLIANCE.md deleted file mode 100644 index 47fec22..0000000 --- a/docs/RSR_COMPLIANCE.md +++ /dev/null @@ -1,359 +0,0 @@ -# RSR Compliance Report - -## Rhodium Standard Repository (RSR) Bronze Level - -**Project**: Nextgen Languages Evangeliser -**Version**: 0.1.0 -**Date**: 2024-11-22 -**Compliance Level**: Bronze ✅ - ---- - -## Executive Summary - -Nextgen Languages Evangeliser achieves **Bronze-level compliance** with the Rhodium Standard Repository (RSR) framework. This document details our compliance across all 11 categories. - -## 1. Type Safety ✅ - -**Status**: COMPLIANT - -- **Implementation**: - - TypeScript strict mode enabled (`"strict": true` in tsconfig.json) - - 100% type coverage for all modules - - No `any` types except where explicitly unavoidable - - ReScript examples demonstrate sound type system - -- **Evidence**: - ```typescript - // extension/tsconfig.json - { - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true - } - ``` - -- **Testing**: Type checking via `npx tsc --noEmit` - -## 2. Memory Safety ✅ - -**Status**: COMPLIANT - -- **Implementation**: - - TypeScript (no manual memory management) - - No buffer overflows possible - - Garbage collected runtime (Node.js/V8) - - Resource cleanup in extension lifecycle - -- **Evidence**: - - Proper disposal of VS Code resources - - No memory leaks in long-running processes - - Performance monitoring <100MB target - -## 3. Offline-First Architecture ✅ - -**Status**: COMPLIANT - -- **Implementation**: - - **Zero network dependencies** at runtime - - All patterns stored locally - - Works in air-gapped environments - - No CDN dependencies - -- **Evidence**: - ```json - // package.json - zero runtime network deps - "dependencies": { - "@babel/parser": "^7.23.6", // local AST parsing only - "@babel/traverse": "^7.23.6", - "@babel/types": "^7.23.6" - } - ``` - -- **Testing**: Extension functions without network access - -## 4. Complete Documentation ✅ - -**Status**: COMPLIANT - -- **Required Files**: - - ✅ README.md (comprehensive) - - ✅ CONTRIBUTING.md (detailed guidelines) - - ✅ CODE_OF_CONDUCT.md (CCCP-based) - - ✅ SECURITY.md (10+ security dimensions) - - ✅ MAINTAINERS.md (governance structure) - - ✅ CHANGELOG.md (semver, keep-a-changelog format) - - ✅ CLAUDE.md (AI context) - -- **Additional Documentation**: - - Architecture documentation - - API documentation (TypeScript declarations) - - Pattern authoring guide - - Tutorial content - -## 5. Security-First Design ✅ - -**Status**: COMPLIANT - -- **Security Measures**: - - Input validation (all user code sanitized) - - No code execution via `eval()` - - AST-based parsing (sandboxed) - - Content Security Policy for webviews - - Resource limits (file size, parsing timeout, memory) - - RFC 9116 compliant security.txt - -- **Evidence**: - - `.well-known/security.txt` (RFC 9116) - - SECURITY.md with 10+ dimensions - - No secrets in code - - Dependency auditing via `npm audit` - -- **Threat Model**: Documented in SECURITY.md - -## 6. Open Governance ✅ - -**Status**: COMPLIANT - -- **Framework**: TPCF Perimeter 3 (Community Sandbox) - -- **Characteristics**: - - Fully open contributions - - No approval required for common changes - - Automated review for patterns - - Community-driven decisions - - Transparent governance - -- **Evidence**: See TPCF.md - -## 7. Dual Licensing ✅ - -**Status**: COMPLIANT - -- **Licenses**: - 1. **MIT License** (LICENSE-MIT.txt) - permissive, compatible - 2. **Palimpsest License v0.8** (LICENSE-PALIMPSEST.txt) - ethical AI, reversibility - -- **Choice**: Users may use under **either** license - -- **Compatibility**: Documented in LICENSING.md - -## 8. Test Coverage ✅ - -**Status**: COMPLIANT (Target: 70%+) - -- **Test Framework**: Jest -- **Coverage**: Aiming for 70%+ (enforced in jest.config.js) -- **Test Types**: - - Unit tests (pattern matching, narrative generation) - - Integration tests (extension activation, command execution) - - Performance tests (<300ms pattern detection) - -- **Evidence**: - ```javascript - // jest.config.js - coverageThreshold: { - global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70 - } - } - ``` - -- **Running Tests**: `npm test` - -## 9. Build Reproducibility ✅ - -**Status**: COMPLIANT - -- **Build Systems**: - - **npm** with package-lock.json (committed) - - **justfile** for task running - - **Nix flake** for reproducible builds - - **CI/CD** (GitHub Actions, GitLab CI) - -- **Evidence**: - - flake.nix with locked dependencies - - package-lock.json committed - - Deterministic build process - -- **Verification**: `nix build` produces identical output - -## 10. .well-known/ Directory ✅ - -**Status**: COMPLIANT - -- **Files**: - - ✅ `security.txt` (RFC 9116 compliant) - - ✅ `ai.txt` (AI training policies) - - ✅ `humans.txt` (attribution, credits) - -- **RFC 9116 Compliance**: - ``` - Contact: https://github.com/.../security/advisories/new - Expires: 2025-11-22T23:59:59.000Z - Preferred-Languages: en - Policy: https://github/.../SECURITY.md - ``` - -## 11. No Vendor Lock-in ✅ - -**Status**: COMPLIANT - -- **Platform Independence**: - - Open source (MIT OR Palimpsest) - - Standard formats (TypeScript, JSON, Markdown) - - VS Code API (cross-platform) - - No proprietary dependencies - -- **Migration Path**: - - Pattern library exportable to JSON - - Code transformation logic reusable - - Documentation in standard formats - ---- - -## Additional RSR Requirements - -### Performance Targets ✅ - -| Metric | Target | Actual | Status | -|--------|--------|--------|--------| -| Pattern detection | <300ms | 50-100ms | ✅ Exceeds | -| Memory usage | <100MB | ~20MB | ✅ Exceeds | -| UI response | <50ms | 10-20ms | ✅ Exceeds | - -### Privacy & Telemetry ✅ - -- **Default**: Zero telemetry -- **Opt-in**: Privacy-preserving analytics only -- **No PII**: Ever -- **Local-first**: All data stays local - -### Accessibility ✅ - -- Semantic HTML in webviews -- WCAG 2.1 AA minimum (target) -- Keyboard navigation support -- Screen reader compatible - ---- - -## Compliance Verification - -### Automated Checks - -```bash -# Run RSR validation -just validate - -# Check documentation -just validate-docs - -# Check security -just validate-security - -# Check licenses -just validate-licenses - -# Run tests -just test - -# Build -just compile -``` - -### Manual Verification - -1. **Type Safety**: `npx tsc --noEmit` ✅ -2. **Offline**: Disconnect network, test extension ✅ -3. **Documentation**: All required files present ✅ -4. **Security**: security.txt validates ✅ -5. **Tests**: `npm test` passes ✅ -6. **Build**: `nix build` succeeds ✅ - ---- - -## Compliance Score - -| Category | Weight | Score | Weighted | -|----------|--------|-------|----------| -| Type Safety | 10% | 100% | 10.0 | -| Memory Safety | 10% | 100% | 10.0 | -| Offline-First | 15% | 100% | 15.0 | -| Documentation | 10% | 100% | 10.0 | -| Security | 15% | 100% | 15.0 | -| Governance | 5% | 100% | 5.0 | -| Licensing | 5% | 100% | 5.0 | -| Testing | 10% | 100% | 10.0 | -| Build Repro | 10% | 100% | 10.0 | -| .well-known | 5% | 100% | 5.0 | -| No Lock-in | 5% | 100% | 5.0 | - -**Total**: **100%** ✅ - ---- - -## Bronze Level Requirements - -✅ All 11 categories compliant -✅ Documentation complete -✅ Security baseline met -✅ Open source licensed -✅ Build reproducibility -✅ Test coverage ≥70% -✅ Offline-first architecture - -**Result**: **BRONZE LEVEL ACHIEVED** 🥉 - ---- - -## Future Improvements (Silver/Gold Levels) - -**Silver Level** (future): -- Formal verification (SPARK proofs) -- Advanced security audits -- Multi-language support -- Enhanced accessibility (WCAG 2.1 AAA) - -**Gold Level** (future): -- Mathematically proven correctness -- Zero-trust architecture -- Advanced CRDT support -- Production hardening - ---- - -## Continuous Compliance - -### Monitoring - -- CI/CD runs RSR validation on every commit -- Automated dependency updates -- Security scanning (npm audit) -- Documentation freshness checks - -### Maintenance - -- Quarterly RSR compliance review -- Annual security audit -- Continuous dependency updates -- Community feedback integration - ---- - -## Contact - -**Questions about RSR compliance?** - -- See [MAINTAINERS.md](MAINTAINERS.md) -- Open an issue: [GitHub Issues](https://github.com/Hyperpolymath/nextgen-languages-evangeliser/issues) -- Security: [.well-known/security.txt](.well-known/security.txt) - ---- - -**Last Updated**: 2024-11-22 -**Next Review**: 2025-02-22 -**Compliance Level**: Bronze ✅ diff --git a/docs/TPCF.adoc b/docs/TPCF.adoc new file mode 100644 index 0000000..0dd0f32 --- /dev/null +++ b/docs/TPCF.adoc @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += Tri-Perimeter Contribution Framework (TPCF) +:toc: macro + +*Nextgen Languages Evangeliser operates as TPCF Perimeter 3 — a community +sandbox with maximum openness.* The engine is language-agnostic; the substantive +contribution is *authoring cartridges* (per-language correspondence facts), not +editing a "transformation" tool. + +toc::[] + +== The three perimeters + +[cols="1,3",options="header"] +|=== +| Perimeter | Trust / governance + +| *P1 — Core* | Maintainer-only: CI/CD, release automation, security policy, the + Idris2 ABI / Zig FFI seam, the cartridge *contract* (schema) itself. +| *P2 — Stable* | Reviewed: the engine + interface code, the browser workspace, + the AffineScript host application. +| *P3 — Community Sandbox* | Fully open: *cartridge* authoring (correspondence + facts), reference-pack additions, documentation, examples, translations. +|=== + +== What lives where + +* *Cartridges (P3)* — anyone may add or extend a per-language cartridge under the + cartridge schema. Correspondence facts are data, not code: a Concept, two + Forms, the Transition's `CorrespondenceKind`, its residue, the per-stratum + verdicts, and an optional witness. Validated by + `scripts/validate-cartridges.js` and the invariant tests. +* *Documentation (P3)* — typo fixes merge fast; new guides get light review. +* *Engine / interface (P2)* — `src/interface/**`, `gui/**`, the CLI: maintainer + review; architectural changes discussed in an issue first. +* *Infrastructure / contract (P1)* — workflows, release, security, and the + cartridge schema + ABI seam. + +== Authoring a cartridge (the main P3 path) + +. Read `docs/theory/CORRESPONDENCE-MODEL.adoc` (Concept / Form / Transition; the + six CorrespondenceKinds; the strata). +. Add or extend a `*.cartridge.json` under `cartridges/` following + `cartridges/correspondence-cartridge.schema.json`. +. For each Transition, classify the *kind* (cognate / false-friend / antonym / + alien-realization / novel / vanished), record the *residue*, and write the + *no-shame* narrative (celebrate / minimise / better / safety / example). +. Validate: `just validate-cartridges` and `just test`. +. Open a pull request. + +A worked exemplar is `cartridges/reference/worked-examples.cartridge.json` (one +correspondence per kind). + +== Decision making + +* *Cartridge / doc additions (P3):* automated checks + light community review; + fast merge. Maintainer veto only for harmful or incorrect content. +* *Feature additions (P2):* open an issue/RFC, gather feedback, rough consensus, + implement, review, merge. +* *Breaking changes / contract changes (P1/P2):* RFC with migration notes, + discussion, maintainer consensus, documented in the changelog. +* *Security fixes (P1):* lead maintainer; private fix → disclosure → release. + +== Community standards + +All contribution follows the project's *no-shame* ethic — celebrate existing +knowledge, never shame a developer, give constructive feedback with examples. +See link:../CODE_OF_CONDUCT.md[CODE_OF_CONDUCT.md]. + +[example] +==== +*Good review:* "Nice cartridge! The false-friend call on Erlang `=` is spot on. +Could we add the per-stratum verdict for `structure` too, so the workspace shows +where it diverges? Here's the shape: …" + +*Not this:* "This is wrong, read the spec." +==== + +== Forking + +You are welcome to fork and adapt the engine or author your own cartridge packs — +the engine is language-agnostic and the cartridge contract is portable. See the +repository `LICENSE` for terms. + +== Contact + +* GitHub Issues / Discussions for questions and proposals. +* Maintainers: see link:../MAINTAINERS.adoc[MAINTAINERS.adoc]. diff --git a/docs/TPCF.md b/docs/TPCF.md deleted file mode 100644 index bc101c5..0000000 --- a/docs/TPCF.md +++ /dev/null @@ -1,400 +0,0 @@ -# Tri-Perimeter Contribution Framework (TPCF) - -## Nextgen Languages Evangeliser: Perimeter 3 (Community Sandbox) - -**Project**: Nextgen Languages Evangeliser -**TPCF Level**: Perimeter 3 -**Governance**: Community-driven, fully open -**Last Updated**: 2024-11-22 - ---- - -## What is TPCF? - -The **Tri-Perimeter Contribution Framework** defines three levels of contribution trust and governance: - -1. **Perimeter 1 (Core)**: Maintainer-only, critical infrastructure -2. **Perimeter 2 (Stable)**: Reviewed contributions, production code -3. **Perimeter 3 (Community Sandbox)**: Fully open, experimental, community-driven - -Nextgen Languages Evangeliser operates as **Perimeter 3** - a community sandbox with maximum openness. - ---- - -## Perimeter 3 Characteristics - -### Fully Open Contributions ✅ - -- **No approval required** for pattern additions -- **Automated review** for common changes -- **Community-driven** feature development -- **Inclusive** decision-making - -### Fast Iteration ✅ - -- Merge patterns quickly -- Experiment freely -- Fail fast, learn faster -- Continuous improvement - -### Low Barrier to Entry ✅ - -- First-time contributors welcome -- Good first issues labeled -- Comprehensive contribution guide -- Supportive community (CCCP) - -### Graduated Trust Model ✅ - -Contributors can progress: -- **Contributor** → **Pattern Author** → **Reviewer** → **Maintainer** - ---- - -## How TPCF Works for This Project - -### Pattern Library (Perimeter 3) - -**Full Community Access**: -- Anyone can add patterns -- Automated tests validate patterns -- Community reviews patterns -- Fast merge (24-48 hours) - -**Process**: -1. Fork repository -2. Add pattern to `patterns/` directory -3. Add tests -4. Open pull request -5. Automated CI runs -6. Community feedback (optional) -7. **Auto-merge** if tests pass (no manual approval needed) - -### Documentation (Perimeter 3) - -**Fully Open**: -- Typo fixes: immediate merge -- New guides: community review -- Translations: fully community-driven - -### Core Extension (Perimeter 2) - -**Reviewed Contributions**: -- Extension logic: maintainer review required -- Security-critical code: thorough review -- Architecture changes: design discussion first - -### Infrastructure (Perimeter 1) - -**Maintainer-Only**: -- CI/CD configuration -- Release automation -- Security policies -- Publishing credentials - ---- - -## Contribution Paths - -### Path 1: Pattern Contributor - -**Goal**: Add JavaScript → ReScript transformation patterns - -**Steps**: -1. Find a JavaScript pattern not yet in library -2. Create pattern with celebrate/minimize/better narrative -3. Add tests -4. Submit PR -5. **Auto-merge** on passing tests - -**Example**: -```typescript -{ - id: 'my-pattern', - name: 'My Pattern', - category: 'functional', - difficulty: 'beginner', - jsExample: `const x = [1, 2, 3];`, - rescriptExample: `let x = [1, 2, 3]`, - narrative: { - celebrate: "Great array usage!", - minimize: "Works perfectly...", - better: "ReScript arrays are type-safe!", - safety: "Compiler prevents index errors.", - example: "Try it!" - }, - // ... rest of pattern -} -``` - -### Path 2: Documentation Contributor - -**Goal**: Improve documentation - -**Steps**: -1. Fix typos, improve clarity -2. Add examples -3. Translate content -4. Submit PR -5. **Fast merge** - -### Path 3: Code Contributor - -**Goal**: Improve extension functionality - -**Steps**: -1. Pick an issue (or create one) -2. Discuss approach (for large changes) -3. Implement -4. Add tests -5. Submit PR -6. Maintainer review -7. Merge - -### Path 4: Reviewer - -**Goal**: Help review contributions - -**Requirements**: -- 5+ merged PRs -- Understanding of project goals -- Follow CCCP (compassionate reviews) - -**Promotion**: -- Self-nominate or maintainer invite -- Consensus vote - -### Path 5: Maintainer - -**Goal**: Lead project direction - -**Requirements**: -- 3+ months consistent contributions -- Deep understanding of codebase -- Community trust -- CCCP exemplar - -**Promotion**: -- Maintainer nomination -- Consensus vote - ---- - -## Decision Making - -### Pattern Additions (Perimeter 3) - -- **Who Decides**: Automated tests + community -- **Timeline**: 24-48 hours -- **Veto**: Maintainers (rare, only for harmful content) - -### Feature Additions (Perimeter 2/3) - -- **Who Decides**: Community discussion -- **Process**: - 1. Open issue/RFC - 2. Community feedback (7 days) - 3. Rough consensus - 4. Implementation - 5. Review - 6. Merge - -### Breaking Changes (Perimeter 2) - -- **Who Decides**: Maintainers + community -- **Process**: - 1. RFC with migration plan - 2. Community discussion (14 days) - 3. Maintainer consensus - 4. Implementation - 5. Documented in CHANGELOG - -### Security Fixes (Perimeter 1) - -- **Who Decides**: Lead maintainer -- **Timeline**: Immediate -- **Process**: Private fix → disclosure → release - ---- - -## Community Standards - -### Compassionate Code Contribution Pledge (CCCP) - -**Core Principle**: "Celebrate good, minimize bad, show better" - -**All contributors must**: -- ✅ Never shame developers -- ✅ Celebrate existing knowledge -- ✅ Provide constructive feedback -- ✅ Assume good intent -- ✅ Foster psychological safety - -See [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) for full pledge. - -### Review Guidelines - -**When reviewing PRs**: -- Celebrate what works -- Gently suggest improvements -- Provide examples -- Be patient with learners -- Remember: everyone was a beginner once - -**Example Good Review**: -> Nice pattern! I like how you handled the null check. One small suggestion: could we add a test for nested null values? Here's an example: [code]. What do you think? - -**Example Bad Review** (❌ Don't do this): -> This is wrong. You should know better. Read the docs. - ---- - -## Perimeter Boundaries - -### What Requires Review? - -**Perimeter 1** (Maintainer-only): -- Release process -- CI/CD changes -- Security policies -- Publishing - -**Perimeter 2** (Reviewed): -- Extension core logic -- UI components -- Security-sensitive code -- Breaking changes - -**Perimeter 3** (Auto-merge): -- Pattern additions -- Documentation -- Tests -- Examples -- Translations - -### Migration Between Perimeters - -Code can move between perimeters: - -**P3 → P2**: Experimental features that mature -**P2 → P1**: Critical infrastructure components - -Always documented in issues/discussions. - ---- - -## Conflict Resolution - -### Process - -1. **Direct Discussion**: Contributors talk it out -2. **Community Input**: Open discussion -3. **Maintainer Mediation**: If needed -4. **Consensus**: Aim for agreement -5. **Maintainer Decision**: Final arbiter (rare) - -### Principles - -- Assume good intent -- Focus on project health -- Follow CCCP -- Document decisions -- Move forward - ---- - -## Benefits of Perimeter 3 - -### For Contributors - -- 🚀 Fast iteration -- 💪 Low barriers -- 🎓 Learning opportunities -- 🤝 Community support -- ⭐ Recognition - -### For Users - -- 📈 Rapid feature growth -- 🔧 Community-driven fixes -- 📚 Rich pattern library -- 🌍 Diverse perspectives - -### For Project - -- 🌱 Sustainable growth -- 🎯 Community ownership -- 💡 Innovation -- 🔄 Continuous improvement - ---- - -## Metrics & Transparency - -### Public Metrics - -- Patterns added per month -- Contributors (first-time, returning) -- PR merge time (target: <48h for P3) -- Community satisfaction - -### Transparency - -- All decisions documented -- Public issue discussions -- Open governance meetings -- Regular reports - ---- - -## Examples from Other Projects - -**Perimeter 3 (like us)**: -- Wikipedia article edits -- Open source pattern libraries -- Community-driven docs - -**Perimeter 2**: -- Linux kernel contributions -- VS Code extensions (core) - -**Perimeter 1**: -- Security patches -- Release management - ---- - -## FAQ - -**Q: Can I add any pattern?** -A: Yes! As long as it: -- Has JavaScript → ReScript transformation -- Includes tests -- Follows "celebrate good" philosophy -- Passes automated checks - -**Q: How do I become a maintainer?** -A: Contribute regularly (3+ months), help community, exemplify CCCP values, get nominated. - -**Q: What if my PR is rejected?** -A: Very rare for P3. Usually means: -- Tests failing (fix and resubmit) -- Harmful content (won't merge) -- Better discussed first (open issue) - -**Q: Can I fork and create my own version?** -A: Absolutely! MIT OR Palimpsest license. We'd love to see your innovations! - ---- - -## Contact - -**Questions about TPCF?** - -- GitHub Discussions: General questions -- Issues: Specific concerns -- Maintainers: See [MAINTAINERS.md](../MAINTAINERS.md) - ---- - -**Remember**: Perimeter 3 means you're empowered to contribute! We trust you. Welcome to the community! 💙 From cc8377b9f1caa0af2f4c389206a93cb28df6f390 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:46:05 +0000 Subject: [PATCH 15/17] Align licensing: MPL-2.0 (code) / CC-BY-SA-4.0 (docs); retire MIT+Palimpsest Per owner direction, make the repo's licensing consistent: - Code SPDX is MPL-2.0; docs SPDX is CC-BY-SA-4.0. The dual remnants ("MPL-2.0 OR Palimpsest-0.8" headers, config.ncl "MIT OR Palimpsest-0.8") are gone. - Root LICENSE stays the MPL-2.0 text, so GitHub labels the repo MPL-2.0. - LICENSES/ already holds verbatim MPL-2.0.txt + CC-BY-SA-4.0.txt; the licence checks (ci.yml, .gitlab-ci.yml, Justfile validate-licenses) now point there. - Removed the old dual-scheme files LICENSE-MIT.txt + LICENSE-PALIMPSEST.txt (MIT/Palimpsest are not this repo's licences; Palimpsest is reserved for the palimpsest repos per estate policy). Touched: SPDX headers (Justfile, Mustfile.epx, config.ncl); config.ncl license field; README label + badge link; QUICKSTART-MAINTAINER; .well-known/humans.txt + ai.txt; guix.scm -> (license mpl2.0); STATE.a2ml blocker cleared; HANDOFF.adoc licensing note updated to resolved. Verified: deno fmt/lint/test/build green; no tracked in-scope Palimpsest/MIT licence-label drift remains (third-party node_modules left untouched per guardrail). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .github/workflows/ci.yml | 4 +- .gitlab-ci.yml | 4 +- .machine_readable/STATE.a2ml | 3 +- .well-known/ai.txt | 2 +- .well-known/humans.txt | 2 +- HANDOFF.adoc | 23 +++--- Justfile | 9 ++- LICENSE-MIT.txt | 21 ----- LICENSE-PALIMPSEST.txt | 153 ----------------------------------- Mustfile.epx | 2 +- QUICKSTART-MAINTAINER.adoc | 2 +- README.adoc | 4 +- config.ncl | 4 +- guix.scm | 3 +- 14 files changed, 30 insertions(+), 206 deletions(-) delete mode 100644 LICENSE-MIT.txt delete mode 100644 LICENSE-PALIMPSEST.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4dc549..1ecfdfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,8 +127,8 @@ jobs: test -f CLAUDE.md || (echo "Missing CLAUDE.md" && exit 1) # Licenses - test -f LICENSE-MIT.txt || (echo "Missing LICENSE-MIT.txt" && exit 1) - test -f LICENSE-PALIMPSEST.txt || (echo "Missing LICENSE-PALIMPSEST.txt" && exit 1) + test -f LICENSES/MPL-2.0.txt || (echo "Missing LICENSES/MPL-2.0.txt" && exit 1) + test -f LICENSES/CC-BY-SA-4.0.txt || (echo "Missing LICENSES/CC-BY-SA-4.0.txt" && exit 1) # .well-known/ test -f .well-known/security.txt || (echo "Missing security.txt" && exit 1) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36fe1d3..e53b1d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -106,8 +106,8 @@ rsr-compliance: test -f ../CHANGELOG.md || (echo "Missing CHANGELOG.md" && exit 1) # Licenses - test -f ../LICENSE-MIT.txt || (echo "Missing LICENSE-MIT.txt" && exit 1) - test -f ../LICENSE-PALIMPSEST.txt || (echo "Missing LICENSE-PALIMPSEST.txt" && exit 1) + test -f ../LICENSES/MPL-2.0.txt || (echo "Missing LICENSES/MPL-2.0.txt" && exit 1) + test -f ../LICENSES/CC-BY-SA-4.0.txt || (echo "Missing LICENSES/CC-BY-SA-4.0.txt" && exit 1) # .well-known/ test -f ../.well-known/security.txt || (echo "Missing security.txt" && exit 1) diff --git a/.machine_readable/STATE.a2ml b/.machine_readable/STATE.a2ml index 2d26cbb..547c72d 100644 --- a/.machine_readable/STATE.a2ml +++ b/.machine_readable/STATE.a2ml @@ -42,8 +42,7 @@ (blockers-and-issues "AffineScript/Idris2/Zig/Deno toolchain absent in some environments — author now, verify in CI" - "JTV grammar-v2 worked example stubbed pending julia-the-viper repo access" - "Licence-label drift to flag to owner (PMPL badge / 'Palimpsest License (MPL-2.0)' / ROADMAP 'MPL-2.0-or-later') vs MPL-2.0 SPDX — FLAG-ONLY, manual") + "JTV grammar-v2 worked example stubbed pending julia-the-viper repo access") (critical-next-actions "Port the legacy pattern catalogue into cartridge facts (Concept/Form/Transition + CorrespondenceKind)" diff --git a/.well-known/ai.txt b/.well-known/ai.txt index 334b406..db0e473 100644 --- a/.well-known/ai.txt +++ b/.well-known/ai.txt @@ -10,7 +10,7 @@ Disallow-Generation: yes # This project's code is licensed under MPL-2.0. # AI agents may read and analyze this code for assisting contributors. # AI agents must NOT use this code for model training without explicit consent. -# AI agents must preserve Emotional Lineage per PMPL Section 3. +# This project's documentation is licensed under CC-BY-SA-4.0. # # For AI agent integration instructions, see: # 0-AI-MANIFEST.a2ml (universal AI entry point) diff --git a/.well-known/humans.txt b/.well-known/humans.txt index 5b6216d..263b838 100644 --- a/.well-known/humans.txt +++ b/.well-known/humans.txt @@ -9,6 +9,6 @@ From: United Kingdom /* SITE */ Last update: 2026-03-28 Standards: RSR (Rhodium Standard Repository) -License: MPL-2.0 (Palimpsest MPL) +License: MPL-2.0 (code) / CC-BY-SA-4.0 (docs) Components: Idris2 ABI, Zig FFI Tools: just, Podman, Guix diff --git a/HANDOFF.adoc b/HANDOFF.adoc index 322a16d..20770ff 100644 --- a/HANDOFF.adoc +++ b/HANDOFF.adoc @@ -49,11 +49,10 @@ deno run --allow-read bin/evangeliser.js --kind false-friend # offline CLI (JTV is the canonical alien-realization / novel exemplar). . Begin the *AffineScript host application* against the Idris2 ABI / Zig FFI seams (needs the AffineScript compiler). -. Owner-only follow-ups (flagged, not done — out of agent scope): the - `CONTRIBUTING.md` / `CHANGELOG.md` `.md`-vs-`.adoc` format choice (GitHub-required - files), and the licence-label drift (`config.ncl` `MIT OR Palimpsest`, the README - "Palimpsest License (MPL-2.0)" label, `LICENSE-MIT.txt` / `LICENSE-PALIMPSEST.txt` - vs the `MPL-2.0` classification). Both are owner-domain — flag, never sweep. +. Owner-only follow-up (flagged, not done): the `CONTRIBUTING.md` / `CHANGELOG.md` + `.md`-vs-`.adoc` format choice (these GitHub-required files have clean `.adoc` + twins; pick one per file). The licence-label drift is *resolved* per owner + direction — see the Licensing note below. == Guardrails (non-negotiable) @@ -65,11 +64,11 @@ deno run --allow-read bin/evangeliser.js --kind false-friend # offline CLI * *Licence files and SPDX headers are owner-only.* Never relicense or bulk-sweep SPDX; licence-label drift is FLAG-ONLY to the owner. -== Known open item (owner decision) +== Licensing (resolved 2026-06) -The `governance / Licence consistency` check fails because `LICENSE` has no -`SPDX-License-Identifier:` header. This is *pre-existing* and *owner-domain* — -left untouched on purpose. It is entangled with label drift (the `LICENSE` body -is the Palimpsest text; `config.ncl` says `MIT OR Palimpsest-0.8`; the README -says "Palimpsest License (MPL-2.0)"; the manifests say `MPL-2.0`). Resolve only -with an explicit owner decision on the exact identifier. +Per owner direction: *code is MPL-2.0, documentation is CC-BY-SA-4.0.* The root +`LICENSE` is the MPL-2.0 text (so GitHub labels the repo MPL-2.0); `LICENSES/` +holds verbatim `MPL-2.0.txt` + `CC-BY-SA-4.0.txt`. The old MIT + Palimpsest +dual-licence artifacts (`LICENSE-MIT.txt`, `LICENSE-PALIMPSEST.txt`) have been +removed. SPDX headers and licence files remain *owner-only* — never relicense or +bulk-sweep. diff --git a/Justfile b/Justfile index e05c192..85d0e7f 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,6 @@ # justfile for Nextgen Languages Evangeliser # https://github.com/casey/just -# SPDX-License-Identifier: MPL-2.0 OR Palimpsest-0.8 +# SPDX-License-Identifier: MPL-2.0 # # Per Hyperpolymath policy: # - Use Deno, not npm/bun @@ -126,9 +126,10 @@ validate-security: # Validate licenses validate-licenses: @echo "⚖️ Checking licenses..." - @test -f LICENSE-MIT.txt || (echo "❌ Missing LICENSE-MIT.txt" && exit 1) - @test -f LICENSE-PALIMPSEST.txt || (echo "❌ Missing LICENSE-PALIMPSEST.txt" && exit 1) - @echo "✅ Dual licenses present" + @test -f LICENSE || (echo "❌ Missing LICENSE (MPL-2.0)" && exit 1) + @test -f LICENSES/MPL-2.0.txt || (echo "❌ Missing LICENSES/MPL-2.0.txt" && exit 1) + @test -f LICENSES/CC-BY-SA-4.0.txt || (echo "❌ Missing LICENSES/CC-BY-SA-4.0.txt" && exit 1) + @echo "✅ Licenses present (MPL-2.0 code · CC-BY-SA-4.0 docs)" # Validate language policy (no Makefile, no new TS) validate-policy: diff --git a/LICENSE-MIT.txt b/LICENSE-MIT.txt deleted file mode 100644 index a0e9929..0000000 --- a/LICENSE-MIT.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Jonathan D.A. Jewell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE-PALIMPSEST.txt b/LICENSE-PALIMPSEST.txt deleted file mode 100644 index ec540b3..0000000 --- a/LICENSE-PALIMPSEST.txt +++ /dev/null @@ -1,153 +0,0 @@ -SPDX-License-Identifier: MPL-2.0 -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/Mustfile.epx b/Mustfile.epx index 6606b35..907012f 100644 --- a/Mustfile.epx +++ b/Mustfile.epx @@ -1,5 +1,5 @@ # Mustfile.epx - Deployment Contract for Nextgen Languages Evangeliser -# SPDX-License-Identifier: MPL-2.0 OR Palimpsest-0.8 +# SPDX-License-Identifier: MPL-2.0 # # This file defines the physical state transitions for deployment. # Based on Ephapax Linear Logic - resources are consumed to produce outputs. diff --git a/QUICKSTART-MAINTAINER.adoc b/QUICKSTART-MAINTAINER.adoc index 34ac810..64e5615 100644 --- a/QUICKSTART-MAINTAINER.adoc +++ b/QUICKSTART-MAINTAINER.adoc @@ -106,7 +106,7 @@ Or via OPSM: `opsm update {{PACKAGE_NAME}}` == Security Notes -* License: MPL-2.0 (Palimpsest License) +* License: MPL-2.0 (code) · CC-BY-SA-4.0 (docs) * All dependencies SHA-pinned * `panic-attacker` scan results: link:INSTALL-SECURITY-REPORT.adoc[] * OpenSSF Scorecard: see badge in README diff --git a/README.adoc b/README.adoc index 675c4ee..1753fe0 100644 --- a/README.adoc +++ b/README.adoc @@ -1,7 +1,7 @@ = Nextgen Languages Evangeliser image:https://img.shields.io/badge/RSR-Bronze-CD7F32[RSR Bronze,link=docs/RSR_COMPLIANCE.adoc] -image:https://img.shields.io/badge/License-MPL--2.0-blue.svg[License: MPL-2.0,link="https://github.com/hyperpolymath/palimpsest-license"] +image:https://img.shields.io/badge/License-MPL--2.0-blue.svg[License: MPL-2.0,link="https://www.mozilla.org/en-US/MPL/2.0/"] // SPDX-License-Identifier: CC-BY-SA-4.0 // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell @@ -270,7 +270,7 @@ This is a *TPCF Perimeter 3* project - all contributions are welcome! // SPDX-License-Identifier: CC-BY-SA-4.0 -This project is licensed under the *Palimpsest License (MPL-2.0)*. +This project's code is licensed under the *Mozilla Public License 2.0 (MPL-2.0)*; its documentation under *CC-BY-SA-4.0*. Full licence texts are in link:LICENSES/[LICENSES/], and the repository `LICENSE` is the MPL-2.0 text. See link:LICENSE[LICENSE] for the full license text. diff --git a/config.ncl b/config.ncl index 6ae37b6..efd8fb1 100644 --- a/config.ncl +++ b/config.ncl @@ -1,5 +1,5 @@ # config.ncl - Nickel configuration for Nextgen Languages Evangeliser -# SPDX-License-Identifier: MPL-2.0 OR Palimpsest-0.8 +# SPDX-License-Identifier: MPL-2.0 # # Type-safe configuration using Nickel # Renamed from rescript-evangeliser on 2026-04-16 @@ -10,7 +10,7 @@ predecessor = "rescript-evangeliser", version = "0.6.0-alpha", description = "Learn next-generation type-safe languages (flagship: AffineScript) through progressive code transformation", - license = "MIT OR Palimpsest-0.8", + license = "MPL-2.0", }, # Target languages (what we teach) diff --git a/guix.scm b/guix.scm index f2ce961..cc35bbf 100644 --- a/guix.scm +++ b/guix.scm @@ -14,5 +14,4 @@ (synopsis "nextgen-languages-evangeliser") (description "nextgen-languages-evangeliser — part of the hyperpolymath ecosystem.") (home-page "https://github.com/hyperpolymath/nextgen-languages-evangeliser") - (license ((@@ (guix licenses) license) "MPL-2.0" - "https://github.com/hyperpolymath/palimpsest-license"))) + (license mpl2.0)) From c3e181004d746f398b30b41495c30d0575183cfe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 21:46:40 +0000 Subject: [PATCH 16/17] Consolidate CONTRIBUTING + CHANGELOG to .adoc (retire the .md duplicates) Per owner direction ("go adoc for all of those"), complete the doc-format migration for the two remaining duplicates: - CONTRIBUTING.adoc is now the full guide (ported from the .md); CONTRIBUTING.md removed. - CHANGELOG.adoc is canonical (verified superset of the .md history); CHANGELOG.md removed. Repointed every reference: README, CODE_OF_CONDUCT.md, SECURITY.md, 0-AI-MANIFEST.a2ml, .machine_readable/TRUST.contractile, docs/RSR_COMPLIANCE.adoc, HANDOFF.adoc, and the doc-presence checks in .github/workflows/ci.yml + .gitlab-ci.yml + Justfile validate-docs. (quality.yml already accepts either.) Verified: deno fmt/lint/test green; no dangling .md links remain. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- .github/workflows/ci.yml | 4 +- .gitlab-ci.yml | 4 +- .machine_readable/TRUST.contractile | 2 +- 0-AI-MANIFEST.a2ml | 2 +- CHANGELOG.md | 217 ---------------------------- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.adoc | 136 +++++++++++++++-- CONTRIBUTING.md | 130 ----------------- HANDOFF.adoc | 7 +- Justfile | 4 +- README.adoc | 6 +- SECURITY.md | 2 +- docs/RSR_COMPLIANCE.adoc | 2 +- 13 files changed, 141 insertions(+), 377 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ecfdfa..39b944d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,11 +119,11 @@ jobs: # Documentation test -f README.md || test -f README.adoc || (echo "Missing README.md or README.adoc" && exit 1) - test -f CONTRIBUTING.md || (echo "Missing CONTRIBUTING.md" && exit 1) + test -f CONTRIBUTING.adoc || (echo "Missing CONTRIBUTING.adoc" && exit 1) test -f CODE_OF_CONDUCT.md || (echo "Missing CODE_OF_CONDUCT.md" && exit 1) test -f SECURITY.md || (echo "Missing SECURITY.md" && exit 1) test -f MAINTAINERS.adoc || (echo "Missing MAINTAINERS.adoc" && exit 1) - test -f CHANGELOG.md || (echo "Missing CHANGELOG.md" && exit 1) + test -f CHANGELOG.adoc || (echo "Missing CHANGELOG.adoc" && exit 1) test -f CLAUDE.md || (echo "Missing CLAUDE.md" && exit 1) # Licenses diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e53b1d3..cc7b8a9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -99,11 +99,11 @@ rsr-compliance: # Documentation test -f ../README.adoc || (echo "Missing README.adoc" && exit 1) - test -f ../CONTRIBUTING.md || (echo "Missing CONTRIBUTING.md" && exit 1) + test -f ../CONTRIBUTING.adoc || (echo "Missing CONTRIBUTING.adoc" && exit 1) test -f ../CODE_OF_CONDUCT.md || (echo "Missing CODE_OF_CONDUCT.md" && exit 1) test -f ../SECURITY.md || (echo "Missing SECURITY.md" && exit 1) test -f ../MAINTAINERS.adoc || (echo "Missing MAINTAINERS.adoc" && exit 1) - test -f ../CHANGELOG.md || (echo "Missing CHANGELOG.md" && exit 1) + test -f ../CHANGELOG.adoc || (echo "Missing CHANGELOG.adoc" && exit 1) # Licenses test -f ../LICENSES/MPL-2.0.txt || (echo "Missing LICENSES/MPL-2.0.txt" && exit 1) diff --git a/.machine_readable/TRUST.contractile b/.machine_readable/TRUST.contractile index d9b5ed2..32a3f60 100644 --- a/.machine_readable/TRUST.contractile +++ b/.machine_readable/TRUST.contractile @@ -20,7 +20,7 @@ ; ; Scope: ; TRUST governs AI agent behaviour only. It does not affect human -; contributors — humans follow CONTRIBUTING.md and GOVERNANCE.adoc. +; contributors — humans follow CONTRIBUTING.adoc and GOVERNANCE.adoc. ; ; Relationship to other contractiles: ; - MUST: Trust never overrides MUST. Even at maximal trust, MUST diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index c1dd36a..8d5e431 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -63,7 +63,7 @@ Policy enforcement contracts (k9, dust, lust, must, trust). - `MAINTAINERS` - Machine-readable contact list - `SUPPORT` - Support channels - `SECURITY.md` - Technical security policy -- `CONTRIBUTING.md` - Technical contribution manual +- `CONTRIBUTING.adoc` - Technical contribution manual - `CODE_OF_CONDUCT.md` - Conduct rules ### Agent Instructions: `.machine_readable/agent_instructions/` ONLY diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 50f5b0a..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,217 +0,0 @@ -# Changelog - -All notable changes to the Nextgen Languages Evangeliser project (formerly *ReScript Evangeliser*) will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added -- Migration to **Nextgen Languages Evangeliser** — multi-target pattern catalogue -- AffineScript added as flagship target (successor to ReScript as the teaching subject) -- Roadmap with 6 migration phases (see `ROADMAP.adoc`) -- Zig formalised as canonical FFI layer; V banned outside V-ecosystem work -- ReScript source files (Types.res, Glyphs.res, Narrative.res, Patterns.res) -- Deno configuration (deno.json) and build scripts -- Mustfile.epx deployment contract -- Nickel configuration (config.ncl) -- Makefile blocker workflow (enforce Justfile usage) -- npm/bun blocker workflow (enforce Deno usage) -- SPDX license headers on all ReScript source files - -### Changed -- **BREAKING**: Migrated from TypeScript to ReScript (Hyperpolymath language policy) -- **BREAKING**: Migrated from npm to Deno (Hyperpolymath language policy) -- Updated Justfile to use Deno tasks instead of npm scripts -- Updated README.adoc to reflect ReScript/Deno stack -- Updated CLAUDE.md with new language policy and structure -- Updated .gitignore for ReScript and Deno artifacts - -### Removed -- TypeScript source files (migrated to ReScript) -- extension/ directory (old VS Code extension scaffolding) -- npm package.json (replaced with deno.json) -- tsconfig.json, eslint configuration (TypeScript tooling) -- TS_CONVERSION_NEEDED.md (no longer needed) - -### Deprecated -- N/A (initial release) - -### Removed -- N/A (initial release) - -### Fixed -- N/A (initial release) - -### Security -- Implemented input validation for all user code -- Content Security Policy for webviews -- Resource limits (file size, parsing timeout, memory) -- No network dependencies (offline-first security model) -- Supply chain security (minimal dependencies, lockfile) -- RFC 9116 compliant security.txt - -## [0.1.0] - 2024-11-22 - -### Added -- Initial alpha release -- Core extension functionality -- 10 foundational patterns (Phase 1 complete): - 1. Null safety checks → Option types - 2. Async/await → Promise types - 3. Try/catch → Result types - 4. Array.map/filter/reduce → Pipe operator - 5. Ternary conditionals → Pattern matching - 6. Object destructuring → Record patterns - 7. Default parameters → Option.getOr - 8. Array.reduce → Recursive patterns - 9. Template literals → String interpolation - 10. Arrow functions → ReScript functions -- RAW view (side-by-side comparison with narratives) -- GLYPHED view (symbol-annotated code) -- Basic UI shells for FOLDED and WYSIWYG views -- 11 core glyphs (🔄🎯🛡️➡️🌿📦🔗💎⏳✅💡) -- Pattern detection via regex (Phase 1) and AST (Phase 2) -- VS Code extension scaffolding -- TypeScript strict mode configuration -- ESLint and testing infrastructure -- Initial documentation - -### Performance -- Pattern detection: 50-100ms (target: <300ms) ✅ -- Memory usage: ~20MB (target: <100MB) ✅ -- UI response: 10-20ms (target: <50ms) ✅ - -### Documentation -- README with quick start guide -- CLAUDE.md with project context -- Pattern library documentation -- Quick start guide -- Project overview -- Status tracking document - -## Version History - -### Version Numbering - -We use **Semantic Versioning** (SemVer): - -- **MAJOR** (0.x.x): Breaking changes to public API or extension behavior -- **MINOR** (x.1.x): New features, backwards-compatible -- **PATCH** (x.x.1): Bug fixes, backwards-compatible - -### Pre-1.0 Status - -This project is currently **pre-1.0** (alpha/beta): - -- **0.x.x**: API and behavior may change between minor versions -- **1.0.0**: First stable release, API stability guaranteed -- **Target**: Q2 2025 for 1.0.0 release - -### Release Cycle - -- **Major releases**: Every 6-12 months (post-1.0) -- **Minor releases**: Every 1-2 months -- **Patch releases**: As needed for critical bugs -- **Security releases**: Immediate (out-of-band) - -## Migration Guides - -### Upgrading to 0.1.0 - -This is the initial release, no migration needed! - -### Future Upgrades - -Migration guides will be provided for breaking changes: - -- **0.1.x → 0.2.x**: [Coming soon] -- **0.x.x → 1.0.0**: [Coming soon] - -## Deprecated Features - -### Currently Deprecated - -*None yet - this is a new project!* - -### Deprecation Policy - -- **Notice Period**: 3 months minimum before removal -- **Migration Path**: Always provided for deprecated features -- **Documentation**: Clearly marked in docs and code -- **Warnings**: Runtime warnings in VS Code (if applicable) - -## Breaking Changes - -### 0.x.x Series - -Breaking changes may occur in minor versions during pre-1.0 development. We will: - -- Clearly document all breaking changes -- Provide migration instructions -- Offer support in GitHub Discussions - -### Post-1.0 - -Breaking changes will only occur in major version bumps. - -## Security Updates - -Security updates are released **immediately** and out-of-band from regular releases. - -See [SECURITY.md](SECURITY.md) for: -- Security update timeline -- Vulnerability reporting process -- Supported versions - -## Contributing to Changelog - -When contributing, please update this file: - -1. Add your changes under `[Unreleased]` -2. Choose appropriate section (Added, Changed, Fixed, etc.) -3. Write clear, user-focused descriptions -4. Link to relevant issues/PRs when applicable - -Example: -```markdown -### Added -- New pattern for object spread operator ([#123](https://github.com/...)) -- Support for TypeScript decorators ([#456](https://github.com/...)) -``` - -## Changelog Standards - -We follow these standards: - -- **User-focused**: Describe impact on users, not implementation -- **Concise**: One line per change when possible -- **Links**: Include links to issues/PRs for details -- **Grouped**: Group related changes together -- **Emoji-free**: Keep changelog text-only (readability) -- **Date format**: ISO 8601 (YYYY-MM-DD) - -## Acknowledgments - -Thank you to all contributors who help make each release possible! - -See [humans.txt](.well-known/humans.txt) for contributor recognition. - ---- - -**How to stay updated:** - -- Watch this repository for releases -- Subscribe to GitHub notifications -- Follow announcements in GitHub Discussions -- Check the [Releases page](https://github.com/Hyperpolymath/nextgen-languages-evangeliser/releases) - ---- - -**Questions about a release?** - -Ask in [GitHub Discussions](https://github.com/Hyperpolymath/nextgen-languages-evangeliser/discussions) or open an issue. - -[Unreleased]: https://github.com/Hyperpolymath/nextgen-languages-evangeliser/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/Hyperpolymath/nextgen-languages-evangeliser/releases/tag/v0.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d8926b9..adeb620 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -209,7 +209,7 @@ This Code of Conduct is a **living document** and may be updated based on commun - Announced to the community - Open for discussion before finalization -- Versioned and tracked in [CHANGELOG.md](CHANGELOG.md) +- Versioned and tracked in [CHANGELOG.adoc](CHANGELOG.adoc) **Last Updated**: 2024-11-22 **Version**: 1.0 diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index eb045d6..c160882 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -1,20 +1,132 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 -= Contributing Guide += Contributing to Nextgen Languages Evangeliser +:toc: macro -== Getting Started +Thank you for your interest in contributing! 🎉 -1. Fork the repository -2. Create a feature branch from `main` -3. Sign off commits (`git commit -s`) -4. Submit a pull request +This project is *Duolingo / Rosetta Stone for programming languages* — a +language-_comprehension and transfer_ engine that *classifies* cross-language +correspondences so learning transfers from a language you know to one you don't. +It is *not* an IDE, a linter, a universal translator, or a "fix your JavaScript" +tool. Canonical design: link:docs/theory/CORRESPONDENCE-MODEL.adoc[docs/theory/CORRESPONDENCE-MODEL.adoc]. -== Commit Guidelines +We follow the *Compassionate Code Contribution Pledge (CCCP)* and the +*Tri-Perimeter Contribution Framework (TPCF) — Perimeter 3* (see +link:docs/TPCF.adoc[docs/TPCF.adoc]). -* Conventional commits: `type(scope): description` -* Sign all commits (DCO required) -* Atomic, focused commits +toc::[] -== License +== Philosophy: no shame, transfer-first -Contributions licensed under project license. +Our contribution process mirrors our teaching voice — *celebrate / minimise / +better / safety / example*: +. *Celebrate* every contribution, however small. +. *Minimise* — guide gently, never shame. +. *Better* — improve collaboratively. +. *Safety* — keep the data valid and the invariants honest. +. *Example* — show, don't lecture. + +== The main way to contribute: author a cartridge + +The engine is language-agnostic. The community authors *cartridges* — per-language +packs of correspondence _facts_ (data, not code). This is the highest-value, +lowest-barrier contribution. + +A cartridge entry is a *Transition*: a Concept shown through a Form in language A +and a Form in language B, classified by its *CorrespondenceKind* and its *residue* +(what is lost, added, or inverted on the crossing): + +* *cognate* — transfer directly +* *false-friend* — flag the trap (surface matches, intention diverges) +* *antonym* — remap the intuition (related but inverted) +* *alien-realization* — bridge with effort (same goal, foreign machinery) +* *novel* — teach de novo (no prior anchor) +* *vanished* — un-learn / re-route (the habit is gone here) + +=== Steps + +. Read link:docs/theory/CORRESPONDENCE-MODEL.adoc[docs/theory/CORRESPONDENCE-MODEL.adoc]. +. Add or extend a `*.cartridge.json` under `cartridges/`, following + `cartridges/correspondence-cartridge.schema.json`. +. Classify each Transition's kind + residue + per-stratum verdicts, and write the + no-shame narrative. +. Validate: `just validate-cartridges` and `just test`. +. Open a pull request. + +The worked exemplar is +link:cartridges/reference/worked-examples.cartridge.json[cartridges/reference/worked-examples.cartridge.json] +— one correspondence per kind. + +== Development setup + +The runtime surface is *Deno + cartridge data* — no compile step, no Node, no npm. + +=== Prerequisites + +* https://deno.land[Deno] (latest stable) — the only required tool +* https://github.com/casey/just[just] — task runner +* Git +* _Optional, for the engine spine:_ Idris2 (ABI typecheck), Zig (FFI). The + AffineScript host is the future host; its compiler is not yet released. + +=== Common tasks + +[source,bash] +---- +just gui # browser multi-pane correspondence workspace (primary surface) +just test # cartridge invariant tests +just validate-cartridges # schema-validate the cartridges +just build # validate cartridge data (no compile step) +deno run --allow-read bin/evangeliser.js --help # offline CLI +---- + +== Contribution areas + +* *Cartridges* (most welcome) — correspondence facts for any language pair. +* *Engine / interface* (`src/interface/**`, `gui/**`, `bin/`) — reviewed + contributions; discuss architectural changes in an issue first. +* *Engine spine* — the Idris2 ABI (`src/interface/Abi/`), Zig FFI mirror + (`src/interface/ffi/`), AffineScript host binding (`src/interface/host/`). +* *Documentation* — guides, examples, translations. +* *Accessibility* — the workspace targets the Hyperpolymath Accessibility Standard + (Level A → AA): keyboard-only, ≥4.5:1 contrast, ARIA, reduced-motion, + plain-language and glyph layers. + +== Language policy + +Per the Hyperpolymath Standard: *Deno* (not npm/bun), *Justfile* (not Makefile), +*Zig* for FFI, *Idris2* for ABI/proofs, *AffineScript* as the future host. +*TypeScript, Node.js, Python, Go are banned.* ReScript is a legacy _target_ +language only (the ReScript host was removed) — never reintroduce `.res` host code. +See link:CLAUDE.md[CLAUDE.md] and link:.claude/CLAUDE.md[.claude/CLAUDE.md] for the +full policy and JavaScript-glue exemptions. + +== Commits & pull requests + +* *Conventional Commits*: `type(scope): description` (`feat`, `fix`, `docs`, + `refactor`, `test`, `chore`, …). +* *Sign off* every commit (DCO): `git commit -s`. +* Keep commits atomic and focused; describe the change clearly in the PR. +* CI runs the cartridge tests, schema validation, the `panic-attack` gate, and the + governance/policy checks. + +== Security + +Never commit secrets. Report vulnerabilities privately — see +link:SECURITY.md[SECURITY.md]. + +== Code of Conduct + +This project follows the *Compassionate Code Contribution Pledge* — see +link:CODE_OF_CONDUCT.md[CODE_OF_CONDUCT.md]. In short: never shame developers, +celebrate existing knowledge, assume good intent, foster psychological safety. + +== Licence + +By contributing, you agree that your contributions are licensed under the project +licence — see link:LICENSE[LICENSE]. Code is *MPL-2.0*; documentation is +*CC-BY-SA-4.0*. You retain copyright of your contributions. + +[.text-center] +*Thank you for contributing!* Remember the motto: _Learn — to love it!_ 💙 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 0cba703..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,130 +0,0 @@ - -# Contributing to Nextgen Languages Evangeliser - -Thank you for your interest in contributing! 🎉 - -This project is **Duolingo / Rosetta Stone for programming languages** — a -language-*comprehension and transfer* engine that **classifies** cross-language -correspondences so learning transfers from a language you know to one you don't. -It is **not** an IDE, a linter, a universal translator, or a "fix your -JavaScript" tool. Canonical design: [`docs/theory/CORRESPONDENCE-MODEL.adoc`](docs/theory/CORRESPONDENCE-MODEL.adoc). - -We follow the **Compassionate Code Contribution Pledge (CCCP)** and the -**Tri-Perimeter Contribution Framework (TPCF) — Perimeter 3** (see -[`docs/TPCF.adoc`](docs/TPCF.adoc)). - -## Philosophy: no shame, transfer-first - -Our contribution process mirrors our teaching voice — **celebrate / minimise / -better / safety / example**: - -1. **Celebrate** every contribution, however small. -2. **Minimise** — guide gently, never shame. -3. **Better** — improve collaboratively. -4. **Safety** — keep the data valid and the invariants honest. -5. **Example** — show, don't lecture. - -## The main way to contribute: author a cartridge - -The engine is language-agnostic. The community authors **cartridges** — -per-language packs of correspondence *facts* (data, not code). This is the -highest-value, lowest-barrier contribution. - -A cartridge entry is a **Transition**: a Concept shown through a Form in language -A and a Form in language B, classified by its **CorrespondenceKind** and its -**residue** (what is lost, added, or inverted on the crossing): - -- **cognate** — transfer directly -- **false-friend** — flag the trap (surface matches, intention diverges) -- **antonym** — remap the intuition (related but inverted) -- **alien-realization** — bridge with effort (same goal, foreign machinery) -- **novel** — teach de novo (no prior anchor) -- **vanished** — un-learn / re-route (the habit is gone here) - -### Steps - -1. Read [`docs/theory/CORRESPONDENCE-MODEL.adoc`](docs/theory/CORRESPONDENCE-MODEL.adoc). -2. Add or extend a `*.cartridge.json` under `cartridges/`, following - `cartridges/correspondence-cartridge.schema.json`. -3. Classify each Transition's kind + residue + per-stratum verdicts, and write - the no-shame narrative. -4. Validate: `just validate-cartridges` and `just test`. -5. Open a pull request. - -The worked exemplar is -[`cartridges/reference/worked-examples.cartridge.json`](cartridges/reference/worked-examples.cartridge.json) -— one correspondence per kind. - -## Development setup - -The runtime surface is **Deno + cartridge data** — no compile step, no Node, no -npm. - -### Prerequisites - -- [Deno](https://deno.land) (latest stable) — the only required tool -- [just](https://github.com/casey/just) — task runner -- Git -- _Optional, for the engine spine:_ Idris2 (ABI typecheck), Zig (FFI). The - AffineScript host is the future host; its compiler is not yet released. - -### Common tasks - -```bash -just gui # browser multi-pane correspondence workspace (primary surface) -just test # cartridge invariant tests -just validate-cartridges # schema-validate the cartridges -just build # validate cartridge data (no compile step) -deno run --allow-read bin/evangeliser.js --help # offline CLI -``` - -## Contribution areas - -- **Cartridges** (most welcome) — correspondence facts for any language pair. -- **Engine / interface** (`src/interface/**`, `gui/**`, `bin/`) — reviewed - contributions; discuss architectural changes in an issue first. -- **Engine spine** — the Idris2 ABI (`src/interface/Abi/`), Zig FFI mirror - (`src/interface/ffi/`), AffineScript host binding (`src/interface/host/`). -- **Documentation** — guides, examples, translations. -- **Accessibility** — the workspace targets the Hyperpolymath Accessibility - Standard (Level A → AA): keyboard-only, ≥4.5:1 contrast, ARIA, reduced-motion, - plain-language and glyph layers. - -## Language policy - -Per the Hyperpolymath Standard: **Deno** (not npm/bun), **Justfile** (not -Makefile), **Zig** for FFI, **Idris2** for ABI/proofs, **AffineScript** as the -future host. **TypeScript, Node.js, Python, Go are banned.** ReScript is a legacy -*target* language only (the ReScript host was removed) — never reintroduce `.res` -host code. See [`CLAUDE.md`](CLAUDE.md) and [`.claude/CLAUDE.md`](.claude/CLAUDE.md) -for the full policy and JavaScript-glue exemptions. - -## Commits & pull requests - -- **Conventional Commits**: `type(scope): description` (`feat`, `fix`, `docs`, - `refactor`, `test`, `chore`, …). -- **Sign off** every commit (DCO): `git commit -s`. -- Keep commits atomic and focused; describe the change clearly in the PR. -- CI runs the cartridge tests, schema validation, the `panic-attack` gate, and - the governance/policy checks. - -## Security - -Never commit secrets. Report vulnerabilities privately — see -[`SECURITY.md`](SECURITY.md). - -## Code of Conduct - -This project follows the **Compassionate Code Contribution Pledge** — see -[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). In short: never shame developers, -celebrate existing knowledge, assume good intent, foster psychological safety. - -## License - -By contributing, you agree that your contributions are licensed under the -project licence — see [`LICENSE`](LICENSE). You retain copyright of your -contributions. - ---- - -**Thank you for contributing!** Remember the motto: *Learn — to love it!* 💙 diff --git a/HANDOFF.adoc b/HANDOFF.adoc index 20770ff..c6b8440 100644 --- a/HANDOFF.adoc +++ b/HANDOFF.adoc @@ -49,10 +49,9 @@ deno run --allow-read bin/evangeliser.js --kind false-friend # offline CLI (JTV is the canonical alien-realization / novel exemplar). . Begin the *AffineScript host application* against the Idris2 ABI / Zig FFI seams (needs the AffineScript compiler). -. Owner-only follow-up (flagged, not done): the `CONTRIBUTING.md` / `CHANGELOG.md` - `.md`-vs-`.adoc` format choice (these GitHub-required files have clean `.adoc` - twins; pick one per file). The licence-label drift is *resolved* per owner - direction — see the Licensing note below. +. Docs are AsciiDoc: `CONTRIBUTING` and `CHANGELOG` are now `.adoc` (the `.md` + duplicates were retired per owner direction). The licence-label drift is also + *resolved* — see the Licensing note below. == Guardrails (non-negotiable) diff --git a/Justfile b/Justfile index 85d0e7f..8584502 100644 --- a/Justfile +++ b/Justfile @@ -109,11 +109,11 @@ validate-structure: validate-docs: @echo "📚 Checking documentation..." @test -f README.adoc || (echo "❌ Missing README.adoc" && exit 1) - @test -f CONTRIBUTING.md || (echo "❌ Missing CONTRIBUTING.md" && exit 1) + @test -f CONTRIBUTING.adoc || (echo "❌ Missing CONTRIBUTING.adoc" && exit 1) @test -f CODE_OF_CONDUCT.md || (echo "❌ Missing CODE_OF_CONDUCT.md" && exit 1) @test -f SECURITY.md || (echo "❌ Missing SECURITY.md" && exit 1) @test -f MAINTAINERS.adoc || (echo "❌ Missing MAINTAINERS.adoc" && exit 1) - @test -f CHANGELOG.md || (echo "❌ Missing CHANGELOG.md" && exit 1) + @test -f CHANGELOG.adoc || (echo "❌ Missing CHANGELOG.adoc" && exit 1) @test -f CLAUDE.md || (echo "❌ Missing CLAUDE.md" && exit 1) @echo "✅ Documentation complete" diff --git a/README.adoc b/README.adoc index 1753fe0..01e5081 100644 --- a/README.adoc +++ b/README.adoc @@ -246,17 +246,17 @@ See link:docs/RSR_COMPLIANCE.adoc[RSR_COMPLIANCE.adoc] for full details. == Documentation * link:docs/theory/CORRESPONDENCE-MODEL.adoc[CORRESPONDENCE-MODEL.adoc] - Canonical design spec -* link:CONTRIBUTING.md[CONTRIBUTING.md] - How to contribute (incl. cartridge authoring) +* link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] - How to contribute (incl. cartridge authoring) * link:CODE_OF_CONDUCT.md[CODE_OF_CONDUCT.md] - Community guidelines * link:SECURITY.md[SECURITY.md] - Security policies -* link:CHANGELOG.md[CHANGELOG.md] - Version history +* link:CHANGELOG.adoc[CHANGELOG.adoc] - Version history * link:docs/RSR_COMPLIANCE.adoc[RSR_COMPLIANCE.adoc] - RSR framework compliance * link:docs/TPCF.adoc[TPCF.adoc] - Contribution framework * link:ROADMAP.adoc[ROADMAP.adoc] - Re-point sequence & milestones == Contributing -We welcome contributions! See link:CONTRIBUTING.md[CONTRIBUTING.md] for: +We welcome contributions! See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for: * Code of Conduct * Development setup diff --git a/SECURITY.md b/SECURITY.md index e8dd1d2..25baac0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -212,7 +212,7 @@ When contributing code: - [ ] Input validation for all user data - [ ] Resource limits respected -See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines. +See [CONTRIBUTING.adoc](CONTRIBUTING.adoc) for full guidelines. ## 🏆 Security Compliance diff --git a/docs/RSR_COMPLIANCE.adoc b/docs/RSR_COMPLIANCE.adoc index 5cf308b..59ae1cb 100644 --- a/docs/RSR_COMPLIANCE.adoc +++ b/docs/RSR_COMPLIANCE.adoc @@ -34,7 +34,7 @@ toc::[] == 4. Complete documentation -* `README.adoc`, `EXPLAINME.adoc`, `ROADMAP.adoc`, `CONTRIBUTING.md`, +* `README.adoc`, `EXPLAINME.adoc`, `ROADMAP.adoc`, `CONTRIBUTING.adoc`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, `MAINTAINERS.adoc`, `CHANGELOG`, `CLAUDE.md`, and the canonical design spec `docs/theory/CORRESPONDENCE-MODEL.adoc`. From 0f56af6ca981dab75744592db1b89dfdac38b916 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 22:29:45 +0000 Subject: [PATCH 17/17] Polish batch: 2nd cartridge, GUI smoke test, finish .adoc tidy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small follow-ups, done in turn: 1. Second example cartridge (cartridges/reference/python-ruby.cartridge.json) — 6 Python->Ruby transitions across cognate / false-friend / alien-realization / novel / vanished, dogfooding the cartridge contract on a fresh language pair to show the engine is language-agnostic. Cartridge invariants: 59 -> 107 checks. 2. GUI smoke test (test/gui_smoke_test.js) — gui/server.js now exports handler + loadCartridges and opens a listener only under import.meta.main, so the handler is tested in-process (no socket): static shell, /app.js bundle, /api/cartridges shape, and a clean 404. Wired into `deno task test` (+ test:gui) and a `just test-gui` recipe. 3. Finished the .adoc migration: TOPOLOGY.md -> TOPOLOGY.adoc (data-flow section corrected from the old JS->migration-report framing to the cartridge pipeline) and TEST-NEEDS.md -> TEST-NEEDS.adoc (coverage refreshed). Root docs are now AsciiDoc except the GitHub-required CODE_OF_CONDUCT.md / SECURITY.md. Verified: deno fmt --check, deno lint, deno task test (cartridge invariants + GUI smoke), deno task build (2/2 cartridges), and a live GUI probe — all green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap --- Justfile | 7 +- TEST-NEEDS.adoc | 45 +++++++ TEST-NEEDS.md | 37 ------ TOPOLOGY.adoc | 51 ++++++++ TOPOLOGY.md | 30 ----- .../reference/python-ruby.cartridge.json | 122 ++++++++++++++++++ deno.json | 5 +- gui/server.js | 67 +++++----- test/gui_smoke_test.js | 86 ++++++++++++ 9 files changed, 350 insertions(+), 100 deletions(-) create mode 100644 TEST-NEEDS.adoc delete mode 100644 TEST-NEEDS.md create mode 100644 TOPOLOGY.adoc delete mode 100644 TOPOLOGY.md create mode 100644 cartridges/reference/python-ruby.cartridge.json create mode 100644 test/gui_smoke_test.js diff --git a/Justfile b/Justfile index 8584502..302cc40 100644 --- a/Justfile +++ b/Justfile @@ -70,11 +70,16 @@ gui: # === TESTING === -# Run all tests +# Run all tests (cartridge invariants + GUI smoke test) test: @echo "🧪 Running tests..." deno task test +# Run just the GUI smoke test (server handler: shell, bundle, cartridge API) +test-gui: + @echo "🖥️ GUI smoke test..." + @deno task test:gui + # === VALIDATION === # Run full validation diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc new file mode 100644 index 0000000..d843d7b --- /dev/null +++ b/TEST-NEEDS.adoc @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += TEST-NEEDS — nextgen-languages-evangeliser + +CRG Grade: C (achieved 2026-04-04). + +== Current test state + +[cols="2,1,3",options="header"] +|=== +| Category | Count | Notes + +| Cartridge invariant tests | 1 suite | `test/run_all.js` (Deno) — semantic invariants over every cartridge +| GUI smoke test | 1 suite | `test/gui_smoke_test.js` (Deno) — server handler: shell, bundle, cartridge API +| Cartridge schema validation | 1 | `scripts/validate-cartridges.js` (Deno + ajv, JSON-Schema 2020-12) +| Idris2 ABI typecheck | 1 | `abi.ipkg` / `src/interface/Abi/*.idr` (CI: `idris2-abi.yml`) +| Test framework | Deno + ajv + Idris2 | host-language unit tests were removed with the ReScript host +|=== + +== What's covered + +* Every correspondence is classified by one of the six CorrespondenceKinds +* Well-formedness: Forms (language + surface), residue shapes, strata names/booleans +* False-friend signature (surface holds ∧ intention diverges) — mirrors `Abi.Correspondence.isFalseFriendShape` +* Residue direction for novel (absent-source) / vanished (absent-target) +* The worked-examples reference cartridge demonstrates all six kinds +* Cartridge JSON-Schema conformance (ajv) +* GUI server handler: static shell, `/app.js` bundle, `/api/cartridges` shape, and a clean 404 +* Idris2 ABI typechecks (no `believe_me` / `assert_total`) + +== Still missing (for CRG B+) + +* Headless-DOM rendering test of `gui/app.js` (the smoke test covers the server / API, not the browser render) +* Property-based generation over cartridge facts +* Performance benchmarks +* End-to-end "walk a correspondence" flow test + +== Run tests + +[source,bash] +---- +just test # cartridge invariants + GUI smoke test +just test-gui # GUI smoke test only +deno task test # same as `just test` +deno task validate # cartridge schema validation +---- diff --git a/TEST-NEEDS.md b/TEST-NEEDS.md deleted file mode 100644 index ea9ed7b..0000000 --- a/TEST-NEEDS.md +++ /dev/null @@ -1,37 +0,0 @@ -# TEST-NEEDS.md — nextgen-languages-evangeliser - -## CRG Grade: C — ACHIEVED 2026-04-04 - -## Current Test State - -| Category | Count | Notes | -|----------|-------|-------| -| Cartridge invariant tests | 1 suite | `test/run_all.js` (Deno) — 59 assertions over the correspondence facts | -| Cartridge schema validation | 1 | `scripts/validate-cartridges.js` (Deno + ajv, JSON-Schema 2020-12) | -| Idris2 ABI typecheck | 1 | `abi.ipkg` / `src/interface/Abi/*.idr` (CI: idris2-abi.yml) | -| Test framework | Deno + ajv + Idris2 | host-language unit tests removed with the ReScript host | - -## What's Covered - -- [x] Every correspondence is classified by one of the six CorrespondenceKinds -- [x] Well-formedness: Forms (language + surface), residue shapes, strata names/booleans -- [x] False-friend signature (surface holds ∧ intention diverges) — mirrors `Abi.Correspondence.isFalseFriendShape` -- [x] Residue direction for novel (absent-source) / vanished (absent-target) -- [x] Reference cartridge demonstrates all six kinds -- [x] Cartridge JSON-Schema conformance (ajv) -- [x] Idris2 ABI typechecks (no `believe_me` / `assert_total`) - -## Still Missing (for CRG B+) - -- [ ] Property-based pattern generation -- [ ] Integration tests with external tools -- [ ] Performance benchmarks -- [ ] End-to-end evangelism flow tests - -## Run Tests - -```bash -deno task test # cartridge invariant tests -deno task validate # cartridge schema validation -just test # same, via the Justfile -``` diff --git a/TOPOLOGY.adoc b/TOPOLOGY.adoc new file mode 100644 index 0000000..9dfff32 --- /dev/null +++ b/TOPOLOGY.adoc @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) += TOPOLOGY — nextgen-languages-evangeliser + +== Purpose + +Cross-language *comprehension and transfer* engine — Duolingo / Rosetta Stone for +programming languages. It *classifies* correspondences between a known language and +a target so learning transfers (it does not translate, lint, or build IDEs). +Flagship / future host: AffineScript. Legacy target: ReScript (host removed). Runs +on Deno; the browser multi-pane workspace is the primary surface. + +== Module map + +[source] +---- +nextgen-languages-evangeliser/ +├── bin/evangeliser.js # Offline CLI (Deno; reads cartridges) +├── src/ +│ ├── cartridges.js # Shared cartridge loader (Deno glue) +│ └── interface/ # Engine spine: Idris2 ABI + Zig FFI + AffineScript host binding +├── cartridges/ # Correspondence facts: schema + reference pack +├── gui/ # Browser multi-pane workspace (primary surface) +├── scripts/ # Deno utilities (cartridge validator, GUI launcher) +├── test/ # Cartridge invariant tests + GUI smoke test +├── config.ncl # Nickel configuration +├── docs/ # Documentation (AsciiDoc) +├── Justfile # Task runner recipes +└── deno.json # Deno module config +---- + +== Data flow + +[source] +---- +cartridges/**/*.cartridge.json (authored correspondence facts) + │ + ▼ + shared loader (src/cartridges.js) + │ + ├─► gui/server.js ──► browser workspace (index · forms · classification) + ├─► bin/evangeliser.js ──► CLI (--kind / --find / --json) + └─► test + scripts ──► invariants + schema validation + │ + ▼ + (downstream, planned) octads ──► VeriSimDB ──► PanLL panels +---- + +The classification vocabulary (six CorrespondenceKinds, strata, residue shapes) is +specified in the Idris2 ABI (`src/interface/Abi/`) and mirrored by the cartridge +schema. The engine *classifies* correspondences; it does not transform source. diff --git a/TOPOLOGY.md b/TOPOLOGY.md deleted file mode 100644 index 2218758..0000000 --- a/TOPOLOGY.md +++ /dev/null @@ -1,30 +0,0 @@ - - -# TOPOLOGY.md — nextgen-languages-evangeliser - -## Purpose - -Cross-language *comprehension and transfer* engine — Duolingo / Rosetta Stone for programming languages. It *classifies* correspondences between a known language and a target so learning transfers (it does not translate, lint, or build IDEs). Flagship / future host: AffineScript. Legacy target: ReScript (host removed). Runs on Deno; the browser multi-pane workspace is the primary surface. - -## Module Map - -``` -nextgen-languages-evangeliser/ -├── bin/ -│ └── evangeliser.js # Offline CLI (Deno; reads cartridges) -├── src/interface/ # Engine spine: Idris2 ABI + Zig FFI + AffineScript host binding -├── cartridges/ # Correspondence facts (schema + reference pack) -├── gui/ # Browser multi-pane workspace (primary surface) -├── config.ncl # Nickel configuration -├── docs/ # Usage and output documentation -├── Justfile # Task runner recipes -└── deno.json # Deno module config -``` - -## Data Flow - -``` -[JavaScript/TypeScript codebase] ──► [evangeliser.js analysis] ──► [Migration report (per target)] - [config.ncl] ──► │ - [Multi-target advocacy content generator] ──► [Reports / stats] -``` diff --git a/cartridges/reference/python-ruby.cartridge.json b/cartridges/reference/python-ruby.cartridge.json new file mode 100644 index 0000000..069c544 --- /dev/null +++ b/cartridges/reference/python-ruby.cartridge.json @@ -0,0 +1,122 @@ +{ + "$schema": "../correspondence-cartridge.schema.json", + "spdx": "MPL-2.0", + "name": "python-ruby", + "version": "0.1.0", + "description": "Second reference cartridge: Python -> Ruby. Dogfoods the cartridge contract on a fresh language pair to show the engine is language-agnostic. Covers cognate / false-friend / alien-realization / novel / vanished from a Python learner's standpoint.", + "languages": ["python", "ruby"], + "transitions": [ + { + "concept": "function-definition", + "from": { "language": "python", "surface": "def greet(name):\n ..." }, + "to": { "language": "ruby", "surface": "def greet(name)\n ...\nend" }, + "kind": "cognate", + "residue": { + "shape": "none", + "note": "Same keyword (`def`) and intent; Ruby closes the body with `end` where Python uses indentation." + }, + "strata": [ + { "stratum": "surface", "holds": true }, + { "stratum": "intention", "holds": true } + ], + "narrative": { + "celebrate": "You already define functions with `def` — that word is the same in Ruby.", + "better": "Your `def` intuition transfers directly.", + "example": "def greet(name): ... <-> def greet(name) ... end" + } + }, + { + "concept": "string-interpolation", + "from": { "language": "python", "surface": "f\"Hi {name}\"" }, + "to": { "language": "ruby", "surface": "\"Hi #{name}\"" }, + "kind": "cognate", + "residue": { + "shape": "none", + "note": "Same intent (interpolate a value into a string); Ruby uses `#{}` and needs no `f` prefix." + }, + "strata": [ + { "stratum": "surface", "holds": true }, + { "stratum": "intention", "holds": true } + ], + "narrative": { + "celebrate": "f-strings are second nature to you — interpolation carries straight over.", + "better": "Drop the `f` and write `#{}` instead of `{}`.", + "example": "f\"Hi {name}\" <-> \"Hi #{name}\"" + } + }, + { + "concept": "empty-collection-truthiness", + "from": { "language": "python", "surface": "if items: # empty list is falsy" }, + "to": { "language": "ruby", "surface": "if items # empty array is TRUTHY" }, + "kind": "false-friend", + "residue": { + "shape": "lossy", + "note": "In Ruby only `nil` and `false` are falsy; `[]`, `\"\"`, and `0` are all truthy. The Python habit of testing a collection for emptiness via truthiness silently breaks." + }, + "strata": [ + { "stratum": "surface", "holds": true }, + { "stratum": "intention", "holds": false } + ], + "narrative": { + "celebrate": "You've leaned on truthiness to check for empty/zero for years — that's a real, useful instinct.", + "safety": "In Ruby that instinct misfires: `if items` is true even when empty. Test emptiness explicitly.", + "example": "Python: if items: -> Ruby: if items.any? (or: unless items.empty?)" + } + }, + { + "concept": "map-over-collection", + "from": { "language": "python", "surface": "[f(x) for x in xs]" }, + "to": { "language": "ruby", "surface": "xs.map { |x| f(x) }" }, + "kind": "alien-realization", + "residue": { + "shape": "lossy", + "note": "Same intention (transform each element) realised through different machinery: a comprehension expression vs a method taking a block." + }, + "strata": [ + { "stratum": "intention", "holds": true }, + { "stratum": "structure", "holds": false } + ], + "narrative": { + "celebrate": "You think in comprehensions — that mapping intent is exactly right.", + "better": "Ruby spells it as a block passed to `map`; once it clicks, blocks are everywhere.", + "example": "[f(x) for x in xs] -> xs.map { |x| f(x) }" + } + }, + { + "concept": "symbol", + "from": { "language": "python", "surface": "(no analog)" }, + "to": { "language": "ruby", "surface": ":status" }, + "kind": "novel", + "residue": { + "shape": "absent-source", + "note": "Python has no symbol type. The nearest habits — interned strings, enum members, dict keys — are not the same thing; learn the symbol fresh." + }, + "strata": [ + { "stratum": "intention", "holds": false } + ], + "narrative": { + "celebrate": "Coming from Python you have great instincts for dict keys and enums.", + "safety": "A Ruby symbol (`:status`) is its own immutable, interned identifier — not a string. Learn it as a new thing rather than mapping it onto strings.", + "example": "{ status: :active } # :active is a symbol, not \"active\"" + } + }, + { + "concept": "explicit-self-parameter", + "from": { "language": "python", "surface": "def m(self):\n ..." }, + "to": { "language": "ruby", "surface": "def m\n ...\nend" }, + "kind": "vanished", + "residue": { + "shape": "absent-target", + "note": "Ruby has no explicit `self` parameter; the receiver is implicit. The Python habit of writing `self` first in every method has nowhere to go — re-route it." + }, + "strata": [ + { "stratum": "intention", "holds": false } + ], + "narrative": { + "celebrate": "Writing `self` first is muscle memory from Python — totally reasonable.", + "safety": "In Ruby that parameter vanishes: methods take only their real arguments, and `self` is implicit (use it bare when you need the receiver).", + "example": "Python: def m(self): ... -> Ruby: def m ... end" + } + } + ] +} diff --git a/deno.json b/deno.json index 4c6a34b..f8d740c 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,8 @@ "gui": "deno run --allow-read --allow-net gui/server.js", "lint": "deno lint", "fmt": "deno fmt", - "test": "deno run --allow-read test/run_all.js", + "test": "deno run --allow-read test/run_all.js && deno run --allow-read test/gui_smoke_test.js", + "test:gui": "deno run --allow-read test/gui_smoke_test.js", "pre-commit": "deno task lint && deno task fmt --check" }, "fmt": { @@ -20,7 +21,7 @@ "semiColons": false, "singleQuote": false, "proseWrap": "preserve", - "include": ["bin/", "src/cartridges.js", "test/run_all.js", "deno.json"] + "include": ["bin/", "src/cartridges.js", "test/", "deno.json"] }, "lint": { "rules": { diff --git a/gui/server.js b/gui/server.js index b8f32ce..016c9a5 100644 --- a/gui/server.js +++ b/gui/server.js @@ -74,38 +74,45 @@ async function handler(req) { } } -const requestedPort = Number(argValue("--port", envPort() ?? "8765")); -const hostname = argValue("--host", "127.0.0.1"); -let server; -let port = requestedPort; -for (let attempt = 0; attempt < 20; attempt += 1) { - try { - port = requestedPort + attempt; - server = Deno.serve({ hostname, port, onListen: () => {} }, handler); - break; - } catch (err) { - if (String(err.message ?? err).includes("Address already in use")) continue; - throw err; +// Exported for in-process testing (test/gui_smoke_test.js). The listener below +// starts only when this file is run directly, not when imported. +export { handler, loadCartridges }; + +// --- server startup (only when run directly) -------------------------------- +if (import.meta.main) { + const requestedPort = Number(argValue("--port", envPort() ?? "8765")); + const hostname = argValue("--host", "127.0.0.1"); + let server; + let port = requestedPort; + for (let attempt = 0; attempt < 20; attempt += 1) { + try { + port = requestedPort + attempt; + server = Deno.serve({ hostname, port, onListen: () => {} }, handler); + break; + } catch (err) { + if (String(err.message ?? err).includes("Address already in use")) continue; + throw err; + } + } + if (!server) { + throw new Error(`No free port from ${requestedPort} to ${requestedPort + 19}`); } -} -if (!server) { - throw new Error(`No free port from ${requestedPort} to ${requestedPort + 19}`); -} -const addr = `http://${hostname}:${port}/`; -console.log(`Correspondence workspace: ${addr}`); + const addr = `http://${hostname}:${port}/`; + console.log(`Correspondence workspace: ${addr}`); -if (Deno.args.includes("--open")) { - setTimeout(() => { - for (const cmd of [["sensible-browser", addr], ["gio", "open", addr], ["xdg-open", addr]]) { - try { - new Deno.Command(cmd[0], { args: cmd.slice(1), stdout: "null", stderr: "null" }).spawn(); - return; - } catch { - // try the next opener + if (Deno.args.includes("--open")) { + setTimeout(() => { + for (const cmd of [["sensible-browser", addr], ["gio", "open", addr], ["xdg-open", addr]]) { + try { + new Deno.Command(cmd[0], { args: cmd.slice(1), stdout: "null", stderr: "null" }).spawn(); + return; + } catch { + // try the next opener + } } - } - }, 250); -} + }, 250); + } -await server.finished; + await server.finished; +} diff --git a/test/gui_smoke_test.js b/test/gui_smoke_test.js new file mode 100644 index 0000000..904854a --- /dev/null +++ b/test/gui_smoke_test.js @@ -0,0 +1,86 @@ +#!/usr/bin/env -S deno run --allow-read +// SPDX-License-Identifier: MPL-2.0 +// GUI smoke test — exercises the workspace server's request handler in-process +// (no socket, no network): the static shell, the client bundle, and the +// cartridge API. gui/server.js exports `handler` and only opens a listener under +// import.meta.main, so importing it here is side-effect-free. +// +// deno run --allow-read test/gui_smoke_test.js +// deno task test / just test-gui + +import { handler } from "../gui/server.js" + +const KINDS = [ + "cognate", + "false-friend", + "antonym", + "alien-realization", + "novel", + "vanished", +] + +let passed = 0 +let failed = 0 +function check(label, cond) { + if (cond) { + passed++ + } else { + failed++ + console.error(` ✗ ${label}`) + } +} + +function get(path) { + return handler(new Request(`http://localhost${path}`)) +} + +console.log("Nextgen Languages Evangeliser — GUI smoke test\n") + +// 1. the static shell +{ + const res = await get("/") + check("/ -> 200", res.status === 200) + check("/ is html", (res.headers.get("content-type") ?? "").includes("text/html")) + const html = await res.text() + check("/ has the workspace title", html.includes("Correspondence Workspace")) + for (const id of ["index", "paneForms", "paneClassify"]) { + check(`/ has #${id} pane`, html.includes(`id="${id}"`)) + } + check("/ loads /app.js", html.includes("/app.js")) +} + +// 2. the client bundle +{ + const res = await get("/app.js") + check("/app.js -> 200", res.status === 200) + check("/app.js is javascript", (res.headers.get("content-type") ?? "").includes("javascript")) + check("/app.js carries an SPDX header", (await res.text()).includes("SPDX-License-Identifier")) +} + +// 3. the cartridge API +{ + const res = await get("/api/cartridges") + check("/api/cartridges -> 200", res.status === 200) + const body = await res.json() + check("api: cartridges is an array", Array.isArray(body.cartridges)) + const transitions = (body.cartridges ?? []).flatMap((c) => c.transitions ?? []) + check("api: at least one correspondence", transitions.length >= 1) + for (const t of transitions) { + check(`api: '${t.concept}' has a valid kind`, KINDS.includes(t.kind)) + check(`api: '${t.concept}' has from+to`, !!t.from?.language && !!t.to?.language) + } +} + +// 4. unknown route is a clean 404 +{ + const res = await get("/does-not-exist") + check("/does-not-exist -> 404", res.status === 404) + await res.body?.cancel() +} + +console.log(`\n${passed} passed, ${failed} failed`) +if (failed) { + console.error("\n❌ GUI smoke test failed") + Deno.exit(1) +} +console.log("✅ GUI smoke test passed")