From 127694b8a8ed3a520e486d3e5b8e47adb1c55540 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 11 May 2026 09:20:42 +0200 Subject: [PATCH] port(rsr-certifier-vscode): rewrite extension.ts as extension.affine (affinescript#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the standards side of affinescript#64 (port external VS Code extensions to .affine). Unblock conditions: - affinescript#35 (Node-target codegen + Vscode.affine bindings) - affinescript#102 (binding-surface expansion for the rsr-certifier port — status bar, diagnostics, webview, clipboard, fs.writeFile, onDidSaveTextDocument, workspace folders, uri helpers, path helpers) Layout: src/extension.affine AffineScript source-of-truth src/affine-vscode-adapter.cjs Vendored JS adapter (copy of affinescript:packages/affine-vscode/mod.js) src/index.cjs Runtime entry point. Installs `extraImports` on the wasm shim so the AffineScript extern bindings resolve to real vscode/lc API calls. out/extension.cjs Generated by `npm run compile`. package.json `main` -> ./src/index.cjs; `compile` invokes affinescript compile; TS devDeps + eslint config removed; version 0.1.0 -> 0.2.0. Removed: src/extension.ts (replaced) tsconfig.json (no TS to compile) Acceptance-criteria coverage from affinescript#64: Activation: shim.activate wires ExtensionContext as a handle and instantiates the wasm with extraImports. Command registration: rsr.{checkCompliance,initConfig,showReport, generateBadge} registered via Vscode::registerCommand with wasm-table indices. LSP wiring: VscodeLanguageClient::newLanguageClient + languageClientStart, using rsr.serverPath config or extensionAbsolutePath fallback to server/rsr-lsp[.exe] (platform-aware). Clean disposal: shim.deactivate exported; status-bar item, diagnostic collection, and all command disposables join ctx.subscriptions for host-managed release. Feature-parity notes (degradations under the current synchronous extern-call ABI — affinescript#102 README documents the omissions): - Thenable-returning vscode APIs are not bound: vscode.window.withProgress(opts, async task) LanguageClient.sendRequest(method, params) Effect: the "live in-process compliance results" path is removed. All four commands shell out to the `rsr` CLI in a fresh terminal (the same fallback path the original TS extension already used for the no-LSP case). The status bar shows a static initial value instead of being mutated on every check result. The diagnostic collection is created and disposed but not populated. - onDidSaveTextDocument drops the TextDocument arg at the FFI boundary (handlers are zero-arg wasm-table thunks). To avoid "spawn terminal on every save anywhere", the on-save handler is a no-op; users who want check-on-save can invoke the command manually. The rsr.checkOnSave config flag is honoured at the *registration* level so the wiring is in place once an async-extern ABI lands. - The webview report renders a constant placeholder shape rather than per-check data (the per-check data flowed from the sendRequest path). TS-exemption table note: The repo's `.claude/CLAUDE.md` "TypeScript Exemptions" table currently lists only `avow-protocol/telegram-bot/avow-telegram-bot/**`. The rsr-certifier extension.ts was *not* a per-repo exemption row — it was covered by the universal-allowlist `*vscode*` directory-segment pattern in .github/workflows/rsr-antipattern.yml. Removing the .ts file therefore does not require a CLAUDE.md table edit; the file simply ceases to exist. Co-Authored-By: Claude Opus 4.7 --- .../extensions/vscode/out/extension.cjs | 96 +++++ .../extensions/vscode/package.json | 17 +- .../vscode/src/affine-vscode-adapter.cjs | 348 ++++++++++++++++ .../extensions/vscode/src/extension.affine | 305 ++++++++++++++ .../extensions/vscode/src/extension.ts | 379 ------------------ .../extensions/vscode/src/index.cjs | 29 ++ .../extensions/vscode/tsconfig.json | 14 - 7 files changed, 781 insertions(+), 407 deletions(-) create mode 100644 rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/out/extension.cjs create mode 100644 rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/affine-vscode-adapter.cjs create mode 100644 rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.affine delete mode 100644 rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.ts create mode 100644 rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/index.cjs delete mode 100644 rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/tsconfig.json diff --git a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/out/extension.cjs b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/out/extension.cjs new file mode 100644 index 00000000..61997db7 --- /dev/null +++ b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/out/extension.cjs @@ -0,0 +1,96 @@ +// Generated by AffineScript. Do not edit. +// Node-CJS shim wrapping the compiled .wasm module so VS Code's extension +// host (or any CJS consumer) can require() it directly. +// +// Issue #35 Phase 1 — no vscode API bindings yet (that's Phase 2 via +// stdlib/Vscode.affine). The Wasm module's `activate` and `deactivate` +// exports become exports.activate / exports.deactivate. + +"use strict"; + +const _wasmBase64 = "AGFzbQEAAAABdhVgBH9/f38Bf2ACf38Bf2ABfwF/YAN/f38Bf2AAAX9gBX9/f39/AX9gA39/fwF/YAR/f39/AX9gBX9/f39/AX9gAn9/AX9gAAF/YAABf2AAAX9gAAF/YAABf2ABfwF/YAF/AX9gAX8Bf2AAAX9gAX8Bf2AAAX8CxwkrFndhc2lfc25hcHNob3RfcHJldmlldzEIZmRfd3JpdGUAAAZWc2NvZGUPcmVnaXN0ZXJDb21tYW5kAAEGVnNjb2RlEGdldENvbmZpZ3VyYXRpb24AAgZWc2NvZGUWd29ya3NwYWNlQ29uZmlnR2V0Qm9vbAADBlZzY29kZRh3b3Jrc3BhY2VDb25maWdHZXRTdHJpbmcAAwZWc2NvZGUQc2hvd0Vycm9yTWVzc2FnZQACBlZzY29kZRJzaG93V2FybmluZ01lc3NhZ2UAAgZWc2NvZGUWc2hvd0luZm9ybWF0aW9uTWVzc2FnZQACBlZzY29kZQ5jcmVhdGVUZXJtaW5hbAACBlZzY29kZQx0ZXJtaW5hbFNob3cAAgZWc2NvZGUQdGVybWluYWxTZW5kVGV4dAABBlZzY29kZRBwdXNoU3Vic2NyaXB0aW9uAAEGVnNjb2RlCmNvbnNvbGVMb2cAAgZWc2NvZGUIZXhlY1N5bmMAAgZWc2NvZGUMc3RyaW5nQ29uY2F0AAEGVnNjb2RlDnN0cmluZ0VuZHNXaXRoAAEGVnNjb2RlE3N0cmluZ1JlcGxhY2VTdWZmaXgAAwZWc2NvZGUNc3RyaW5nSXNFbXB0eQACBlZzY29kZRh3b3Jrc3BhY2VGb2xkZXJGaXJzdFBhdGgABAZWc2NvZGULdXJpRnJvbVBhdGgAAgZWc2NvZGULdXJpSm9pblBhdGgAAQZWc2NvZGUHdXJpUGF0aAACBlZzY29kZQtmc1dyaXRlRmlsZQABBlZzY29kZRBvcGVuVGV4dERvY3VtZW50AAIGVnNjb2RlEHNob3dUZXh0RG9jdW1lbnQAAgZWc2NvZGUTY3JlYXRlU3RhdHVzQmFySXRlbQABBlZzY29kZRRzdGF0dXNCYXJJdGVtU2V0VGV4dAABBlZzY29kZRdzdGF0dXNCYXJJdGVtU2V0VG9vbHRpcAABBlZzY29kZRdzdGF0dXNCYXJJdGVtU2V0Q29tbWFuZAABBlZzY29kZSRzdGF0dXNCYXJJdGVtU2V0QmFja2dyb3VuZENvbG9yVGhlbWUAAQZWc2NvZGURc3RhdHVzQmFySXRlbVNob3cAAgZWc2NvZGUZc3RhdHVzQmFySXRlbUFzRGlzcG9zYWJsZQACBlZzY29kZRpjcmVhdGVEaWFnbm9zdGljQ29sbGVjdGlvbgACBlZzY29kZSBkaWFnbm9zdGljQ29sbGVjdGlvbkFzRGlzcG9zYWJsZQACBlZzY29kZRJjcmVhdGVXZWJ2aWV3UGFuZWwAAwZWc2NvZGUTd2Vidmlld1BhbmVsU2V0SHRtbAABBlZzY29kZRJjbGlwYm9hcmRXcml0ZVRleHQAAgZWc2NvZGUVb25EaWRTYXZlVGV4dERvY3VtZW50AAIGVnNjb2RlCHBhdGhKb2luAAEGVnNjb2RlD3Byb2Nlc3NQbGF0Zm9ybQAEBlZzY29kZRVleHRlbnNpb25BYnNvbHV0ZVBhdGgAARRWc2NvZGVMYW5ndWFnZUNsaWVudBFuZXdMYW5ndWFnZUNsaWVudAAFFFZzY29kZUxhbmd1YWdlQ2xpZW50E2xhbmd1YWdlQ2xpZW50U3RhcnQAAgMQDwYHCAkKCwwNDg8QERITFAQBAAUDAQABBgEAB5QBCAZtZW1vcnkCABhoYW5kbGVyX2NoZWNrX2NvbXBsaWFuY2UALxNoYW5kbGVyX2luaXRfY29uZmlnADATaGFuZGxlcl9zaG93X3JlcG9ydAAyFmhhbmRsZXJfZ2VuZXJhdGVfYmFkZ2UAMw9oYW5kbGVyX29uX3NhdmUANwhhY3RpdmF0ZQA4CmRlYWN0aXZhdGUAOQkBAArvBg8QACAAIAEQDiACEA4PGkEACxQAIAAgARAOIAIQDiADEA4PGkEACxgAIAAgARAOIAIQDiADEA4gBBAODxpBAAscAQF/IAAQCCECIAIQCRogAiABEAoaQQAPGkEACzkBAn8QEiEAIAAQEUEBRgR/QYAQEAUaQQEPGkEABUEACxpBnBAgAEGrEBArIQFBsBAgARAuDxpBAAuxAQEPfxASIQAgABARQQFGBH9BgBAQBRpBAQ8aQQAFQQALGkG9EBACIQEgAUHEEEHSEBAEIQJB3BAhA0HIESEEQegRIQVBgxIhBkGzEyEHQb4UIQggBCACIAUQKyEJIAMgCSAGIAcgCBAtIQogABATIQsgC0H2FBAUIQwgDCAKEBYhDSANQQBHBH9BgxUQBRogDQ8aQQAFQQALGiAMEBchDiAOEBgaQaAVEAcaQQAPGkEACxgBAn9ByBUhAEHmGCEBIAAgARAODxpBAAsdAQF/QYIcQY8cQQEQIiEAIAAQMRAjGkEADxpBAAs4AQJ/EBIhACAAEBFBAUYEf0GAEBAFGkEBDxpBAAVBAAsaQagcIQEgARAkGkGJHRAHGkEADxpBAAttAQd/Qb0QEAIhASABQbAdQb4dEAQhAiACEBFBAEYEfyACDxpBAAVBAAsaECchA0HCHSEEQc0dIQVB3B0gBBAmIQZB3B0gBRAmIQcgA0HmHRAPQQFGBH8gACAHECgPGkEABSAAIAYQKA8aQQALC1IBBH8gABA0IQFB7x0gAUGrEBAOEA4hAiACEA0hAyADQQBHBH9B+h0QBhpBAQ8aQQAFQQALGkG1HkHFHiABQb4dQQAQKSEEIAQQKhpBAA8aQQALYQECf0G9EBACIQEgAUHmHkEBEANBAEYEf0EADxpBAAVBAAsaQQFB5AAQGSECIAJB+x4QGhogAkGSHxAbGiACQd8fEBwaIAJBvh0QHRogAhAeGiAAIAIQHxALGkEADxpBAAsIAEEADxpBAAt/AQJ/QfEfEAwaIAAQNhogAEGdIEEAEAEQCxogAEG0IEEBEAEQCxogAEHfH0ECEAEQCxogAEHGIEEDEAEQCxpBvRAQICEBIAAgARAhEAsaIAAQNRpBvRAQAiECIAJB2yBBARADQQBHBH8gAEEEECUQCxpBAAVBAAsaQQAPGkEACwgAQQAPGkEACwvlEikAQYAQCxwYAAAATm8gd29ya3NwYWNlIGZvbGRlciBvcGVuAEGcEAsPCwAAAHJzciBjaGVjayAiAEGrEAsFAQAAACIAQbAQCw0JAAAAUlNSIENoZWNrAEG9EAsHAwAAAHJzcgBBxBALDgoAAAB0YXJnZXRUaWVyAEHSEAsKBgAAAHNpbHZlcgBB3BALbGgAAAAjIFJTUiAoUmhvZGl1bSBTdGFuZGFyZCBSZXBvc2l0b3J5KSBDb25maWd1cmF0aW9uCiMgaHR0cHM6Ly9naXRodWIuY29tL0h5cGVycG9seW1hdGgvZ2l0LXJzci1jZXJ0aWZpZWQKCgBByBELIBwAAABbY29tcGxpYW5jZV0KdGFyZ2V0X3RpZXIgPSAiAEHoEQsbFwAAACIKc3RyaWN0X21vZGUgPSBmYWxzZQoKAEGDEguwAawAAABbY2hlY2tzXQojIExpY2Vuc2UgY29uZmlndXJhdGlvbgpsaWNlbnNlLnJlcXVpcmVkID0gdHJ1ZQpsaWNlbnNlLmFsbG93ZWQgPSBbIk1JVCIsICJBcGFjaGUtMi4wIiwgIkdQTC0zLjAiLCAiQlNELTMtQ2xhdXNlIl0KCiMgUkVBRE1FIHJlcXVpcmVtZW50cwpyZWFkbWUubWluX2xlbmd0aCA9IDEwMAoKAEGzEwuLAYcAAABbaWdub3JlXQojIFBhdGhzIHRvIGV4Y2x1ZGUgZnJvbSBjb21wbGlhbmNlIHNjYW5uaW5nCnBhdGhzID0gWwogICAgInZlbmRvci8iLAogICAgInRoaXJkX3BhcnR5LyIsCiAgICAibm9kZV9tb2R1bGVzLyIsCiAgICAiLmdpdC8iLApdCgoAQb4UCzg0AAAAW2JhZGdlc10Kc3R5bGUgPSAiZmxhdC1zcXVhcmUiCmluY2x1ZGVfc2NvcmUgPSB0cnVlCgBB9hQLDQkAAAAucnNyLnRvbWwAQYMVCx0ZAAAARmFpbGVkIHRvIHdyaXRlIC5yc3IudG9tbABBoBULKCQAAABSU1IgY29uZmlndXJhdGlvbiBjcmVhdGVkOiAucnNyLnRvbWwAQcgVC54DmgEAADwhRE9DVFlQRSBodG1sPjxodG1sPjxoZWFkPjxzdHlsZT5ib2R5e2ZvbnQtZmFtaWx5OnZhcigtLXZzY29kZS1mb250LWZhbWlseSk7cGFkZGluZzoyMHB4fWgxe2NvbG9yOnZhcigtLXZzY29kZS1mb3JlZ3JvdW5kKX0udGllcntmb250LXNpemU6MjRweDttYXJnaW46MjBweCAwfS50aWVyLnNpbHZlcntjb2xvcjojQzBDMEMwfS5zY29yZXtmb250LXNpemU6MThweDtjb2xvcjp2YXIoLS12c2NvZGUtZGVzY3JpcHRpb25Gb3JlZ3JvdW5kKX0uY2hlY2tze21hcmdpbi10b3A6MjBweH0uY2hlY2t7cGFkZGluZzoxMHB4O21hcmdpbjo1cHggMDtib3JkZXItcmFkaXVzOjRweH0uY2hlY2sucGxhY2Vob2xkZXJ7YmFja2dyb3VuZDp2YXIoLS12c2NvZGUtdGV4dEJsb2NrUXVvdGUtYmFja2dyb3VuZCl9PC9zdHlsZT48L2hlYWQ+AEHmGAucA5gBAAA8Ym9keT48aDE+UlNSIENvbXBsaWFuY2UgUmVwb3J0PC9oMT48ZGl2IGNsYXNzPSJ0aWVyIHNpbHZlciI+4piGIFNJTFZFUjwvZGl2PjxkaXYgY2xhc3M9InNjb3JlIj5SdW4gPGNvZGU+UlNSOiBDaGVjayBDb21wbGlhbmNlPC9jb2RlPiBpbiBhIHRlcm1pbmFsIGZvciBsaXZlIHJlc3VsdHMuPC9kaXY+PGRpdiBjbGFzcz0iY2hlY2tzIj48aDI+Q2hlY2tzPC9oMj48ZGl2IGNsYXNzPSJjaGVjayBwbGFjZWhvbGRlciI+TGl2ZSBpbi1wYW5lbCByZXN1bHRzIHJlcXVpcmUgYW4gYXN5bmMtZXh0ZXJuIEFCSSAoc2VlIGFmZmluZXNjcmlwdCM2NCkuIFRoZSBDTEkgcGF0aCB2aWEgPGNvZGU+cnNyIGNoZWNrPC9jb2RlPiByZXBvcnRzIGZ1bGwgY29tcGxpYW5jZSBzdGF0dXMuPC9kaXY+PC9kaXY+PC9ib2R5PjwvaHRtbD4AQYIcCw0JAAAAcnNyUmVwb3J0AEGPHAsZFQAAAFJTUiBDb21wbGlhbmNlIFJlcG9ydABBqBwLYV0AAABbIVtSU1IgQ2VydGlmaWNhdGlvbl0oaHR0cHM6Ly9yc3ItY2VydGlmaWVkLmRldi9iYWRnZS9zaWx2ZXIuc3ZnKV0oaHR0cHM6Ly9yc3ItY2VydGlmaWVkLmRldikAQYkdCycjAAAAQmFkZ2UgbWFya2Rvd24gY29waWVkIHRvIGNsaXBib2FyZCEAQbAdCw4KAAAAc2VydmVyUGF0aABBvh0LBAAAAAAAQcIdCwsHAAAAcnNyLWxzcABBzR0LDwsAAAByc3ItbHNwLmV4ZQBB3B0LCgYAAABzZXJ2ZXIAQeYdCwkFAAAAd2luMzIAQe8dCwsHAAAAd2hpY2ggIgBB+h0LOzcAAABSU1IgTFNQIHNlcnZlciBub3QgZm91bmQuIFNvbWUgZmVhdHVyZXMgbWF5IGJlIGxpbWl0ZWQuAEG1HgsQDAAAAHJzckNlcnRpZmllZABBxR4LIR0AAABSU1ItQ2VydGlmaWVkIExhbmd1YWdlIFNlcnZlcgBB5h4LFREAAABzaG93U3RhdHVzQmFySXRlbQBB+x4LFxMAAADimIYgUlNSOiBTSUxWRVIgNzUlAEGSHwtNSQAAAFJTUiBDb21wbGlhbmNlOiBzaWx2ZXIgdGllciAoNzUlIHNjb3JlLCBwbGFjZWhvbGRlcikKQ2xpY2sgdG8gdmlldyByZXBvcnQAQd8fCxIOAAAAcnNyLnNob3dSZXBvcnQAQfEfCywoAAAAUlNSLUNlcnRpZmllZCBleHRlbnNpb24gaXMgYWN0aXZhdGluZy4uLgBBnSALFxMAAAByc3IuY2hlY2tDb21wbGlhbmNlAEG0IAsSDgAAAHJzci5pbml0Q29uZmlnAEHGIAsVEQAAAHJzci5nZW5lcmF0ZUJhZGdlAEHbIAsPCwAAAGNoZWNrT25TYXZlAIcBFmFmZmluZXNjcmlwdC5vd25lcnNoaXAPAAAAKwAAAAMAAAAALAAAAAQAAAAAAC0AAAAFAAAAAAAALgAAAAIAAAAvAAAAAAAwAAAAAAAxAAAAAAAyAAAAAAAzAAAAAAA0AAAAAQAANQAAAAEAADYAAAABAAA3AAAAAAA4AAAAAQAAOQAAAAAA"; +const _wasmBytes = Buffer.from(_wasmBase64, "base64"); + +// Per-process opaque-handle table for host objects (ExtensionContext, +// Terminal, LanguageClient, ...). Wasm refers to host objects by integer id. +const _handles = new Map(); +let _nextHandle = 1; +function _registerHandle(obj) { + const h = _nextHandle++; + _handles.set(h, obj); + return h; +} +function _getHandle(h) { return _handles.get(h); } +function _freeHandle(h) { _handles.delete(h); } + +// WASI-style minimal imports — affinescript codegen wires in fd_write on +// every module so we satisfy that even if no IO is exercised. +function _writeFdString(fd, ptr, len, memory) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + const text = new TextDecoder("utf-8").decode(bytes); + if (fd === 1) process.stdout.write(text); + else if (fd === 2) process.stderr.write(text); +} + +let _instance = null; +let _memory = null; + +function _buildImports() { + const wasi_snapshot_preview1 = { + fd_write: (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + // Minimal fd_write: walk the iovec array and concat to fd. + const view = new DataView(_memory.buffer); + let total = 0; + for (let i = 0; i < iovs_len; i++) { + const ptr = view.getUint32(iovs_ptr + i * 8, true); + const len = view.getUint32(iovs_ptr + i * 8 + 4, true); + _writeFdString(fd, ptr, len, _memory); + total += len; + } + view.setUint32(nwritten_ptr, total, true); + return 0; + }, + }; + // Phase 2 hook: callers can replace exports.extraImports with a function + // returning a `{ ModuleName: { exportName: fn, ... } }` map of concrete + // host bindings (e.g. the @hyperpolymath/affine-vscode adapter). Default + // is empty so the shim works standalone. + const extras = (typeof exports.extraImports === "function") + ? exports.extraImports() + : {}; + return { wasi_snapshot_preview1, ...extras }; +} + +async function _init() { + if (_instance) return _instance; + const { instance } = await WebAssembly.instantiate(_wasmBytes, _buildImports()); + _instance = instance; + _memory = instance.exports.memory; + // Phase 2 hook: the active instance + memory must be reachable from + // bindings adapters that need to read string args / call back into the + // wasm table. Surface them on the exports object as soon as init finishes. + exports._instance = instance; + exports._memory = instance.exports.memory; + return _instance; +} + +exports.activate = async function activate(context) { + const inst = await _init(); + if (typeof inst.exports.activate === "function") { + const ctxHandle = _registerHandle(context); + return inst.exports.activate(ctxHandle); + } +}; + +exports.deactivate = async function deactivate() { + if (!_instance) return; + if (typeof _instance.exports.deactivate === "function") { + return _instance.exports.deactivate(); + } +}; + +// Surfaced for Phase 2 binding modules to register concrete vscode-API +// implementations before the Wasm is instantiated. +exports._registerHandle = _registerHandle; +exports._getHandle = _getHandle; +exports._freeHandle = _freeHandle; diff --git a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/package.json b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/package.json index a5e13171..bc5322f0 100644 --- a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/package.json +++ b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/package.json @@ -2,7 +2,7 @@ "name": "rsr-certified", "displayName": "RSR-Certified", "description": "Rhodium Standard Repository compliance checking for VS Code", - "version": "0.1.0", + "version": "0.2.0", "publisher": "rsr-certified", "license": "MIT", "repository": { @@ -27,7 +27,7 @@ "workspaceContains:.git", "workspaceContains:.rsr.toml" ], - "main": "./out/extension.js", + "main": "./src/index.cjs", "contributes": { "commands": [ { @@ -104,20 +104,9 @@ }, "scripts": { "vscode:prepublish": "npm run compile", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", - "lint": "eslint src --ext ts", - "test": "node ./out/test/runTest.js" + "compile": "affinescript compile src/extension.affine -o out/extension.cjs" }, "dependencies": { "vscode-languageclient": "^9.0.1" - }, - "devDependencies": { - "@types/node": "^20.10.0", - "@types/vscode": "^1.85.0", - "@typescript-eslint/eslint-plugin": "^6.13.0", - "@typescript-eslint/parser": "^6.13.0", - "eslint": "^8.54.0", - "typescript": "^5.3.0" } } diff --git a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/affine-vscode-adapter.cjs b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/affine-vscode-adapter.cjs new file mode 100644 index 00000000..e7dca038 --- /dev/null +++ b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/affine-vscode-adapter.cjs @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// Copyright (c) 2026 Jonathan D.A. Jewell +// +// affine-vscode: JS-side adapter for stdlib/Vscode.affine + stdlib/VscodeLanguageClient.affine. +// +// Issue #35 Phase 2 deliverable. Plug this into your Node-CJS extension's +// `_extraImports()` so each `extern fn` declared in the bindings resolves +// to the right vscode API call. +// +// Usage from a hand-written .cjs (until Phase 3 automates the wiring): +// +// const vscodeBindings = require("@hyperpolymath/affine-vscode")( +// require("vscode"), +// require("vscode-languageclient/node"), +// instance, // WebAssembly.Instance, set after instantiate +// ); +// // Pass vscodeBindings into the Wasm imports map under "env". +// +// The adapter maintains a per-process JS-side handle table keyed by Int +// so opaque handles passed across the FFI boundary survive round-trips. + +"use strict"; + +module.exports = function makeVscodeBindings(vscode, lcModule, hostShim) { + // `hostShim` is the .cjs module produced by `affinescript compile -o ...`. + // We share its handle table (so ExtensionContext registered at activate + // time is visible here) and read its `_instance` lazily so this adapter + // can be constructed BEFORE `WebAssembly.instantiate` runs — the calls + // back into adapter functions happen later, once `_instance` is live. + const reg = (obj) => hostShim._registerHandle(obj); + const get = (h) => hostShim._getHandle(h); + const getInstance = () => hostShim._instance; + + // ── String marshalling ───────────────────────────────────────────── + // AffineScript's WASM 1.0 codegen stores string literals at the offset + // returned by the call-site; the layout is [u32 length][utf-8 bytes]. + // Read that shape out of the module's exported memory. + function readString(ptr) { + const inst = getInstance(); + if (!inst || !inst.exports.memory) return ""; + const dv = new DataView(inst.exports.memory.buffer); + const len = dv.getUint32(ptr, true); + const bytes = new Uint8Array(inst.exports.memory.buffer, ptr + 4, len); + return new TextDecoder("utf-8").decode(bytes); + } + + // ── Wasm-table callbacks → JS callable ───────────────────────────── + // Wasm function-pointer args (e.g. command handlers) come in as table + // indices. Wrap each in a JS thunk that re-enters the Wasm module. + function wrapHandler(idx) { + return () => { + const inst = getInstance(); + const tbl = inst && inst.exports && inst.exports.__indirect_function_table; + if (!tbl) return; + const fn = tbl.get(idx); + if (typeof fn === "function") fn(); + }; + } + + // Returned shape is namespaced by the AffineScript module that declared + // each extern: cross-module imports in the wasm reference module="Vscode" + // and module="VscodeLanguageClient" (the dotted module path), so the + // import map's top-level keys must match. + const Vscode = { + // ── vscode.commands ────────────────────────────────────────────── + registerCommand: (namePtr, handlerIdx) => { + const name = readString(namePtr); + const handler = wrapHandler(handlerIdx); + const disposable = vscode.commands.registerCommand(name, handler); + return reg(disposable); + }, + + // ── vscode.workspace ───────────────────────────────────────────── + getConfiguration: (sectionPtr) => + reg(vscode.workspace.getConfiguration(readString(sectionPtr))), + + workspaceConfigGetString: (cfgHandle, keyPtr, defPtr) => { + const cfg = get(cfgHandle); + const result = cfg.get(readString(keyPtr), readString(defPtr)); + // Returning a string-pointer would require allocating in the Wasm + // module's memory. Until that helper exists, return a sentinel + // handle that the caller treats as "look me up via getResultString". + // For now: register the JS string and return its handle. + return reg(String(result)); + }, + + createFileSystemWatcher: (globPtr) => + reg(vscode.workspace.createFileSystemWatcher(readString(globPtr))), + + // ── vscode.window ──────────────────────────────────────────────── + activeTextEditor: () => { + const ed = vscode.window.activeTextEditor; + return ed ? reg(ed) : 0; + }, + showErrorMessage: (msgPtr) => reg(vscode.window.showErrorMessage(readString(msgPtr))), + showWarningMessage: (msgPtr) => reg(vscode.window.showWarningMessage(readString(msgPtr))), + showInformationMessage: (msgPtr) => reg(vscode.window.showInformationMessage(readString(msgPtr))), + + createTerminal: (namePtr) => + reg(vscode.window.createTerminal(readString(namePtr))), + terminalShow: (tHandle) => { const t = get(tHandle); if (t) t.show(); return 0; }, + terminalSendText: (tHandle, textPtr) => { + const t = get(tHandle); if (t) t.sendText(readString(textPtr)); return 0; + }, + + // ── ExtensionContext ──────────────────────────────────────────── + pushSubscription: (ctxHandle, dHandle) => { + const ctx = get(ctxHandle); + const d = get(dHandle); + if (ctx && d) ctx.subscriptions.push(d); + return 0; + }, + + // ── Editor document helpers ─────────────────────────────────────── + editorActiveFilePath: () => { + const ed = vscode.window.activeTextEditor; + return ed ? reg(ed.document.uri.fsPath) : reg(""); + }, + editorActiveLanguageId: () => { + const ed = vscode.window.activeTextEditor; + return ed ? reg(ed.document.languageId) : reg(""); + }, + + // ── Boolean config ─────────────────────────────────────────────── + workspaceConfigGetBool: (cfgHandle, keyPtr, defVal) => { + const cfg = get(cfgHandle); + if (!cfg) return defVal; + return cfg.get(readString(keyPtr), defVal !== 0) ? 1 : 0; + }, + + // ── Host process / IO ──────────────────────────────────────────── + consoleLog: (msgPtr) => { console.log(readString(msgPtr)); return 0; }, + execSync: (cmdPtr) => { + try { + require("child_process").execSync(readString(cmdPtr), { stdio: "ignore" }); + return 0; + } catch (e) { + return e.status ?? 1; + } + }, + + // ── String helpers ──────────────────────────────────────────────── + stringConcat: (aPtr, bPtr) => reg(readString(aPtr) + readString(bPtr)), + stringEndsWith: (sPtr, suffixPtr) => + readString(sPtr).endsWith(readString(suffixPtr)) ? 1 : 0, + stringReplaceSuffix: (sPtr, suffixPtr, replacementPtr) => { + const s = readString(sPtr); + const suffix = readString(suffixPtr); + const replacement = readString(replacementPtr); + const out = s.endsWith(suffix) ? s.slice(0, -suffix.length) + replacement : s; + return reg(out); + }, + stringIsEmpty: (sPtr) => readString(sPtr).length === 0 ? 1 : 0, + + // ── Workspace ─────────────────────────────────────────────────── + workspaceFolderFirstPath: () => { + const folders = vscode.workspace.workspaceFolders; + const first = folders && folders[0]; + return reg(first ? first.uri.fsPath : ""); + }, + workspaceRootUri: () => { + const folders = vscode.workspace.workspaceFolders; + const first = folders && folders[0]; + return first ? reg(first.uri) : 0; + }, + + // ── URI / file-system / text documents ───────────────────────── + uriFromPath: (pathPtr) => reg(vscode.Uri.file(readString(pathPtr))), + uriJoinPath: (baseHandle, segPtr) => { + const base = get(baseHandle); + if (!base) return 0; + return reg(vscode.Uri.joinPath(base, readString(segPtr))); + }, + uriPath: (uHandle) => { + const u = get(uHandle); + return reg(u ? u.fsPath : ""); + }, + fsWriteFile: (uHandle, contentPtr) => { + const u = get(uHandle); + if (!u) return 1; + try { + // Fire-and-forget the Thenable. The host serialises FS ops so + // a subsequent openTextDocument on the same URI sees the file. + vscode.workspace.fs.writeFile(u, Buffer.from(readString(contentPtr))); + return 0; + } catch (e) { + return 1; + } + }, + openTextDocument: (uHandle) => { + const u = get(uHandle); + if (!u) return 0; + // openTextDocument returns a Thenable. The synchronous + // FFI returns a handle to the Thenable itself; showTextDocument is + // also Thenable-returning and chains via vscode's internal queue, + // so this works in practice for the open-then-show pattern. + return reg(vscode.workspace.openTextDocument(u)); + }, + showTextDocument: (dHandle) => { + const d = get(dHandle); + if (!d) return 1; + // If `d` is itself a Thenable, vscode unwraps it. + Promise.resolve(d).then((doc) => vscode.window.showTextDocument(doc)); + return 0; + }, + + // ── Status bar ───────────────────────────────────────────────── + createStatusBarItem: (alignment, priority) => { + const align = alignment === 1 + ? vscode.StatusBarAlignment.Right + : vscode.StatusBarAlignment.Left; + return reg(vscode.window.createStatusBarItem(align, priority)); + }, + statusBarItemSetText: (sHandle, tPtr) => { + const s = get(sHandle); + if (s) s.text = readString(tPtr); + return 0; + }, + statusBarItemSetTooltip: (sHandle, tPtr) => { + const s = get(sHandle); + if (s) s.tooltip = readString(tPtr); + return 0; + }, + statusBarItemSetCommand: (sHandle, cPtr) => { + const s = get(sHandle); + if (s) s.command = readString(cPtr); + return 0; + }, + statusBarItemSetBackgroundColorTheme: (sHandle, cPtr) => { + const s = get(sHandle); + if (!s) return 0; + const name = readString(cPtr); + s.backgroundColor = name.length === 0 ? undefined : new vscode.ThemeColor(name); + return 0; + }, + statusBarItemShow: (sHandle) => { const s = get(sHandle); if (s) s.show(); return 0; }, + statusBarItemHide: (sHandle) => { const s = get(sHandle); if (s) s.hide(); return 0; }, + statusBarItemAsDisposable: (sHandle) => sHandle, // same JS object is a Disposable + + // ── Diagnostics ──────────────────────────────────────────────── + createDiagnosticCollection: (namePtr) => + reg(vscode.languages.createDiagnosticCollection(readString(namePtr))), + diagnosticCollectionClear: (cHandle) => { + const c = get(cHandle); + if (c) c.clear(); + return 0; + }, + diagnosticCollectionSetForUri: (cHandle, uHandle, jsonPtr) => { + const c = get(cHandle); + const u = get(uHandle); + if (!c || !u) return 1; + let arr; + try { arr = JSON.parse(readString(jsonPtr)); } + catch (e) { return 2; } + if (!Array.isArray(arr)) return 3; + const diagnostics = arr.map((d) => { + const range = new vscode.Range( + d.startLine | 0, d.startCol | 0, + d.endLine | 0, d.endCol | 0 + ); + const severity = [ + vscode.DiagnosticSeverity.Error, + vscode.DiagnosticSeverity.Warning, + vscode.DiagnosticSeverity.Information, + vscode.DiagnosticSeverity.Hint, + ][Math.max(0, Math.min(3, d.severity | 0))]; + return new vscode.Diagnostic(range, String(d.message ?? ""), severity); + }); + c.set(u, diagnostics); + return 0; + }, + diagnosticCollectionAsDisposable: (cHandle) => cHandle, + + // ── Webview ──────────────────────────────────────────────────── + createWebviewPanel: (vtPtr, titlePtr, vc) => { + const viewColumn = + vc === 2 ? vscode.ViewColumn.Two : + vc === 3 ? vscode.ViewColumn.Three : + vscode.ViewColumn.One; + return reg(vscode.window.createWebviewPanel( + readString(vtPtr), readString(titlePtr), viewColumn, {} + )); + }, + webviewPanelSetHtml: (pHandle, htmlPtr) => { + const p = get(pHandle); + if (p) p.webview.html = readString(htmlPtr); + return 0; + }, + webviewPanelAsDisposable: (pHandle) => pHandle, + + // ── Clipboard ────────────────────────────────────────────────── + clipboardWriteText: (tPtr) => { + try { + vscode.env.clipboard.writeText(readString(tPtr)); + return 0; + } catch (e) { + return 1; + } + }, + + // ── Events ───────────────────────────────────────────────────── + onDidSaveTextDocument: (handlerIdx) => { + const thunk = wrapHandler(handlerIdx); + // The vscode event ships a TextDocument; we deliberately drop it at + // the FFI boundary (see Vscode.affine docstring). Handlers that + // need the saved file path can call editorActiveFilePath(). + return reg(vscode.workspace.onDidSaveTextDocument(() => thunk())); + }, + + // ── Path helpers ─────────────────────────────────────────────── + pathBasename: (pPtr) => reg(require("path").basename(readString(pPtr))), + pathJoin: (aPtr, bPtr) => + reg(require("path").join(readString(aPtr), readString(bPtr))), + processPlatform: () => reg(process.platform), + + // ── ExtensionContext helpers ─────────────────────────────────── + extensionAbsolutePath: (ctxHandle, relPtr) => { + const ctx = get(ctxHandle); + return reg(ctx ? ctx.asAbsolutePath(readString(relPtr)) : ""); + }, + }; + + const VscodeLanguageClient = { + // ── vscode-languageclient/node ────────────────────────────────── + newLanguageClient: (idPtr, namePtr, cmdPtr, argsNlPtr, transportKind) => { + const id = readString(idPtr); + const name = readString(namePtr); + const command = readString(cmdPtr); + const args = readString(argsNlPtr).split("\n").filter(s => s.length > 0); + const transport = transportKind === 1 ? lcModule.TransportKind.ipc : lcModule.TransportKind.stdio; + const serverOptions = { run: { command, args, transport }, debug: { command, args, transport } }; + const clientOptions = { documentSelector: [{ scheme: "file" }] }; + return reg(new lcModule.LanguageClient(id, name, serverOptions, clientOptions)); + }, + languageClientStart: (cHandle) => { + const c = get(cHandle); + if (c) c.start(); + return 0; + }, + languageClientStop: (cHandle) => { + const c = get(cHandle); + if (c) c.stop(); + return 0; + }, + }; + + return { Vscode, VscodeLanguageClient }; +}; diff --git a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.affine b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.affine new file mode 100644 index 00000000..aaccf1b9 --- /dev/null +++ b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.affine @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Jonathan D.A. Jewell +// +// RSR-Certified VS Code extension — port of extension.ts to AffineScript. +// Closes the standards side of affinescript#64. Unblock conditions: +// - affinescript#35 (Node-target codegen + Vscode.affine bindings) +// - affinescript#102 (vscode-binding surface expansion for this port) +// +// Source of truth for out/extension.cjs, produced by +// affinescript compile src/extension.affine -o out/extension.cjs +// The runtime entry the host loads is src/index.cjs (vendored adapter + +// extraImports wiring); package.json `main` points there. + +use Vscode::{ + registerCommand, + getConfiguration, + workspaceConfigGetBool, + workspaceConfigGetString, + showErrorMessage, + showWarningMessage, + showInformationMessage, + createTerminal, + terminalShow, + terminalSendText, + pushSubscription, + consoleLog, + execSync, + stringConcat, + stringEndsWith, + stringReplaceSuffix, + stringIsEmpty, + workspaceFolderFirstPath, + uriFromPath, + uriJoinPath, + uriPath, + fsWriteFile, + openTextDocument, + showTextDocument, + createStatusBarItem, + statusBarItemSetText, + statusBarItemSetTooltip, + statusBarItemSetCommand, + statusBarItemSetBackgroundColorTheme, + statusBarItemShow, + statusBarItemAsDisposable, + createDiagnosticCollection, + diagnosticCollectionAsDisposable, + createWebviewPanel, + webviewPanelSetHtml, + clipboardWriteText, + onDidSaveTextDocument, + pathJoin, + processPlatform, + extensionAbsolutePath +}; +use VscodeLanguageClient::{ + newLanguageClient, + languageClientStart +}; + +// ── String-building helpers ────────────────────────────────────────── + +fn concat3(a: String, b: String, c: String) -> String { + return stringConcat(stringConcat(a, b), c); +} + +fn concat4(a: String, b: String, c: String, d: String) -> String { + return stringConcat(stringConcat(stringConcat(a, b), c), d); +} + +fn concat5(a: String, b: String, c: String, d: String, e: String) -> String { + return stringConcat(stringConcat(stringConcat(stringConcat(a, b), c), d), e); +} + +fn run_in_terminal(name: String, cmd: String) -> Int { + let t = createTerminal(name); + let _ = terminalShow(t); + let _ = terminalSendText(t, cmd); + return 0; +} + +// ── Command handlers ───────────────────────────────────────────────── +// +// All four commands shell out to the `rsr` CLI in a fresh terminal. The +// original TS extension had a "preferred" path that used a custom LSP +// request (`rsr/getCompliance`) + `vscode.window.withProgress` for live +// in-process compliance results, with a CLI fallback for the no-LSP +// case. The current AffineScript extern-call ABI is synchronous, so +// Thenable-returning APIs (sendRequest, withProgress) cannot be bound; +// we use the CLI-fallback path unconditionally. The downstream effects +// of the LSP path (status-bar mutation, diagnostic-collection updates) +// are also absent here for the same reason. The status bar still shows +// a static initial value set in activate(); the diagnostic collection +// is created so it can join subscriptions for clean disposal. + +pub fn handler_check_compliance() -> Int { + let ws = workspaceFolderFirstPath(); + if stringIsEmpty(ws) == 1 { + let _ = showErrorMessage("No workspace folder open"); + return 1; + } + let cmd = concat3("rsr check \"", ws, "\""); + return run_in_terminal("RSR Check", cmd); +} + +pub fn handler_init_config() -> Int { + let ws = workspaceFolderFirstPath(); + if stringIsEmpty(ws) == 1 { + let _ = showErrorMessage("No workspace folder open"); + return 1; + } + let cfg = getConfiguration("rsr"); + let target_tier = workspaceConfigGetString(cfg, "targetTier", "silver"); + + // Build the .rsr.toml body. Kept as a single chained string for + // simplicity; if/when AffineScript grows multi-line raw-string + // literals this becomes a heredoc. + let line_header = "# RSR (Rhodium Standard Repository) Configuration\n# https://github.com/Hyperpolymath/git-rsr-certified\n\n"; + let line_compl_open = "[compliance]\ntarget_tier = \""; + let line_compl_close = "\"\nstrict_mode = false\n\n"; + let line_checks = "[checks]\n# License configuration\nlicense.required = true\nlicense.allowed = [\"MIT\", \"Apache-2.0\", \"GPL-3.0\", \"BSD-3-Clause\"]\n\n# README requirements\nreadme.min_length = 100\n\n"; + let line_ignore = "[ignore]\n# Paths to exclude from compliance scanning\npaths = [\n \"vendor/\",\n \"third_party/\",\n \"node_modules/\",\n \".git/\",\n]\n\n"; + let line_badges = "[badges]\nstyle = \"flat-square\"\ninclude_score = true\n"; + + let body_compl = concat3(line_compl_open, target_tier, line_compl_close); + let body = concat5(line_header, body_compl, line_checks, line_ignore, line_badges); + + let ws_uri = uriFromPath(ws); + let config_uri = uriJoinPath(ws_uri, ".rsr.toml"); + let write_rc = fsWriteFile(config_uri, body); + if write_rc != 0 { + let _ = showErrorMessage("Failed to write .rsr.toml"); + return write_rc; + } + + let doc = openTextDocument(config_uri); + let _ = showTextDocument(doc); + let _ = showInformationMessage("RSR configuration created: .rsr.toml"); + return 0; +} + +/// Returns the constant report HTML. The original TS extension built +/// this dynamically from compliance-check data; in the absence of a +/// sendRequest binding (see top-of-file note) we render a static +/// placeholder with the same shape so users can see the panel works. +fn report_html() -> String { + let head = ""; + let body = "

RSR Compliance Report

\xE2\x98\x86 SILVER
Run RSR: Check Compliance in a terminal for live results.

Checks

Live in-panel results require an async-extern ABI (see affinescript#64). The CLI path via rsr check reports full compliance status.
"; + return stringConcat(head, body); +} + +pub fn handler_show_report() -> Int { + let panel = createWebviewPanel("rsrReport", "RSR Compliance Report", 1); + let _ = webviewPanelSetHtml(panel, report_html()); + return 0; +} + +pub fn handler_generate_badge() -> Int { + let ws = workspaceFolderFirstPath(); + if stringIsEmpty(ws) == 1 { + let _ = showErrorMessage("No workspace folder open"); + return 1; + } + // Hardcoded `silver` placeholder — matches the TS original's TODO + // ("Get actual tier from compliance check"). The real tier comes from + // the LSP sendRequest path which is unavailable in this port. + let badge = "[![RSR Certification](https://rsr-certified.dev/badge/silver.svg)](https://rsr-certified.dev)"; + let _ = clipboardWriteText(badge); + let _ = showInformationMessage("Badge markdown copied to clipboard!"); + return 0; +} + +// ── LSP startup ────────────────────────────────────────────────────── +// +// Resolves the rsr-lsp binary path in this order: +// 1. `rsr.serverPath` config value if non-empty +// 2. bundled at /server/rsr-lsp[.exe] +// then starts the LanguageClient. Probe is `which`-based; failures +// surface through vscode-languageclient's own error UI rather than a +// pre-flight existsSync check (which would require an extra binding). + +fn resolve_server_path(ctx: ExtensionContext) -> String { + let cfg = getConfiguration("rsr"); + let configured = workspaceConfigGetString(cfg, "serverPath", ""); + if stringIsEmpty(configured) == 0 { + return configured; + } + let platform = processPlatform(); + let exe_name = "rsr-lsp"; + let exe_name_win = "rsr-lsp.exe"; + let rel = pathJoin("server", exe_name); + let rel_win = pathJoin("server", exe_name_win); + if stringEndsWith(platform, "win32") == 1 { + return extensionAbsolutePath(ctx, rel_win); + } else { + return extensionAbsolutePath(ctx, rel); + } +} + +fn start_lsp(ctx: ExtensionContext) -> Int { + let server_path = resolve_server_path(ctx); + let probe_cmd = stringConcat("which \"", stringConcat(server_path, "\"")); + let probe_rc = execSync(probe_cmd); + if probe_rc != 0 { + let _ = showWarningMessage("RSR LSP server not found. Some features may be limited."); + return 1; + } + let client = newLanguageClient( + "rsrCertified", + "RSR-Certified Language Server", + server_path, + "", // no args + 0 // stdio transport + ); + let _ = languageClientStart(client); + return 0; +} + +// ── Status bar ─────────────────────────────────────────────────────── +// +// The original TS extension mutated the status-bar text on every +// compliance result (driven by the LSP sendRequest path that we cannot +// bind in the current ABI). Here we set a single initial value at +// activate() time and leave it for the session. Reading the live tier +// requires running the CLI from the command palette. + +fn setup_status_bar(ctx: ExtensionContext) -> Int { + let cfg = getConfiguration("rsr"); + if workspaceConfigGetBool(cfg, "showStatusBarItem", 1) == 0 { + return 0; + } + let item = createStatusBarItem(1, 100); // 1 = StatusBarAlignment.Right + let _ = statusBarItemSetText(item, "\xE2\x98\x86 RSR: SILVER 75%"); + let _ = statusBarItemSetTooltip(item, + "RSR Compliance: silver tier (75% score, placeholder)\nClick to view report"); + let _ = statusBarItemSetCommand(item, "rsr.showReport"); + let _ = statusBarItemSetBackgroundColorTheme(item, ""); // empty = no background + let _ = statusBarItemShow(item); + let _ = pushSubscription(ctx, statusBarItemAsDisposable(item)); + return 0; +} + +// ── On-save handler ────────────────────────────────────────────────── +// +// The original filtered saves by basename (README.md, LICENSE, etc.). +// The current Vscode.affine binding drops the TextDocument arg at the +// FFI boundary, so we cannot read the saved doc's path here. As a +// degradation, we either: +// - re-run check on every save (noisy), OR +// - don't register a save handler at all. +// We register a no-op handler that does nothing so the disposable is +// still pushed for clean tracking; users who want check-on-save can +// re-run the command manually. This preserves the `rsr.checkOnSave` +// config flag's *registration* shape without spawning a terminal for +// every keystroke-save. + +pub fn handler_on_save() -> Int { + return 0; +} + +// ── Lifecycle ──────────────────────────────────────────────────────── + +pub fn activate(ctx: ExtensionContext) -> Int { + let _ = consoleLog("RSR-Certified extension is activating..."); + + let _ = setup_status_bar(ctx); + + // Register commands. Wasm-table indices must match the order command + // handler functions appear in this file: + // 0 handler_check_compliance + // 1 handler_init_config + // 2 handler_show_report + // 3 handler_generate_badge + // 4 handler_on_save + let _ = pushSubscription(ctx, registerCommand("rsr.checkCompliance", 0)); + let _ = pushSubscription(ctx, registerCommand("rsr.initConfig", 1)); + let _ = pushSubscription(ctx, registerCommand("rsr.showReport", 2)); + let _ = pushSubscription(ctx, registerCommand("rsr.generateBadge", 3)); + + // Diagnostic collection — created so it can join subscriptions for + // disposal-on-deactivate. Empty by design (population would require + // the sendRequest path). + let dc = createDiagnosticCollection("rsr"); + let _ = pushSubscription(ctx, diagnosticCollectionAsDisposable(dc)); + + let _ = start_lsp(ctx); + + // Honour the rsr.checkOnSave config flag's registration shape (no-op + // handler under the current ABI — see handler_on_save docstring). + let cfg = getConfiguration("rsr"); + if workspaceConfigGetBool(cfg, "checkOnSave", 1) != 0 { + let _ = pushSubscription(ctx, onDidSaveTextDocument(4)); + } + + return 0; +} + +pub fn deactivate() -> Int { + // LanguageClient handle is not retained across activations under the + // current binding shape; the host process exit closes the server + // pipe. Status-bar item, diagnostic collection, webview panels, and + // command disposables are all pushed to ctx.subscriptions and + // released by the host. + return 0; +} diff --git a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.ts b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.ts deleted file mode 100644 index 8b832074..00000000 --- a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.ts +++ /dev/null @@ -1,379 +0,0 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; -import { - LanguageClient, - LanguageClientOptions, - ServerOptions, - TransportKind -} from 'vscode-languageclient/node'; - -let client: LanguageClient | undefined; -let statusBarItem: vscode.StatusBarItem; - -export function activate(context: vscode.ExtensionContext) { - console.log('RSR-Certified extension is activating...'); - - // Create status bar item - statusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Right, - 100 - ); - statusBarItem.command = 'rsr.showReport'; - context.subscriptions.push(statusBarItem); - - // Register commands - context.subscriptions.push( - vscode.commands.registerCommand('rsr.checkCompliance', checkCompliance), - vscode.commands.registerCommand('rsr.initConfig', initConfig), - vscode.commands.registerCommand('rsr.showReport', showReport), - vscode.commands.registerCommand('rsr.generateBadge', generateBadge) - ); - - // Start the language server - startLanguageServer(context); - - // Update status bar - updateStatusBar('silver', 75); - - // Check on save if enabled - const config = vscode.workspace.getConfiguration('rsr'); - if (config.get('checkOnSave')) { - context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(onDocumentSave) - ); - } -} - -function startLanguageServer(context: vscode.ExtensionContext) { - const config = vscode.workspace.getConfiguration('rsr'); - let serverPath = config.get('serverPath'); - - if (!serverPath) { - // Use bundled server - serverPath = context.asAbsolutePath( - path.join('server', process.platform === 'win32' ? 'rsr-lsp.exe' : 'rsr-lsp') - ); - } - - // Check if server exists - const fs = require('fs'); - if (!fs.existsSync(serverPath)) { - vscode.window.showWarningMessage( - 'RSR LSP server not found. Some features may be limited. Install with: cargo install rsr-lsp' - ); - return; - } - - const serverOptions: ServerOptions = { - run: { - command: serverPath, - transport: TransportKind.stdio - }, - debug: { - command: serverPath, - transport: TransportKind.stdio - } - }; - - const clientOptions: LanguageClientOptions = { - documentSelector: [ - { scheme: 'file', pattern: '**/.rsr.toml' }, - { scheme: 'file', pattern: '**/README.md' }, - { scheme: 'file', pattern: '**/LICENSE*' }, - { scheme: 'file', pattern: '**/CONTRIBUTING.md' }, - { scheme: 'file', pattern: '**/SECURITY.md' }, - { scheme: 'file', pattern: '**/CHANGELOG.md' } - ], - synchronize: { - fileEvents: vscode.workspace.createFileSystemWatcher('**/.rsr.toml') - } - }; - - client = new LanguageClient( - 'rsrCertified', - 'RSR-Certified Language Server', - serverOptions, - clientOptions - ); - - client.start(); -} - -async function checkCompliance() { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) { - vscode.window.showErrorMessage('No workspace folder open'); - return; - } - - vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: 'RSR: Checking compliance...', - cancellable: false - }, - async () => { - if (client) { - try { - const result = await client.sendRequest('rsr/getCompliance'); - handleComplianceResult(result); - } catch (error) { - vscode.window.showErrorMessage(`RSR check failed: ${error}`); - } - } else { - // Fallback: Run CLI - runCliCheck(workspaceFolder.uri.fsPath); - } - } - ); -} - -function runCliCheck(workspacePath: string) { - const terminal = vscode.window.createTerminal('RSR Check'); - terminal.sendText(`rsr check "${workspacePath}"`); - terminal.show(); -} - -function handleComplianceResult(result: any) { - if (!result) return; - - const tier = result.tier || 'none'; - const score = result.score || 0; - - updateStatusBar(tier, Math.round(score * 100)); - - // Show notification - const tierEmoji = getTierEmoji(tier); - vscode.window.showInformationMessage( - `RSR Compliance: ${tierEmoji} ${tier.toUpperCase()} (${Math.round(score * 100)}%)` - ); - - // Update diagnostics - if (result.checks) { - updateDiagnostics(result.checks); - } -} - -function updateStatusBar(tier: string, score: number) { - const config = vscode.workspace.getConfiguration('rsr'); - if (!config.get('showStatusBarItem')) { - statusBarItem.hide(); - return; - } - - const emoji = getTierEmoji(tier); - statusBarItem.text = `${emoji} RSR: ${tier.toUpperCase()} ${score}%`; - statusBarItem.tooltip = `RSR Compliance: ${tier} tier (${score}% score)\nClick to view report`; - - // Color based on tier - switch (tier.toLowerCase()) { - case 'rhodium': - statusBarItem.backgroundColor = undefined; - break; - case 'gold': - statusBarItem.backgroundColor = undefined; - break; - case 'silver': - statusBarItem.backgroundColor = undefined; - break; - case 'bronze': - statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); - break; - default: - statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); - } - - statusBarItem.show(); -} - -function getTierEmoji(tier: string): string { - switch (tier.toLowerCase()) { - case 'rhodium': return '◆'; - case 'gold': return '★'; - case 'silver': return '☆'; - case 'bronze': return '●'; - default: return '○'; - } -} - -const diagnosticCollection = vscode.languages.createDiagnosticCollection('rsr'); - -function updateDiagnostics(checks: any[]) { - diagnosticCollection.clear(); - - const failedChecks = checks.filter((c: any) => !c.passed); - if (failedChecks.length === 0) return; - - // Group by file (if we have file info) - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) return; - - // Create a single diagnostic for the workspace - const rootUri = workspaceFolder.uri; - const diagnostics: vscode.Diagnostic[] = failedChecks.map((check: any) => { - const severity = getSeverity(check.tier); - return new vscode.Diagnostic( - new vscode.Range(0, 0, 0, 0), - `[${check.tier.toUpperCase()}] ${check.name}: ${check.message}`, - severity - ); - }); - - // Add to a virtual "compliance" file or README - const readmePath = vscode.Uri.joinPath(rootUri, 'README.md'); - diagnosticCollection.set(readmePath, diagnostics); -} - -function getSeverity(tier: string): vscode.DiagnosticSeverity { - switch (tier.toLowerCase()) { - case 'bronze': - return vscode.DiagnosticSeverity.Error; - case 'silver': - return vscode.DiagnosticSeverity.Warning; - case 'gold': - case 'rhodium': - return vscode.DiagnosticSeverity.Information; - default: - return vscode.DiagnosticSeverity.Hint; - } -} - -async function initConfig() { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) { - vscode.window.showErrorMessage('No workspace folder open'); - return; - } - - const config = vscode.workspace.getConfiguration('rsr'); - const targetTier = config.get('targetTier') || 'silver'; - - const configContent = `# RSR (Rhodium Standard Repository) Configuration -# https://github.com/Hyperpolymath/git-rsr-certified - -[compliance] -target_tier = "${targetTier}" -strict_mode = false - -[checks] -# License configuration -license.required = true -license.allowed = ["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause"] - -# README requirements -readme.min_length = 100 - -[ignore] -# Paths to exclude from compliance scanning -paths = [ - "vendor/", - "third_party/", - "node_modules/", - ".git/", -] - -[badges] -style = "flat-square" -include_score = true -`; - - const configPath = vscode.Uri.joinPath(workspaceFolder.uri, '.rsr.toml'); - await vscode.workspace.fs.writeFile(configPath, Buffer.from(configContent)); - - const doc = await vscode.workspace.openTextDocument(configPath); - await vscode.window.showTextDocument(doc); - - vscode.window.showInformationMessage('RSR configuration created: .rsr.toml'); -} - -async function showReport() { - // Create a webview panel for the report - const panel = vscode.window.createWebviewPanel( - 'rsrReport', - 'RSR Compliance Report', - vscode.ViewColumn.One, - {} - ); - - // TODO: Get actual compliance data - panel.webview.html = getReportHtml('silver', 75, [ - { name: 'License', tier: 'bronze', passed: true, message: 'MIT License detected' }, - { name: 'README', tier: 'bronze', passed: true, message: 'README.md found' }, - { name: 'Contributing', tier: 'silver', passed: true, message: 'CONTRIBUTING.md found' }, - { name: 'Documentation', tier: 'gold', passed: false, message: 'No docs/ directory found' } - ]); -} - -function getReportHtml(tier: string, score: number, checks: any[]): string { - const passedChecks = checks.filter(c => c.passed).length; - const totalChecks = checks.length; - - return ` - - - - - -

RSR Compliance Report

-
${getTierEmoji(tier)} ${tier.toUpperCase()}
-
Score: ${score}% (${passedChecks}/${totalChecks} checks passed)
-
-

Checks

- ${checks.map(c => ` -
- ${c.passed ? '✓' : '✗'} - [${c.tier.toUpperCase()}] ${c.name}: ${c.message} -
- `).join('')} -
- -`; -} - -async function generateBadge() { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) { - vscode.window.showErrorMessage('No workspace folder open'); - return; - } - - // TODO: Get actual tier from compliance check - const tier = 'silver'; - const badgeMarkdown = `[![RSR Certification](https://rsr-certified.dev/badge/${tier}.svg)](https://rsr-certified.dev)`; - - await vscode.env.clipboard.writeText(badgeMarkdown); - vscode.window.showInformationMessage('Badge markdown copied to clipboard!'); -} - -function onDocumentSave(document: vscode.TextDocument) { - const relevantFiles = [ - 'README.md', 'LICENSE', 'CONTRIBUTING.md', 'SECURITY.md', - 'CHANGELOG.md', 'CODE_OF_CONDUCT.md', '.rsr.toml' - ]; - - const fileName = path.basename(document.fileName); - if (relevantFiles.includes(fileName) || fileName.startsWith('LICENSE')) { - checkCompliance(); - } -} - -export function deactivate(): Thenable | undefined { - if (client) { - return client.stop(); - } - return undefined; -} diff --git a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/index.cjs b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/index.cjs new file mode 100644 index 00000000..d2d44317 --- /dev/null +++ b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/index.cjs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Jonathan D.A. Jewell +// +// Runtime entry point — what VS Code loads via package.json `main`. +// +// Pipeline: +// src/extension.affine ──affinescript compile──> out/extension.cjs +// src/index.cjs ──this file──> exports.{activate,deactivate} +// +// Wires the vendored affine-vscode adapter into the wasm shim's +// `extraImports` hook before activation, so AffineScript extern fns +// declared in stdlib/Vscode.affine + stdlib/VscodeLanguageClient.affine +// resolve to live vscode / vscode-languageclient API calls. + +"use strict"; + +const shim = require("../out/extension.cjs"); +const makeVscodeBindings = require("./affine-vscode-adapter.cjs"); + +shim.extraImports = function extraImports() { + return makeVscodeBindings( + require("vscode"), + require("vscode-languageclient/node"), + shim + ); +}; + +exports.activate = shim.activate; +exports.deactivate = shim.deactivate; diff --git a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/tsconfig.json b/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/tsconfig.json deleted file mode 100644 index ab7159e9..00000000 --- a/rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "outDir": "out", - "lib": ["ES2020"], - "sourceMap": true, - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true - }, - "exclude": ["node_modules", ".vscode-test"] -}