## 1. Project Overview & Quickstart (unisonweb/unison) ## File: README.md The Unison language =================== [](https://github.com/unisonweb/unison/actions/workflows/ci.yaml?query=branch%3Atrunk) [](https://github.com/unisonweb/unison/actions/workflows/pre-release.yaml) * [Overview](#overview) * [Building using Stack](#building-using-stack) * [Language Server Protocol (LSP)](docs/language-server.markdown) * [Codebase Server](#codebase-server) * [Configuration](./docs/configuration.md) Overview -------- [Unison](https://unison-lang.org) is a statically-typed functional language with type inference, an effect system, and advanced tooling. It is based around [a big idea of content-addressed code](https://www.unison-lang.org/learn/the-big-idea/), in which function are identified by a hash of their implementation rather than by name, and code is stored as its AST in a database. This provides a number of benefits: * No builds. Unison has perfect incremental compilation, with a shared compilation cache that is part of the codebase format. Despite the strong static typing, you are almost never waiting for code to compile. * Instant, non-breaking renaming of definitions. * Perfect caching of tests, only rerunning determinstic tests if dependencies changed. * Semantically-aware version control, avoiding spurious merge conflicts from things like order of imports, whitespace or code formatting differences, and so on. Unison can be used like any other general-purpose language, or you can use it in conjunction with [Unison Cloud](https://unison.cloud) for building distributed systems. Here is some sample code: ```Haskell -- A comment! -- Function signatures appear before the definition factorial : Nat -> Nat factorial n = product (range 1 (n + 1)) -- Signatures can be left off; they will be inferred List.map f as = go acc rem = match rem with [] -> acc a +: as -> go (acc :+ f a) as go [] as > List.map (x -> x * 10) (range 0 10) = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90] > List.map factorial [1,2,3,4] = [1, 2, 6, 24] ``` Functions arguments are separated by spaces instead of parens and commas. Loops are written using recursion (above the helper function `go` defines a loop). The language supports pattern matching via `match with `, which works for lists and also user-defined data types. Other resources: * [Learn about the big idea behind Unison](https://www.unison-lang.org/learn/the-big-idea/) * Check out [the project website](https://unison-lang.org) * Say hello or lurk [in the Discord chat](https://unison-lang.org/discord) * Explore [the Unison ecosystem](https://share.unison-lang.org/) * [Learn Unison](https://www.unison-lang.org/learn/) Building using Stack -------------------- If these instructions don't work for you or are incomplete, please file an issue. The build uses [Stack](http://docs.haskellstack.org/). If you don't already have it installed, [follow the install instructions](http://docs.haskellstack.org/en/stable/README.html#how-to-install) for your platform. (Hint: `brew update && brew install stack`) ```sh $ git clone https://github.com/unisonweb/unison.git $ cd unison $ stack --version # we'll want to know this version if you run into trouble $ stack build --fast --test && stack exec unison ``` To run the Unison Local UI while building from source, you can use the `/dev-ui-install.sh` script. It will download the latest release of [unison-local-ui](https://github.com/unisonweb/unison-local-ui) and put it in the expected location for the unison executable created by `stack build`. When you start unison, you'll see a url where Unison Local UI is running. See [`development.markdown`](development.markdown) for a list of build commands you'll likely use during development. Language Server Protocol (LSP) ------------------------------ View Language Server setup instructions [here](docs/language-server.markdown). AI Agent Server (MCP) ---------------------- View AI Agent Server setup instructions [here](docs/mcp.md). Codebase Server --------------- When `ucm` starts it starts a Codebase web server that is used by the [Unison Local UI](https://github.com/unisonweb/unison-local-ui). It selects a random port and a unique token that must be used when starting the UI to correctly connect to the server. The port, host and token can all be configured by providing environment variables when starting `ucm`: `UCM_PORT`, `UCM_HOST`, and `UCM_TOKEN`. Configuration ------------- See the documentation for configuration [here](docs/configuration.md) --- ## File: docs/branchless.md ----- ### Dependents The reason we keep track of dependents is for the `todo` calculation. When we make an edit, what are the things that need to be updated as a result? When adding term `a` that depends on "derived" term `b` or type `B`, then a change to `b` or `B` affects `a`, so we record that `a` is a dependent of `b` and `B`. When adding type `A` that depends on type `B`, a change to `B` affects `A`, so we record that `A` is a dependent of `B`. We don't do anything for constructors, because constructors don't change. Depending on the constructor really means you depend on the type that constructor comes from. (i.e. a constructor doesn't have dependents.) Similarly, constructor doesn't have dependencies, but its declaring type may depend on other types. ----- Commands ``` /> cd libs/Foo /libs/Foo> cd .. /libs> fork Foo Foo2 /libs> fork thing /libs> fork Foo /outside/Foo /libs> fork /outside/Foo /outside/Foo2 /libs> help merge `> merge src dest` /libs> merge /outside/Foo Foo /libs> merge Foo2 Foo /libs/Foo> /libs> move /libs/Foo /libs/Foo' /libs> A.B.c A.B.d arya renames, and has: -> A.Z.c A.Z.d paul adds, and has -> A.B.e A.B.c A.B.d then merge -> "Merge introduces the following aliases:" A.Z.c -> A.B.c A.Z.d -> A.B.d /libs> delete /libs/Foo "warning: /libs/Foo includes the following definitions that aren't anywhere else: A.B.e#123 run it again to proceed with deletion" /libs> alias /libs/Foo/sqrt /libs/Foo2/butt -- we talked about combining alias & fork into a single "copy" command /libs> ``` Weird thing: There's no history for `sqrt`! Suppose: ``` data Raw = Raw { _termsR :: Set Referent , _typesR :: Set Reference , _childrenR :: Map NameSegment Hash } ``` ``` /libfoo/Foo <- type /libfoo/Foo <- constructor /libfoo/Foo.f <- term in child namespace /libfoo> move Foo Foo2 /libfoo> alias Foo Foo2 ``` ``` ``` ## Data types: Old **PrettyPrintEnv** is for pretty-pretting code, and ___ ```haskell { terms :: Referent -> Maybe HashQualified , types :: Reference -> Maybe HashQualified } ``` Q: How do we want to handle lookup of names that are outside of our branch? Old **Namespace** ```haskell { _terms :: Relation Name Referent , _types :: Relation Name Reference } ``` Old **Names** is an unconflicted **Namespace**. is for parsing code? Not sufficient to parse hash-qualified names. ```haskell { termNames :: Map Name Referent , typeNames :: Map Name Reference } ``` New **Names** combines old **PrettyPrintEnv** and old **Names**: ```haskell -- these HashQualified are fully qualified { terms :: Relation HashQualified Referent , types :: Relation HashQualified Reference } ``` We should be able to construct one from a `Codebase2`, given: ```haskell root :: Branch current :: Branch terms :: Set HashQualified types :: Set HashQualified ``` or ```haskell root :: Branch current :: Branch terms :: Set Referent types :: Set Reference ``` ### Needed functionality Parsing a .u file: * Look up a Reference by name * Look up a Reference by hash-qualified name? We could avoid this by requiring that the user deconflict the names before parsing. Parsing command-line arguments: * Look up a Reference by name. * Look up a Reference by hash-qualified name (possibly from among deleted names); for resolving conflicted names and edits. ``` /foo> todo These names are conflicted: foo#abc foo#xyz Use `rename` to change a names, or `unname` to remove one. These edits are conflicted: bar#fff -> bar#ggg : Nat (12 usages) bar#fff -> bar#hhh : Nat -> Nat (7 usages) bar#fff (Deprecated) Use `view bar#ggg bar#hhh` to view these choices. Use `edit.resolve` to choose a canonical replacement. Use `edit.unreplace` to cancel a replacement. Use `edit.undeprecate` to cancel a deprecation. Use `edit.replace bar#hhh bar#ggg` to start replacing the 7 usages of `bar#hhh` with `bar#ggg`. /foo> alias bar baz Not sure which bar you meant? bar#ggg bar#hhh Try specifying the hash-qualified name, or sort out the conflicts before making the alias. ``` ``` /foo> edit.resolve bar#fff bar#ggg Cleared bar#fff -> bar#hhh Added bar#ggg -> bar#hhh ``` or ``` /foo> edit.unreplace bar#fff bar#ggg Cleared bar#fff -> bar#ggg ``` Pretty-printing: * Select a name by Reference Q: What to do about names outside the current branch? Option 1: Don't support names outside the current branch; user must go up a level (possibly to the root), set up the names as desired, and then descend again. Option 2: Introduce some syntax for names outside the current branch, e.g. `_root_.Foo.bar`. We could first lookup references in the current branch, then in the root branch, then in the history of the root branch? ## TODO tracking refactoring of existing functionality * [ ] Add edits/patches to Namespace / Branch * [ ] Add patch to `NameTarget` * [ ] rename `propagate` to `patch` * moves names from old hash to new hash, transitively, to the type-preserving frontier * [ ] `list [path]` * ~~by default, don't descend into links with names that start with `_`~~ * [ ] `todo [path]` * list conflicted names (hash-qualified) and edit frontier * [ ] `update [path]` * ~~when updating a term, old names goes into `./_archived`, which will be largely conflicted.~~ * [ ] `propagate [path]` * [ ] `edit.resolve ` * Old names use case 1: ``` patch: #a -> #b #a -> #c namelookup: #b -> "foo" #c -> "foo2" "You have a conflicted edit: #a -> foo#b #a -> foo2#c Please choose one. " /pc/libs/x> edit.resolve #a foo#b ``` You're in the middle of an edit, it's not type preserving * [ ] `rename / move` * [ ] `rename.edits` * [ ] `rename.type` * [ ] `rename.term` * [ ] name / copy `copy <[src][#hash]> ` * [ ] `todo [path]`, `update [path]`, `propagate [path]` * [x] Implement `Branch.sync` operation that synchronizes a monadic `Branch` to disk * [x] Implement something like `Branch.fromDirectory : FilePath -> IO (Branch IO)` for getting a lazy proxy for a `Branch` - Also `Branch.fromExternal : (Path -> m ByteString) -> Hash -> m (Branch m)` - Could we create a `Branch` from a GitHub reference? Seems like yeah, it's just going to do some HTTP fetching. * [x] Tweak `Codebase` to `Codebase2` * [x] Implement a `Codebase2` for `FileCodebase2` * [ ] Implement `Actions2` * [ ] Implement `Editor2` * [ ] Implement `OutputMessages2` * [ ] Implement `InputPatterns2` * [ ] Go back and leave a spot for Link in serialized Branch0 format. * [ ] Split Edits out of `Branch0` * [ ] Delete `oldNamespace`, and instead add deprecated names * [ ] Parsing takes a `Names`, a map from `Name`(fully-qualified name) to `Referent`/`Reference`. We should switch these from `Map` to `Name -> Optional xxx`, or even `Name -> m (Optional xxx)` * [ ] `Context.synthesizeClosed` takes a `TypeLookup`, which includes a map from `Reference` to `Type`, `DataDecl`, `EffectDecl`. Shall we plan to include the full codebase here, or load them on demand? Maybe it doesn't matter yet. * `parseAndSynthesizeFile` takes a `Set Reference -> m (TypeLookup v Ann)`, maybe that's a good model. * [ ] `add` and `update` will need a way to update the `Branch'` at the current level, and all the way back to the root. Some kind of zipper? * [ ] `find` takes an optional path * [ ] `fork` takes a `RepoPath` (or we could have a dedicated command like `clone`) * [ ] `merge` takes at least a path, if not a `RepoPath` * [ ] `publish` or `push`that takes a local path and a remote path? ## Branchless codebase format ## Commands / Usage ``` /> clone gh:aryairani/libfoo Copied gh:aryairani/libfoo blah blah to /libfoo /> undo /> clone gh:aryairani/libfoo /libs/DeepLearning/Foo Copied gh:aryairani/libfoo blah blah to /libs/DeepLearning/Foo /> ``` `clone [path]` `push [path] ` ``` /> cd projects /projects> rename FaceDetector FaceDetector/V1 /projects> cd FaceDetector /projects/FaceDetector> cp V1 V2 ``` `cd ` — support relative paths? `cp ` ``` /projects/FaceDetector> replace.scoped V2 /libs/DeepLearning/Foo/thing1 mything1 Noted replacement of thing1#af2 with mything#i9d within /projects/FaceDetector/V2. ``` ``` replace.write todo ``` ``` /projects/FaceDetector> todo ...7 things... /projects/FaceDetector> todo / ...33 things... /projects/FaceDetector> ``` `mv` / `rename` command: can refer to Terms, Types, Directories, or all three. Use hash-qualified names to discriminate. ## Namespaces ```haskell data Branch' m = Branch' (Causal m Namespace) data Causal m e = One { currentHash :: Hash, head :: e } | Cons { currentHash :: Hash, head :: e, tail :: m (Causal e) } -- The merge operation `<>` flattens and normalizes for order | Merge { currentHash :: Hash, head :: e, tails :: Map Hash (m (Causal e)) } -- just one level of name, like Foo or Bar, but not Foo.Bar newtype NameSegment = NameSegment { toText :: Text } -- no dots, no slashes newtype Path = Path { toList :: [NameSegment] } data Namespace m = Namespace { terms :: Relation NameSegment Referent , types :: Relation NameSegment Reference , children :: Relation NameSegment (Branch' m) } ``` **Repo format:** ``` # types .unison/types//compiled.ub .unison/types//dependents/ .unison/types/_builtin//dependents/ # terms .unison/terms//compiled.ub .unison/terms//type.ub .unison/terms//dependents/ .unison/terms/_builtin//dependents/ # branches (hashes of Causal m Namespace) .unison/branches/.ubf .unison/branches/head/ -- if several, merge to produce new head. ``` ### Backup Names? For pretty-printing, we want a name for every hash. Even for hashes we deleted the names for. 😐 * When we delete a name `x` from path `/p` (i.e. `/p/x`), we add the name `/_deleted/p/x`. * Or, do we just disallow removing the last name of things with dependencies? * When deleting a name, notify the user of the remaining names. ## Edits ```haskell newtype EditMap = EditMap { toMap :: Map GUID (Causal Edits) } data Edits = Edits { terms :: Relation Reference TermEdit , types :: Relation Reference TypeEdit } type FriendlyEditNames = Relation Text GUID ``` **Repo format:** ``` .unison/edits// .unison/edits//name/ -- (base58encode (utf8encode "name of the edit")) .unison/edits//head/ -- if several, merge to produce new head. ``` ### TODO: How to share these edits? * It could be the same as sharing Unison names (e.g. if the edits were Unison terms) * It could be the same as sharing Unison definitions: Make up a URI that references a repo and an edit GUID. e.g. `https://github.com///<...>/[/hash]` * `clone.edits [local-name]` * `guid` comes from remote-url, and is locally given the name `local-name` * if `local-name` is omitted, then copy name from `remote-url`. * if `local-name` already exists locally with a different `guid`, then abort. ### Editsets as first-class unison terms: Benefits: * Don't have two separate dimensions of forking and causality (namespace vs edits). * Makes codebase model way simpler to explain. <— BFD Costs / todo: Q: Do we allow users to edit `EditSets` using standard `view` and `edit` in M1? If Yes: * EditSets are arbitrary Unison programs that need to be evaluated. Once evaluated, they would have a known structure that can be decomposed for EditSet operations. We would need: * * [ ] some new or existing syntax for constructing EditSet values * [x] a way to evaluate these unison programs * [ ] a way to save evaluated results back to the codebase / namespace * Q: Do we evaluate and save these eagerly or lazily? * [ ] a way in Haskell to deconstruct the EditSet value * [ ] a way to modify (append to) values of that type using CLI commands. e.g. `update` ? * either `update` calls a unison function that If no (we don't provide user syntax for constructing `EditSets` in .u file): * EditSets are part of the term language? * Or a constructor with a particular hash? (Applied to Unison terms) ## Collecting external dependencies If a subtree references external dependencies, they should be given local names when exporting. Given: ``` /A/B/c#xxx /D/E/f#yyy (depends on #xxx, #zzz) /D/G/h#zzz /libs/G/bar#zzz ``` If `/D/E` is published, what names should be assigned to `#xxx`, `#zzz`? ### Idea 1: Names relative to nearest parent Collect external dependencies under `Dependencies`, using names relative to the nearest parent in common with the publication point? i.e.: ``` f#yyy Dependencies/A/B/C#xxx Dependencies/G/h#zzz ``` ### Idea 2: Somehow derive from qualified imports used? If ### Idea 3: Surface the condition* to the user *the condition = the publication node contains definitions that reference definitions not under the publication node. Ask them to create aliases below the publication point? ### Idea 4: Add external names to `./_auxNames/` The nearest aux-name would only be used to render code only if there were no primary names known. ### Idea 5: Something with symlinks ```haskell data Branch' m = Branch' (Causal m Namespace) data Causal m e = One { currentHash :: Hash, head :: e } | Cons { currentHash :: Hash, head :: e, tail :: m (Causal m e) } -- The merge operation `<>` flattens and normalizes for order | Merge { currentHash :: Hash, head :: e, tails :: Map Hash (m (Causal m e)) } -- just one level of name, like Foo or Bar, but not Foo.Bar newtype NameSegment = NameSegment { toText :: Text } -- no dots, no slashes newtype Path = Path { toList :: [NameSegment] } data Namespace m = Namespace { terms :: Relation NameSegment Referent , types :: Relation NameSegment Reference , children :: Relation NameSegment (Link m) } data Link m = LocalLink (Branch' m) | RemoteLink RemotePath data RemotePath = Github { username :: Text, repo :: Text, commit :: Text } -- | ... future ``` This lets us avoid redistributing libs unnecessarily — let the requesting user get it from wherever we got it from. But it doesn't specifically address this external naming question. We might be publishing `/app/foo` which references definitions we got from `repo1`. Somewhere in our tree (possibly under `/app/foo` and possibly not?) we have a link to `repo1`. Somewhere under `/app/foo` we reference some defn from `repo1`. Transitive publication algorithm: * find all the things that you're referencing * the things you're publishing that aren't under the pbulication point need to be resolved * they're local, and need to be given names under the publication point * user is notified, or we do something automatic * they're remote, and we need to include, in the publication, a link to the remote repo. * user is notified, or we do something automatic * "Something automatic" will be: * mirror the dependency names from our namespace into `./_Libs`; if it would produce naming conflicts to use `./_Libs`, then `_Libs1`, etc. * Or, just dump them into `./_Libs` and if doing so produces naming conflicts, force the user to resolve them before publishing. ## Syncing with remote codetrees ```haskell -- names tbd data BranchPath = BranchPath RepoRef Path data RepoRef = Local | GithubRef { username :: Text, repo :: Text, treeish :: Text } ``` ``` /libs/community/DL ``` becomes ​```haskell BranchPath Local (Path ["libs","community","DL"]) ``` ``` gh:/[/][?ref=] -- defaults to repo's `default_branch` e.g. gh:aryairani/unison/libs?ref=topic/370 ``` becomes ​```haskell BranchPath (GithubRef "aryairani" "unison" "topic/370") (Path ["libs"]) ``` or ``` gh:user/repo[:treeish][/path] e.g. github:aryairani/unison:topic/370/libs ``` becomes ```haskell BranchPath (GithubRef "'aryairani" "unison" "topic/370") (Path ["libs"]) ``` ## Github Notes Github uses a few different URL schemes. They call the ones you can pluck off their website "html_url"s. They let you refer to files and directories, and can be parameterized by git _treeish_ (branch, tag, commit). We can interpret these to refer to the root of a namespace. https://github.com/unisonweb/unison can be interpreted as: ```haskell GithubRef "unisonweb" "unison" <$> getDefaultBranch "unisonweb" "unison" ``` The Github website will let you navigate to a git branch, e.g https://github.com/unisonweb/unison/tree/topic/370/ can be interpreted as: ```haskell GithubRef "unisonweb" "unison" <$> matchBranch "unisonweb" "unison" "topic/370/" ``` Branch names can contain slashes, such as `topic/370`, complicating parsing if there's meant to be path info following the branch name. 1. Fortunately, if you have a git branch `a/b` then it's not possible to create branches `a` or `a/b/c`. So you can load the [list of branches](https://api.github.com/repos/unisonweb/unison/branches) from JSON, and then test them against that treeish-prefixed path without ambiguity. 2. Github's website doesn't know how to navigate into `Causal` structures, so it's never going to give us URLs with paths into a Unison namespace. So maybe this is a moot point. So, I would still go ahead with the made-up `gh:username/repo[:treeish][/path]` URI scheme; we can try to support the other URLs mentioned above, and let them refer to the root of the published namespace. Our Javascript viewer can be made to create URLs with query params or fragments in them that can indicate the Unison path, and those can be the ones we share in tweets, etc: http(s)://.github.io/?branch=&path= with the default branch being the head, and the default path being `/`. ``` ``` --- ## File: docs/branchless-scratch.md ### Remembering local/remote codetree associations? ```haskell -- Designates remote paths to sync local paths against newtype RemoteMap = RemoteMap { toMap :: Map (Path, RemoteName) RemotePath } ``` If I have some branch (tree node) that I want to sync with github on an ongoing basis. e.g. `/projects/foo` to `github:aryairani/foo` — that becomes a place I can publish to or pull from, how should I associate the two? If I If I associate it by path, then what should happen when I move or copy the node in the tree? What do I have to update to make that happen? What happens if I associate it by `Causal` hash? ``` # parenthesized hashes represent the branch hash /projects (mZm)> remote.set github:user/foo foo Set remote github:user/foo for /projects/foo (0e9). ``` /projects/foo (0e9) linked to github:user/foo ``` /projects (mZm)> cp foo foo-fork /projects (wkP)> cd foo-fork /projects/foo-fork (0e9)> add myFunc Added myFunc. /projects/foo-fork (p3z)> Should now have: /projects/foo (0e9) linked to github:user/foo /projects/foo-fork (p3z) linked to github:user/foo ``` ``` # types .unison/types//compiled.ub .unison/types//dependents/ .unison/types/_builtin//dependents/ # terms .unison/terms/_builtin//dependents/ .unison/terms//compiled.ub .unison/terms//type.ub .unison/terms//dependents/ # branches .unison/branches/.ubf .unison/branches/head/ -- if several, merge entries to produce new head. # edits .unison/edits// .unison/edits//name/ -- (base58encode (utf8encode "name of the edit")) .unison/edits//head/ -- if several, merge entries # remotes .unison/remotes/ ``` --- ## File: docs/commandline-editor-dev.md The Unison CLI code is made up of a few components: `CommandLine.Main` sets up threads to watch the filesystem and parse `stdin` to produce `Editor.Event`s and `Editor.Input`s respectively. `Editor.Input` parsers are defined in InputPattern.hs and InputPatterns.hs. `Action.loop` receives `Editor.Event`s and `Editor.Input`s and executes `Editor.Command`s. This loop can't use `IO` or access the `Codebase` -- any access to these things must come from what `Editor.Command` provides. `Editor.Command`s are defined in Editor.hs and interpreted by `Editor.commandLine`. `Editor.commandLine` *does* use `IO` and access the `Codebase`.` One of the `Editor.Commands` that can be executed is `Notify`, which presents an `Editor.Output` to the user. Our current implementation is in `OutputMessages.notifyUser`. --- ## File: docs/configuration.md # Configuration * [UCM Configuration](#ucm-configuration) * [`UNISON_DEBUG`](#unison_debug) * [`UNISON_PAGER`](#unison_pager) * [`UNISON_LSP_PORT`](#unison_lsp_port) * [`UNISON_LSP_ENABLED`](#unison_lsp_enabled) * [`UNISON_SHARE_HOST`](#unison_share_host) * [`UNISON_SHARE_ACCESS_TOKEN`](#unison_share_access_token) * [`UNISON_READONLY`](#unison_readonly) * [`UNISON_ENTITY_VALIDATION`](#unison_entity_validation) * [`UNISON_SYNC_VERSION`](#unison_sync_version) * [`UNISON_SYNC_HISTORY_COMMENTS`](#unison_sync_history_comments) * [`UNISON_MIGRATION`](#unison_migration) * [`UNISON_FZF_PATH`](#unison_fzf_path) * [Local Codebase Server](#local-codebase-server) * [Codebase Configuration](#codebase-configuration) ## UCM Configuration ### `UNISON_DEBUG` Enable debugging output for various portions of the application. See `lib/unison-prelude/src/Unison/Debug.hs` for the full list of supported flags. E.g. ```sh # Enable ALL debugging flags (likely quite noisy) $ UNISON_DEBUG= ucm # Enable timing debugging, printing how long different actions take. $ UNISON_DEBUG=timing ucm # Enable LSP and TIMING debugging $ UNISON_DEBUG=lsp,timing ucm ``` ### `UNISON_PAGER` Allows selecting which pager to use for long command outputs. Defaults to `less` on Linux & Mac, `more` on Windows E.g. ```sh # User more instead of less $ UNISON_PAGER=more ucm ``` ### `UNISON_LSP_PORT` Allows selecting the port to run the LSP server on. Defaults to `5757`. E.g. ```sh $ UNISON_LSP_PORT=8080 ucm ``` ### `UNISON_LSP_ENABLED` Allows explicitly enabling or disabling the LSP server. Acceptable values are 'true' or 'false' Note for Windows users: Due to an outstanding issue with GHC's IO manager on Windows, the LSP is **disabled by default** on Windows machines. Enabling the LSP on windows can cause UCM to hang on exit and may require the process to be killed by the operating system or via Ctrl-C. Note that this doesn't pose any risk of codebase corruption or cause any known issues, it's simply an annoyance. If you accept this annoyance, you can enable the LSP server on Windows by exporting the `UNISON_LSP_ENABLED=true` environment variable. You can set this persistently in powershell using: ```powershell [System.Environment]::SetEnvironmentVariable('UNISON_LSP_ENABLED','true') ``` See [this issue](https://github.com/unisonweb/unison/issues/3487) for more details. E.g. ```sh $ UNISON_LSP_ENABLED=true ucm ``` ### `UNISON_SHARE_HOST` Allows selecting the location for the default Share server. E.g. ```sh $ UNISON_SHARE_HOST="http://localhost:5424" ucm ``` ### `UNISON_SHARE_ACCESS_TOKEN` Allows overriding the credentials used when authenticating with the Share server. E.g. ```sh $ UNISON_SHARE_ACCESS_TOKEN="my.token.string" ucm ``` ### `UNISON_READONLY` Force unison to use readonly connections to codebases. ```sh $ UNISON_READONLY="true" ucm ``` ### `UNISON_ENTITY_VALIDATION` Allows disabling validation of entities pulled from a codebase server. It's generally a good idea to leave this enabled unless you know exactly what you're doing. Defaults to enabled. ```sh $ UNISON_ENTITY_VALIDATION="false" ucm ``` ### `UNISON_SYNC_VERSION` Allows regressing to sync version 1 when interacting with Share. ```sh $ UNISON_SYNC_VERSION="1" ucm ``` ### `UNISON_SYNC_HISTORY_COMMENTS` Toggles on/off syncing of history comments with Share. Defaults to disabled. ```sh $ UNISON_SYNC_VERSION="true" ucm ``` ### `UNISON_MIGRATION` Setting: ```sh $ UNISON_MIGRATION="auto" ucm ``` will cause ucm to automatically migrate the codebase to the latest version, without prompting for confirmation. ### `UNISON_FZF_PATH` Allows configuring which binary to use when triggering fuzzy searches using `fzf`. If unset, we default to searching for a binary named `fzf` in your `$PATH`. E.g. ```sh $ UNISON_FZF_PATH="/opt/homebrew/bin/fzf" ucm ``` If you wish to disable `fzf` entirely, you can set `UNISON_FZF_PATH` to "NONE". ```sh $ UNISON_FZF_PATH="NONE" ucm ``` ### `UNISON_PULL_WORKERS` Allows setting the number of workers to use when pulling from a codebase server. Defaults to 5. ```sh $ UNISON_PULL_WORKERS=6 ucm ``` ### `UNISON_PUSH_WORKERS` Allows setting the number of workers to use when pushing to a codebase server. Defaults to 1. ```sh $ UNISON_PULL_WORKERS=2 ucm ``` ### `UNISON_SYNC_CHUNK_SIZE` Allows setting the chunk size used in requests when syncing a codebase. Defaults to 50. ```sh $ UNISON_SYNC_CHUNK_SIZE=100 ucm ``` ### Local Codebase Server The port, host and token to be used for the local codebase server can all be configured by providing environment variables when starting `ucm`, using `UCM_PORT`, `UCM_HOST`, and `UCM_TOKEN`. E.g. ```sh UCM_PORT=8080 UCM_HOST=localhost UCM_TOKEN=1234 ucm ``` ## Codebase Configuration Also, see the guide [here](https://www.unison-lang.org/learn/tooling/configuration/) The following configuration options can be provided within the `.unisonConfig` file, which exists within the codebase directory, or at `~/.unisonConfig` for your default codebase. ``` # Attach myself as author and use BSD license for all of my contributions DefaultMetadata = [ ".metadata.authors.chrispenner" , ".metadata.licenses.chrispenner" ] # RemoteMapping allows mapping a path in the codebase to a specific location on share. # Here I state that I want my .share namespace to push to .chrispenner.public # Everything inside .share will be mapped accordingly, e.g. .share.foo will map to # chrispenner.public.foo on share. RemoteMapping { share = "chrispenner.public" } ``` --- ## File: docs/distributed-api-discussion-v1.md # Distributed programming API v1 discussion ```haskell type Either a b = Left a | Right b type Status = Running | Finished | Canceled | Error Error type Error = Unknown | Unreachable | Unresponsive | AbilityCheckFailure ability Remote location where fork : location {e} -> '{e} a ->{Remote location} Future a join : Future a ->{Remote location} Either Error a status : Future a ->{Remote location} Status cancel : Future a ->{Remote location} Either Error () type Future a = Future ('{Remote loc} (Either Err a) -- join ,'{Remote loc} () -- cancel ,'{Remote loc} Status, -- status , Duration ->{Remote loc} ()) -- keepalive (seconds 10) ``` Feb 11 Q&A: * Do we need `Remote.here`? Thinking is: we don’t, we can just get one when starting the Unison Remote server; can then use that value, or restricted derivatives, in applications. ``` Unison.server -> (Location {e} ->{Remote Location} r) -- local computation -> {e} r -- rrrrresult ``` `handle expression with handler` OR `with handler handle expression` * How do you launch anything? * Watch expression lol * launch * What does it mean to `cancel`? Proposal: Runtime needs to support this. `fork`-ing in Unison likely works by forking a new instance `t` of Haskell runtime; that Haskell thread `t` can be asynchronously interrupted. So, the implementation of `Future.cancel` just throws a Haskell async exception into `t`, terminating that instance of the runtime. * How do decide if a received computation is allowed to be run? (and we are capable of running it?) 1. Some Unison term comes over the wire. 2. -Decide the type (typecheck? maybe slow? some other proof?)- No, we can use runtime exception. 3. Scan the term for unknown hashes. (Could we do this lazily? Arya says: that’s crazay [sic]! Rúnar adds: Sounds super fragile.) * Could speculatively send some dependencies with the initial request, especially if protocol has minimum message size, but maybe not easy to anticipate which dependencies will be needed at remote end. * If doing this lazily, could spare sending definitions for code paths not used during this particular execution. * Could get started running the computation if there’s any work that can be done before receiving missing dependencies. Background thread works to populate the term cache from remote sources. 4. If missing some of the dependencies, send list of references back to originator for definitions. Repeat steps 3–4 until the whole application is loaded / cached / whatever. 5. Just run it and then complain if encountering an unexpected ability request. * How do actually run one? --- Do we need to choose a representation of `Location` now? * No, we can use incrementally more sophisticated representations. e.g., loc can initially be `()` or `Nat`, and the handler can maintain pure maps or whatever. (note: need pure maps). * Yes, because the entire `Remote` ability needs to be defined up front, but some APIs e.g. relating to “keepalives” only make sense in the context of true multi-node Locations. Do we need to choose a representation of `Future` now? * Yes, because the entire `Remote` ability needs to be defined up front, but we may need additional remote abilities to operate on `Future`s. * It can just be `'{Remote loc} a` * No, this representation doesn’t contain enough info to asynchronously identify the computation, e.g. to support `Remote.status` in a multi-node implementation. * It can be some kind of handle or GUID. * Can we index typed results by untyped handle? Do we need the ability to automatically clean up zombie tasks? This informs the discussion around keepalives. * Yes: ## Locations A Location is simply a computing context with access to certain computational resources. The `Remote` ability is parameterized with a Location type `loc`, giving us significant flexibility in defining various `Remote` interpreters. The interpreter can then require a `loc` that describes resources in whatever way it likes, and the interpreter can be paired with an appropriate implementation for obtaining or generating `loc`s. For example: ```haskell runLocal : '{Remote () ()} a -> a runLocal r = step nid r = case r of {a} -> a {Remote.fork t -> k} -> handle (step nid) in k t {Remote.spawn -> k} -> handle (step (Node.increment nid)) in k nid {Remote.at _ t -> k} -> handle (step nid) in k !t handle (step (Node.Node 0)) in !r ``` Its runtime representation is essentially a collection of cryptographic tokens authorizing the use of these resources. In Unison code, a Location is represented by a `Loc {e}`. A Unison value of type `Loc {}` supports only pure computations, whereas a `Loc {Remote, GPU}` provides the `Remote` and `GPU` abilities. ### Locations have a composite runtime representation A `Loc` is represented by one or more host / port / auth tokens, along with ability use tokens. The `Remote` handler may use any algorithm in selecting a host to submit a task to, and the receiving host will run the computation provided the accompanying tokens are valid. ```haskell -- Haskell runtime representation -- individual Tokens should be cryptographically unguessable. -- Tokens may correspond to or contain quota/other data. data Loc = Loc Hosts Abilities type Token = TBD type Hosts = Map (Hostname,Port) Token type Abilities = Map Reference Token -- Map Reference (PublicKey, RandomDigits, signature(publicKey, randomDigits <> reference)) ``` ### What's in a Token? In this formulation, Token is a possibly-parameterized catch-all that includes whatever information is necessary to securely authorize some use. Stateless tokens will include: * A description of the authorized resource/activity, sufficient to be understood by the resource servers. * A signature by entity trusted by the resource server. * If the token is composite, each separable piece must be individually signed. Signatures are typically the size of the key (4096 bits = 512 bytes), so they can start to add up. They will optionally include: * An expiration / validity period - or be valid in perpetuity * An "audience", identity of the target resource server, in cases where the signature key is too broad to identify the resource server. Example: ``` Token = abilities e_1, ..., e_n <> expiration <> signature ku ([e_1 ... e_n] <> expiration) <> fingerprint ku or: (e_1 <> expiration <> signature ku (e_1 <> expiration) <> fingerprint ku) <> ... <>(e_n <> expiration <> signature ku (e_n <> expiration) <> fingerprint ku) ``` This is leading up to an exponential number of signatures, just to support `Loc.restrict`. So, let's look at some schemes for delegation. ### Elastically producing new Locations An elastic compute service “front-end” would expose: 1. a function to `provision` new locations 2. a Location at which the function could be run * Can I have this `provision` function in my namespace, without having its implementation in my codebase? - [ ] The implementation of `provision` would need some way to authenticate and validate the request. - [ ] It would need some way to construct a Unison `Loc` value (not yet discussed). - [ ] It should provide a way for the front-end to monitor utilization and spin up or shut down physical resources as needed. _Idea_: Maybe the `Token` value provided by the front-end is structured in a provider-specific way, with whatever data is needed to make these decisions. Having a distinct `Token` type for distinct providers means another type parameter on the `Loc`, which could answer the question about consolidating `Loc`s on the user side. If two Locations share the same provider type, they can be consolidated (hosts, quotas, abilities); otherwise they obviously couldn’t be. ```haskell Remote.forkAt : Loc {e} p -> '({e} a) ->{Remote} Future a Location.join : Loc {e} p -> Loc {e2} p -> Loc {e,e2} p ``` ## Futures A `Future` represents an asynchronous computation. `Remote.forkAt` takes a computation and returns immediately with a `Future`. To wait for the computation’s output, use `Future.force`. ```haskell Remote.forkAt : Loc {e} ->'({e} a) ->{Remote} Future a Future.force : Future a ->{Remote} (Either Err a) type Err = TBD -- example: f1 = forkAt a 'let x = longRunningComputation 101 makeHistogram x y = otherLongComputation x = Future.force f1 Database.save (x, y) ``` * How many times can a future be successfully forced? Suppose a future is shared with 5,000 machines. The task backing the future eventually completes, and now what? * The thought: the machines sending keepalives (subscribers?) are retained at the Location performing the computation; when the computation is complete, the Location should send the result back to those subscribers. The subscribers save the result in their caches until they no longer reference the `Future`. * Random thing - if 5,000 nodes have a reference to a future, the status update / keepalive protocol should come with a response like "send me another keeplive within X time", where X is influenced by the number of other subscribers / density of keepalives. This prevents flooding the network with keepalives. ### Supervision and garbage-collection of Futures Unison Futures can be monitored or terminated using: ```haskell Future.status : Future a ->{Remote} Future.Status type Future.Status = Running LastUpdate | Canceled | Finished | Unreachable | Unresponsive Future.cancel : Future a ->{Remote} (Either Err2 ()) type Err2 = TBD ``` To the extent that an async computation should be canceled if there is no other computation interested in its result, we need some way of determining whether or not this is the case. We discussed having a system of keep-alives, absent which a Future might be canceled by its host: ```haskell -- these likely will just be handled by the interpreter -- of Remote, not by "user" code. Future.keepalive : Duration -> Future a ->{Remote} Status Future.remaining : Future a ->{Remote} Duration ``` Moreover, there will be cases where we want to transfer or delegate the keep-alive responsibility for a long-running tasks to a more available location. ```haskell Remote.supervise : Loc {e} -> Future a -> {Remote} () Remote.unsupervise : Loc {e} -> Future a -> {Remote} () ``` > We discussed producing a `Heartbeat` identifier along with any `Future`, but decided there was no benefit to separating the two. We haven’t discussed how to prevent a delegate supervisor from accumulating and perpetuating many long-running Futures that will never actually be forced. With this in mind, have we gained anything from a GC perspective? ## Stationary data We will need some notion of data that doesn't just move automatically with the computation, even if the computation references it. We identified two reasons you might want to do this: * The data is big, and you don't want to copy it around willy-nilly. * The data is secret, and you don't want to accidentally ship it to another location, you want to be very explicit about when this happens (for instance, secret keys, etc). More generally, we want a way of being explicit about when certain data is moved between locations, rather than implicitly relocating anything in lexical scope (this could be an API thing, a type-system thing, a code-analysis tool). - - - - ## Notes/Desiderata - [ ] Elastic computation - need to be able to talk about spawning new computing resources, and ideally this compute can be garbage collected as soon as you're done using it. - [x] `fork` a task to run on a separate thread or at another "location" - [x] Different locations may have access to different abilities (just pure computation, `IO`, `GPU`, etc) - [x] Need to be able to respond to location failures, with maximal flexibility. Allow different ways of doing failure detection/recovery. * Locations are first-class, permissions, tasks, are first-class - [x] locations - [ ] permissions? - [x] tasks (futures) - [ ] Some notion of data that doesn't just move automatically with the computation, even if the computation references it. * e.g., The data is big, and you don't want to copy it around willy nilly. * e.g., The data is secret, and you don't want to accidentally ship it to another location, you want to be very explicit about when this happens (for instance, secret keys, etc). * Might more generally want a way of being explicit about when data is moved to a location rather than just implicitly relocating anything in lexical scope (could be an API thing, a type system thing, a tool). - [x] Need to be able to launch a long-running computation and have it outlive the task / location / node that launches it. But then how do you interact with this computation later? (Say, to cancel it? Or to check if it's finished? Or more generally, how do you monitor it?) - [ ] Need to be able to hash and serialize any Unison value, so that storage API(s) can be implemented in pure Unison. * Should the hash of a value know the type of the value? (`hash : a -> Hash a`) - [x] How do you represent `Loc{e}` to be securely verified by the receiving node? The `Loc{e}` must be unguessable and tamper-proof. * This is achieved by making the component `Token`s unguessable and tamper-proof. - [ ] Must be safe to say `at loc1 loc2` without allowing nefarious loc1 to abuse loc2. (Needs clarification.) - [x] The runtime needs an unguessable way (crypto?) to represent Locations and their abilities. - [ ] Not all computations should have access to all data. * file system * individual durables - [x] Not all Locations should provide unlimited resources to all users (arbitrary computation, time, storage, bandwidth, priority). - [ ] Not all data should be portable to arbitrary locations (think secret keys, top secret clearance, hipaa). _Misc?_: * Mutable typed (durable if needed) state at each location * For v1, could not have this, just focus on batch computation * Dealing with weird networks? (nat-busting) * Maybe in implementation, but not explicit in v1 API * Well-defined semantics not just a bunch of implementation-defined gobbledygook * Do we need globally-addressed mutable state? e.g. node `a` can refer to mutable data on node `b`; or node `c` can mutate data on node `d`. Yes, probably. ## Choices * We decided that automatically cancelling a child computation when its parent terminates or delaying termination of of the parent until its children complete would break associativity in terms of parallelism when chaining computations, therefore `forkAt` doesn’t enforce any such conditions. See more about cancellation & termination below, in “Supervision and garbage-collection of Futures” #unison --- ## File: docs/github-actions-help.md ## Some things I wish I'd known about Github Actions You can't have an `env:` key defined in terms of another `env` key, but you can use `$GITHUB_ENV` to get around this. You can't define a `matrix` at the top level, it has to be defined within a `job`'s `strategy`. Matrix values aren't automatically strings. They can be other JSON types (or at least they can be booleans)! This matters if you are using them in boolean expressions later, because `true != 'true'`. Note that `!` at the start of a YAML value is a special syntax for casting the value as a particular standard or custom data type. So `foo: !bar` won't work, and does it fact get red squigglies in vscode. `foo: (!bar)` works, and I think `foo: ! bar` might also. `runs-on:` doesn't allow `env` for some reason. Strings don't need quotes, unless you need to force something to be a string. A `@ref` is always needed on a remote action, i.e. `owner/repo[/subaction]@main`. Windows doesn't seem to honor the `default: run: shell:` setting, so you need to set the `shell:` on `run:` manually? Don't hesitate to do a lot with `run:` blocks aka bash scripts — at least bash is mature and well documented. e.g. echo "bar=whatever" >> $GITHUB_OUTPUT # access with `steps..outputs.bar` in yaml strings echo "foo=whatever" >> $GITHUB_ENV # access with `env.foo` in yaml strings, or `$foo` in bash `$GITHUB_ENV` updates the `env` context between steps, but not in the middle of a step. Obvious in retrospect. It's not clear to me when to use `$GITHUB_OUTPUT` vs `$GITHUB_ENV`, but I have been favoring `$GITHUB_ENV` because it requires fewer characters to access. However, it seems a little wrong. ### `hashFiles()` `hashFiles()` can only access files inside of and relative to `$GITHUB_WORKSPACE`. ### `if:` Although the type rules don't totally make sense in Github Actions, `if:` takes a Boolean. Therefore, I think the String interpolation in `if: ${{runner.os}} != 'Windows'` causes the whole expression to become a String, which is coerced to `true`, when you definitely didn't mean `if: true`. So don't use `${{}}` here. ### Job names Job names will automatically get `(${{matrix.os}})` if you don't use `${{matrix.os}}` somewhere in the name. ### Windows The whole thing with `.exe` is a mess. Unix commands typically drop and add `.exe` correctly as needed, but Github Actions (e.g. `actions/upload-artifact`?) don't. ### Cache When using the `cache` action, getting a cache hit on the primary key means you won't update the cache with any changes. When picking a key, you have to ask, "Which key, if exactly matched, would mean that I'm already SO done that I don't even want to save anything new from this run." Similarly, `save-always: true` only if a key hit means there will be nothing new to save, even if a previous run failed AND a failed result is worth starting with. Backup restore keys: "Is there a prior run that would be worth starting out from? With the caveat that any irrelevant garbage it includes will be saved into this run too." ### Upload Artifact I suspect on Windows it can't support paths that select a drive in a Unix-y way, like `/c/asdf` or `/d/asdf`. It's got to be `C:/asdf` or `C:\asdf` etc. Upload will complain if any Upload and Download plugin versions have to match. ### Reusability Github supports splitting off "reusable workflows" (`jobs` that can be imported into another workflow), and "composite actions" (multi-step `steps` that can be imported into another `job`). #### Composite actions Needs to have `shell:` specified on every `run:` #### Reusable workflows These have to be in `.github/workflows`, you can't organize them deeper, or elsewhere. ### Reference Default Environment Variables: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables Workflow syntax: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions Reusable workflows: https://docs.github.com/en/actions/using-workflows/reusing-workflows Composite actions: https://docs.github.com/en/actions/creating-actions/creating-a-composite-action --- ## File: docs/mcp.md # MCP Setup UCM comes packaged with a built-in MCP server for use with AI agents. It includes tools for allowing an AI agent to inspect and search your code, write and typecheck new code, search Share for projects and definitions, and more! ## Setup ### MCP as an independent process (Recommended) This approach allows agents to connect to UCM's MCP server directly via stdin/stdout. Note that this causes an additional UCM to run as an entirely independent process for each agent you're using. #### Claude Code, IBM Bob, Gemini CLI, and Antigravity CLI To configure the MCP for use with Claude Code, IBM Bob, Gemini CLI, Antigravity CLI, and any tools which read the same `mcpServers` JSON config shape, edit the appropriate config JSON file: Claude Code: * On Mac: `~/Library/Application Support/Claude/claude_desktop_config.json` * On Windows: `%APPDATA%\Claude\claude_desktop_config.json` * On Linux: `$HOME/.claude.json` IBM Bob: * Global: `~/.bob/settings/mcp_settings.json` * Project: `.bob/mcp.json` Gemini CLI: * Global: `~/.gemini/settings.json` * Project: `.gemini/settings.json` Antigravity CLI: * Global: `~/.gemini/antigravity-cli/mcp_config.json` * Project: `.agents/mcp_config.json` Configure a `unison` key in your `mcpServers` object as below. Replace `` with the path to your `ucm` executable. E.g. on Mac this is likely `/opt/homebrew/bin/ucm`, you can run `which ucm` to find your UCM executable path. ``` json { "mcpServers": { "unison": { "command": "", "args": ["mcp"] } } } ``` _e.g._ my complete file on macOS looks like this: ``` json { "mcpServers": { "unison": { "command": "/opt/homebrew/bin/ucm", "args": ["mcp"] } } } ``` After saving the file, restart the agent. For Claude, you should then see a new "unison" option in the MCP server list. For Bob, make sure "Use MCP Servers" is enabled in the MCP settings, then manage or restart the `unison` server from there. In Gemini CLI, Claude Code, and Antigravity CLI, check with `/mcp`. #### Codex Codex is primarily used for OpenAI models, but can be used with other model providers that support the OpenAI API. Configuration is similar to Claude Code; locate your Codex config file: * On Linux: `$HOME/.codex/config.toml` ``` toml [mcp_servers.unison] command = "/path/to/ucm" args = ["mcp"] ``` Restart `codex`; you should now be able to see the Unison MCP server by entering `/mcp` in Codex: ``` /mcp 🔌 MCP Tools • Server: unison • Command: /home/bbarker/.nix-profile/bin/ucm mcp • Tools: docs, get-current-project-context, lib-install, list-definition-dependencies, list-definition-dependents, list-library-definitions, list-local-projects, list-project-branches, list-project-definitions, list-project-libraries, search-by-type, search-definitions-by-name, share-project-readme, share-project-search, typecheck-code, view-definitions ``` #### Kiro CLI Kiro CLI supports MCP servers at both global and workspace scope. **Workspace scope** (recommended — scoped to this project): Create `.kiro/settings/mcp.json` in the project root: ``` json { "mcpServers": { "unison": { "command": "", "args": ["mcp"] } } } ``` **Global scope** (available in all projects): ``` bash kiro-cli mcp add --name unison --command --args mcp --scope global ``` If you're using a custom agent (e.g. defined in `~/.kiro/agents/my-agent.json`), workspace and global MCP servers are not automatically included. Instead, add the same `mcpServers` entry above directly to the agent's JSON config file. After saving, restart Kiro CLI. You can verify the server is loaded with `/mcp` in chat. ### Connecting to a running UCM executable (not recommended) If instead you wish to connect an agent to a running UCM executable you can use an HTTP MCP connection. This is less consistent than the stdio approach, since some agents have rather poor handling of error states if you close your running UCM, or start up an agent without UCM already running. The following defaults should work if you haven't tweaked things, but ensure you use the correct port and token if you've changed them: ``` { "mcpServers": { "unison": { "type": "streamable-http", "url": "http://localhost:5858/codebase/mcp", "note": "Replace 5858 and 'codebase' with your UCM_PORT and UCM_TOKEN respectively if you've changed the defaults." } } } ``` After saving the file, restart the Claude Desktop app. You should now see a new "unison" option in the MCP server list. ## Usage By default, your coding assistant will automatically use the Unison MCP server when it deems it appropriate, however you'll get much better results with additional prompting. You may wish to start with the prompts [in this repository](https://github.com/unisoncomputing/unison-llm-support/tree/main), in particular this [main prompt](https://github.com/unisoncomputing/unison-llm-support/blob/main/instructions.md) which delegates to one of several "modes" depending on the situation, with clear instructions for each. There are also some prompts available in the MCP server itself (for instance, there is a Unison language guide). If you're using Claude Desktop, you can find these prompts by clicking the "plus" icon next to the prompt input box, and then choosing `Add from unison` and selecting the appropriate prompt. --- ## File: docs/nix.md (Todo: it might be nice to have a gentle and concise introduction into why Unison does anything with Nix and what cachix is.) We can push any nix store path into our cachix cache. This is typically done with `cachix push unison `. Some ways to come up with a store path: 1. If you build something you get a symlink to the store path of the built thing, named `result` by default. 2. With `nix path-info` So, you could push the unison executable with the cache with ```nix nix build -o my-little-unison-store-path cachix push unison my-little-unison-store-path ``` or ```nix nix build | cachix push unison ``` We want to cache the [immediate] build dependencies of our build products, because those are the only ones actually needed to build our build products. ```nix nix-store --query --references $(nix path-info --derivation) | xargs nix-store --realize | cachix push unison ``` Breaking down the above: ```nix nix path-info --derivation ``` gets the store path of the derivation of the unison executable ```nix nix-store --query --references $(nix path-info --derivation) ``` gets the store paths of the derivations of immediate dependencies of the unison executable derivation. ```nix nix-store --query --references $(nix path-info --derivation) | xargs nix-store --realize ``` builds the above derivations if necessary and writes the resulting store paths to stdout These paths are then fed to cachix with `| cachix push unison`. Development environments are defined in the flake under the `devShells` key. There are a number of different development environments, and they can be entered by giving a different argument to `nix develop`. If you want to push a development environment you could do so with something like: ```nix nix build --no-link '.#devShells.x86_64-linux.default' | cachix push unison ``` and you could push the build dependencies of the default shell with something like ```nix nix-store --query --references $(nix path-info --derivation '.#devShells.x86_64-linux.default') | xargs nix-store --realize | cachix push unison ``` ```nix nix-store --query --references $(nix path-info --derivation '.#devShells.aarch64-darwin.default') | xargs nix-store --realize | cachix push unison ``` --- ## File: docs/publishing.md # Publishing Unison code Thinking about a design for publishing Unison code revealed a nest of interrelated concerns: * [Using library code in my application & sharing my application as a library](publishing-library1.md) * [Updating my library & sharing an updated library](publishing-library2.md) Each of these linked subtopics presents concerns, questions, and ideas, which we can weigh and collect into [our M1 proposal](publishing-M1.md). ## 2. Official Technical Reference & Guides (unisonweb/website) # Unison Website Main Unison website ## Where is all the content? The Unison website is primarily constructed via Unison `Doc`, and the content lives in the associated Unison codebase. Through a build process and the ucm `docs.to-html` command, this content is transformed to HTML pages, post processed for better fit for the website and finally built by eleventy (static site generator). This includes both collections like docs, articles, and posts, as well as individual pages. Some collections have "partials" that make up their pages. Articles for instance has a `_title` Unison `Doc` term that is transformed into the page as frontmatter for eleventy, where Docs has `_sidebar` term that makes up the index of the language documentation. Browse the Unison Docs that power the website here: https://share.unison-lang.org/@unison/website/code/main/latest ## Filing tickets and contributing to the Docs Tickets about the website content should be filed [using the Unison Share ticketing feature](https://share.unison-lang.org/@unison/website/tickets). PR's are gratefully accepted through the regular [Unison code contribution workflow](https://share.unison-lang.org/@unison/website/code/main/latest/terms/docs/contributeDocs). ## Setup for running locally and deployment Ensure all dependencies are installed by running `npm install` (node v17+ recommended). Run `ucm --codebase-create .` and then `clone @unison/website` inside `ucm` to initialize the required Unison `Doc`. ## Running the website locally Once you've followed the steps from the previous section, `npm start` will both build and start a local version of the website. ## Build process 1. Convert Docs in a Unison Namespace to HTML files in the build/articles and build/docs 2. Pre-process HTML with proper links and parse article sidebars 3. Copy from build to src/articles and src/docs 4. Render via eleventy to build/site ## Deployment process Note that this requires being setup in Netlify with the correct access. - `npm run deploy:prod` will deploy the website to production on https://unison-lang.org. After the deploy to Netlify has succeeded, update the algolia search index by going to https://unison-lang.org/algolia.json and saving the json file. Then go to the "docs" index in Algolia, clear it (removing all records), and upload the downloaded json file.