This is branch of the editor focuses on expanding its functionality towards translating the generated FLINT norm specification into an executable form (eFLINT). Part of code contributed by https://github.com/Azoth4006/bsc-thesis.
Figure 1: Screenshot of the "Make Interpretations executable" tab that translates the FLINT specification to eFLINT
The Rule Editor is an application built using web-based technologies that allows users to create interpretations of sources of norms in FLINT in a user-friendly and interactive way. The tool was built using Vue.js and Quasar. The app is deployed on Netlify and uses Netlify functions (serverless functions, including Edge Functions) to extend back-end capabilities.
Try it out
There are two public versions of the Rule Editor available:
- Stable version: The most stable release hosted at https://rule-editor.netlify.app/.
- Latest (preview) version: Includes the newest features and experimental changes (not fully tested or stable) hosted at https://develop--rule-editor.netlify.app/.
Feel free to explore both!
- Application Overview
- Features
- User Manual
- Data Model
- Getting Started
- Codebase Structure
- Development
- Environment Variables
- Contributing
- License
The Rule Editor is a web application for interpreting normative tasks. To use the editor, a task is defined and the normative text (sources) describing the task and the constraints for its execution are collected and imported into the editor. The editor allows users to mark components (e.g., articles, sections, or sentences) of the source as relevant or irrelevant to the task. Relevant sources can be annotated and used as building blocks for the interpretation.
The current version of the Editor enables users to express interpretations in FLINT. The Editor is designed to be easily extendable for other interpretation schemes. It allows users to get automated recommendations, while working on their interpretation by using the FlintFiller. This feature is experimental and available only for Dutch texts.
The Editor uses normative text in JSON or RDF format, according to the Source of Norms Ontology. Text documents in .txt, .xml, or .html format can be translated into this format by the Choppr tool.
Interpretations made using the Rule Editor can be stored as JSON or TriG files locally on your computer or remotely to a linked database/triple store. When choosing a Linked Data output, interpretation data conforms to the FLINT ontology. Linked Data on interpretations, sources, and the related task is bundled in the output according to the Calculemus ontology.
- User interface built using modern technologies, specifically Vue.js (front-end framework) & Quasar (UI Components & Toolkit).
- Serverless back-end with Netlify Functions (API endpoints).
- Netlify Edge Functions(Edge middleware) for middleware-like behaviour.
- Easy deployment and continuous integration with Netlify (Hosting & CI/CD).
- Store, share, and use interpretations locally in JSON or TriG formats, and publish as linked data knowledge graphs to the TriplyDB platform.
- Automated recommendations for interpretations using FlintFiller (experimental feature, Dutch language only)
The interface of the Rule Editor consists of five main views:
- Set task
- Collect sources
- Interpret sources
- Make interpretations executable (not yet in use)
- Execute task (not yet in use)
Navigate between views using the tabs at the top of the page.
In the current version one, it is recommended to start by defining a task and to edit three fields
| Field | Explanation | Example |
|---|---|---|
| Editor | Field to register the name of the person using the Rule editor | John Doe |
| Label | Label to refer to the task | National Budget Cycle |
| Description | Description of the task | What is needed to arrive at an approved national budget? |
It is possible to navigate across all views without restrictions.
There are three ways of adding sources to the editor:
| Field | Explanation | Example |
|---|---|---|
| Dropdown menu labelled "Add source from server" | Field to add a source from the Rule editor server | General Data Protection Regulation |
| Dropdown menu labelled "Add source from Triply" | Field to add a source from the Triply linked data store | General Data Protection Act |
| Button labelled "Upload source from local filesystem" | A button that allows to add a source from local filesystem (in JSON-LD). Examples of source can be found here. | AI_Act.json |
Select the source or the sources you consider relevant in relation to the task you are working on.
Use the checkboxes to select or deselect text fragments. There are also buttons to select all or to deselect all.
You can always get back to this screen to add additional sources, or remove redundant sources or text fragments.
Once you have selected the relevant sources for your task, you can go to the next step by clicking the Interpret sources tab.
The only way to navigate between screens is by using the tabs at the top of the page.
The interpretation screen consists of four main panes:
- Source pane: Contains the selected sources.
- Source of selected frame: Indicates the source of the selected frame.
- Frames: Lists the created frames by type.
- Edit: Where you can create and modify FLINT frames.
Each pane is collapsible, allowing you to allocate more space to the pane you are focussing on.
Source pane
Expanding source text
You can use the expand and collapse icons to show or hide the text.
Selecting text
Select a text fragment (from a single word to multiple lines). When your selection is complete, a modal dialog appears, allowing you to:
- Create a frame (act, claim, or fact), or
- Link the selected text fragment to an existing frame.
You can also click on selected lines to delete the selection or open the frame in the Edit pane for further analysis.
Source of selected frame
This pane provides an overview of the linked text fragments (per source) for the currently selected frame.
Frames
The Frames pane lists all created frames by type. You can filter frames by label using the search field at the top. Alternatively, you can switch to a network visualization, which shows how different frames are related. Switch between List and Network views using the provided radio buttons.
Edit
Here you can create, edit, and delete frames. Supported frame types are act, claim, and fact.
When starting your intepretation, begin by creating some facts.
You create a fact by selecting a text fragment. Click on the selected fragment and click on the fact button. On the right-hand side of the view a fact frame appears.
The selected text appears in the fields short name and full name. If necessary one can make changes in the text, e.g. change the conjugation of an action from the present perfect to the present simple tense.
For facts with a longer text fragments, add a short name.
A fact consists of:
- the text fragment from a source
- the full name, that is the same as the a literal text from the fragment, adjustments can be made regarding to:
- the conjugation of the action,
- punctuation issues,
- making a readable sentence in case of the combination of multiple fragments from a single sentence, or even from multiple sentences,
- making implicit information explicit (add a comment to explicitly lay down the implication).
- a short name for longer text fragments.
Examples are given below.
You can classify facts as one of the following:
- Agent
- Action
- Object
- Duty
Which role a fact takes depends on how it is used in act frames or claim frames:
- Agents: can be actors or recipients in act frames, or claimants/duty holders in claim frames.
- Actions are verbs used in act frames.
- Objects are the things an action refers to, or the thing created or terminated as the result of a valid act.
- Duties refers to acts that must be executed by (or on behalf of) a duty holder for a claimant. In other words every duty is created by an act and can be terminated by another act.
Search for agents (persons that can have the role of actor, recipient, claimant, or duty holder, e.g. a processor, a controller, or data subject ), actions (e.g. processing, collection, erasure), objects (e.g. personal data, or collected data), conditions (e.g. 1. Personal data shall be: (a) processed lawfully, fairly and in a transparent manner in relation to the data subject (‘lawfulness, fairness and transparency’);)
The data subject, processor and controller can be found in Article 4(1),(7) and (8).
The action verbs that can be derived from processing, collection, erasure in Article 4(2) are: process, collect and erase.
Article 5 is about the processing of personal data, Article 5(1)(b) is about collected personal data (the result of collecting personal data).
The condition that 1. Personal data shall be: (a) processed lawfully, fairly and in a transparent manner in relation to the data subject (‘lawfulness, fairness and transparency’); can, e.g., be transformed to the long name personal data shall be processed lawfully, fairly and in a transparent manner in relation to the data subject (‘lawfulness, fairness and transparency’). The short name could be, e.g., personal data shall be processed lawfully, fairly and in a transparent manner.
Create acts in two ways:
- Select sentences and mark them as an act frame,
- Select pre-existing fact frames and assign them a role in an act frame.
To create core-acts (the combination of an action, the actor performing the action, the object that is acted upon and the recipient of the result of the action), it is advised to select whole sentences.
A claim contains:
- a duty
- a claimant
- a duty holder.
The claimant holds a claim that the duty holder fulfills the duty.
A duty should be linked to:
- One or more acts that can create the duty
- One or more acts that terminate the duty
Not yet available.
Not yet available.
The tool uses an internal data structure that differs slightly from the JSON and RDF format in which the interpretations are stored.
The definitions of the classes used in the tool can be found in the folder gui/src/model. The main classes are:
-
frame: This is a class representing a frame. It is the superclass ofact,fact, andclaim_duty. Each frame has the following attributes:- id: A unique id, generated when a frame is instantiated.
- typeId: The name of the frame's type (one of 'fact','act','claim_duty')
- shortName: A short label as displayed in the frame list.
- fullName: The full name of the frame, displayed when hovering the frame in the frame list.
- annotations: A list of
annotationobjects. See below for a detailed explanation of an annotation. - comments: A list of
commentobjects. These are remarks and considerations that the user can store as notes attached to the frame when creating the interpretation.
-
fact: A frame of type fact. In addition to the attributes inherited fromframeit has:- subTypeIds: A possibly empty list of subtype id's. A fact can have zero, one, or more subtypes. Valid subtype id's are: 'agent', 'action', 'object', 'duty', 'condition'.
- subDivision: An object of type
booleanConstructrepresenting a subdivision of a fact. A fact can consist of other facts, but not necessarily. This attribute speficies how a fact is subdivided. See below for an explanation of a booleanConstruct.
-
act: A frame of type act. In addition to the attributes inherited fromframeit has attributes for the roles of an act frame:- action: An object of type
factrepresenting the action of an act - actor: An object of type
factrepresenting the actor of an act - object: An object of type
factrepresenting the object of an act - precondition: A
booleanConstructrepresenting a fact or a combination of facts that form the precondition of an act - recipient: An object of type
factrepresenting the recipient of an act - creates: A list of zero or more
factobjects created by an act - terminates: A list of zero or more
factobjects terminated by an act
In addition, an act has auxiliary attributes, not part of the external data model:
- activeField: The role of the act that is currently selected by the user
- generateLabelAutomatically: If true, labels (shortName and fullName) are generated automatically for the act
- action: An object of type
-
claimduty: A frame of type claim-duty. In addition to the attributes inherited fromframeit has attributes for the roles of a claim-duty frame:- duty: An object of type
factrepresenting the duty of a claim-duty - claimant: An object of type
factrepresenting the claimant of a claim-duty - holder: An object of type
factrepresenting the holder of a claim-duty
In addition, a
claim-dutyhas auxiliary attributes, not part of the data model:- activeField: The role of the claim-duty that is currently selected by the user
- generateLabelAutomatically: If true, labels (shortName and fullName) are generated automatically for the claim-duty
- duty: An object of type
-
booleanConstruct: This class is used to specify a combination of frames. Frames are combined using functions, e.g. boolean operators like OR and AND. AbooleanConstructcan be nested: it can combine other boolean constructs as in:booleanConstruct_1ANDbooleanConstruct_2. AbooleanConstructis a tree, where the leafs arefactsand all other nodes are booleanConstructs. It has the following attributes:- frame: This attributes holds a frame in case the booleanConstruct is a leaf, i.e. it is a single frame, not a combination of frames.
- children: If the booleanConstruct is not a leaf (i.e. frame is not null), this attribute is a list of booleanConstruct objects.
- operatorToJoinChildren: The function with which to join the children, e.g. a boolean operator.
- isNegated (deprecated): If a frame is specified, this attribute tells whether the frame should be negated (i.e. the unary boolean function NOT is applied). [DEPRECATED] This property will be removed in future versions. Use operatorToJoinChildren instead.
-
sourceDocument: This class holds a source document, e.g. the content of a law like the Participatiewet. Its constructor reads a jsonLD object and parses it into a nested structure of sentence objects. The leafs of this structure are individual sentences. The nodes higher in the hierarchy represent paragraphs, chapters, etc. It has these attributes:- title: The title of the source document
- sentenceTree: A nested structure of
sentenceobjects
-
sentence: The text of a sentence, or a heading (i.e. label of a heading, paragraph, section). The text of a sentence (or heading) is divided in snippets (see below) so that an annotation of a frame can contain part of a sentence, and not necessarily complete sentences. Its most important attributes are:- sourceDocument: The
sourceDocumentthis sentence is part of - snippets: A list of
snippetobjects that comprises this sentence - children: If this sentence is a section or other higher-level element in the document: a list of
sentenceobjects that together form this section
- sourceDocument: The
-
snippet: An atomic piece of text. The text is specified as a character range within a sentence. The snippet refers to all annotations of which it is a part. Its attributes are:- id: A unique identifier
- sentence: The sentence object the snippet is part of
- characterRange: The start and end indices of the character range within the sentence. A snippet that starts from the beginning of the sentence has start index zero
- annotations: The annotations associated with this snippet
-
annotation: This object links snippets to a frame. The snippets form the annotation of the frame. Its attributes are:- id: A unique identifier
- frame: The
frameobject that this is the annotation of The link between snippets and annotation is stored in the snippets, see above. Auxiliary attributes used for drawing coloured lines under the source text are: - nrSnippets: Number of snippets of this annotation
- verticalPosition: The vertical position of the coloured line that marks this annotation in the source text
.
├── interpretations/ # List of example interpretations
├── gui/ # UI code
│ ├── netlify/
│ │ ├── edge-functions/ # Edge functions
│ │ └── functions/ # Serverless functions
│ ├── public/ # Static assets (favicon, robots.txt, etc.)
│ ├── src/
│ │ ├── assets/
│ │ ├── components/ # The components performing functionalities are here used by the views in the views/
│ │ ├── helpers/ # Reusable functions and utilities that are used by components
│ │ ├── model/ # The data model that the application is based on
│ │ ├── services/ # the calls to the endpoints are defined here
│ │ ├── store/ # The centralized store for the components
│ │ ├── views/ # The main views of the UI are defined here
│ │ ├── App.vue # Main Vue root component
│ │ └── main.js # Application bootstrap JavaScript entry point (mounts Vue app).
│ ├── .gitignore # Git ignored files
│ ├── Dockerfile
│ ├── .eslintrc.js # ESLint code linting configuration.
│ ├── .prettierrc.json # Prettier configuration
│ ├── index.html # application entry point, root DOM node
│ ├── package-lock.json # Records the full, exact dependency tree and versions
│ ├── package.json # Project dependencies and scripts
│ ├── .env # Environment variables
│ └── vite.config.js # Vite and dev server configuration, plugin registration
├── auth-service/ # Dedicated authentication API (cookie/session based)
│ ├── app.py
│ ├── requirements.txt
│ └── scripts/
├── .gitignore # Git ignored files
├── docker-compose.yml
├── netlify.toml # Netlify build, functions and routing configuration
├── LICENSE # Project license
└── README.md # Project documentation
- Node.js (version
18.xor later recommended) - npm
- Netlify CLI (version
17.38has been used) for local function testing
- To connect to TriplyDB an access token with TRIPLY is needed. You can register here and become more familiar with their environment here.
- To use the Netlify CLI an access token to authenticate with Netlify is needed. You can obtain this token using the Netlify UI. See the docs.
To run the editor locally for development purposes. You can do the following:
git clone git@gitlab.com:normativesystems/ui/interpretation-editor.git
cd interpretation-editorFrom project root, navigate to the gui folder:
cd gui/npm ci
Start the development server with hot reload
npm run devBy default, vite allocates port 5137 to serve the application. Once the app is running, you can view the UI at localhost:5137. If you would like to use another port, you can also pass the additional --port=XXXXparameter:
npm run dev -- --port=XXXXYou can also pass the --open parameter to have the UI automatically opened in a new browser tab:
npm run dev -- --openHave in mind that by following the previous steps, you will not be able to use Netlify's serverless functions. If you want to develop and test Netlify functions locally, use the following command:
netlify devThis starts both SPA and Netlify Functions with live reload.
To create a production version of your app:
npm run buildYou can preview the production build with npm run preview.
If you want to run the GUI as static files (without netlify dev), use this flow.
- Configure frontend runtime targets in
gui/.env.production(example values for local stack):
(or replace with actual URL instead of localhost if live in production)
VITE_AUTH_ENABLED=true
VITE_AUTH_API_BASE_URL=http://localhost:8101
VITE_EFLINT_API_BASE_URL=http://localhost:8000
VITE_EFLINT_EXECUTE_URL=http://localhost:8001
VITE_EFLINT_SERVER_BASE_URL=http://localhost:8080
VITE_MONGO_API_BASE_URL=http://localhost:8102
VITE_X_API_KEY=- Build static assets from
gui/:
npm run build- Serve the generated
distfolder (example):
npx serve -s dist -l 4173- Open
http://localhost:4173.
If your server serves files from /var/www/rule-editor/dist, deploy updates with:
sudo rsync -av --delete ~/rule-editor/gui/dist/ /var/www/rule-editor/dist/Then validate and reload Nginx:
sudo nginx -t
sudo systemctl reload nginxQuick checks when you see 500:
- Confirm
index.htmlexists at/var/www/rule-editor/dist/index.html. - Ensure your server block uses the same root path (for example
root /var/www/rule-editor/dist;). - Keep SPA fallback as
try_files $uri $uri/ /index.html;and avoid proxying/to a stopped dev upstream (e.g.127.0.0.1:8888).
Notes:
VITE_*values are embedded at build time, so rebuild after changing.env.production.- Set
VITE_EFLINT_SERVER_BASE_URLfor static deployments so session endpoints (/sessions,/reset,/spec/register,/statements,/query/holds) target youreflint_serverinstance. - For Mongo-backed project storage/loading,
VITE_MONGO_API_BASE_URLmust be set. - Netlify Functions paths (
/api/serverless/*) are not available in pure static hosting; Triply-related actions will not work unless you replace those endpoints with your own backend.
From the project root, you can start the self-hosted backend dependencies with one command.
- Create and fill stack environment file:
cp .env.stack.example .env.stack- Add at least one auth user hash to
AUTH_USERS_JSON(in.env.stack), for example:
cd auth-service
python scripts/generate_auth_user.py --username editor --env-file ../.env.stack
cd ..- Start services:
docker compose up -d --build- Check health endpoints:
curl http://localhost:8101/health
curl http://localhost:8000/healthThis starts:
auth-serviceonhttp://localhost:8101flint-to-eflintonhttp://localhost:8000mongo-apionhttp://localhost:8102mongodbonlocalhost:27017
The observability stack runs as a separate Docker Compose project that attaches to the backend network. The main app stack must be running first so the shared Docker network exists.
Services exposed locally:
grafanaonhttp://127.0.0.1:3000prometheusonhttp://127.0.0.1:9090lokionhttp://127.0.0.1:3100promtailandblackbox-exporter(internal only)
Setup:
- Create and edit the monitoring env file:
cp deploy/monitoring/monitoring.env.example deploy/monitoring/monitoring.env
# Edit: set GRAFANA_ADMIN_PASSWORD and verify MONITOR_TARGET_NETWORK
# Default network name (rule-editor_rule-editor) is correct if the project folder is named rule-editor.
# Confirm with: docker network ls | grep rule-editor- Start the monitoring stack:
docker compose --env-file deploy/monitoring/monitoring.env -f deploy/monitoring/docker-compose.monitoring.yml up -d- Open Grafana at
http://127.0.0.1:3000and log in.
Grafana is pre-provisioned with Prometheus and Loki datasources and a starter dashboard (Rule Editor Observability) showing:
- health probe status for all core service
/healthendpoints - centralized container logs from the Docker Compose project
Stop monitoring stack:
docker compose --env-file deploy/monitoring/monitoring.env -f deploy/monitoring/docker-compose.monitoring.yml downWant to deploy immediately to Netlify? Click this button
Clicking this button will create a new repo for you that looks exactly like this one, and sets that repo up immediately for deployment on Netlify.
Netlify functions are in the functions/ directory. To test them locally with the app:
netlify devYou can then make HTTP requests to /.netlify/functions/[function-name].
For adding new functions, simply create a new folder in the /functions folder and add an .mjs or .js file to it.
You can find more information in the Netlify Functions docs.
Edge functions live in /.netlify/edge-fuctions/. See Netlify Edge Functions docs.
Routing for Edge functions is configured in netlify.toml.
The application requires several environment variables to function properly, especially for accessing external services such as TriplyDB. Add these variables to your Netlify dashboard or a local .env file as appropriate.
| Variable | Explanation |
|---|---|
| TRIPLY_KEY_R | API key required for reading from TriplyDB (Requires the creation of an account on the TRIPLY website and they can issue one for you) |
| TRIPLY_KEY_W | API key required for writing to TriplyDB (Requires the creation of an account on the TRIPLY website and they can issue one for you) |
| TRIPLY_ENDPOINT | Base URL of the TriplyDB instance/API (Requires the creation of an account on the TRIPLY website) |
| X_API_KEY | Generated API key for authentication purposes. Used by the Edge function to redirect to the correct TRIPLY_DB serverless functions. Create your own. |
| ALLOWED_DOMAINS | The domain from which the Edge function should expect the requests. If you run a local development server, you can set the value to http://localhost:[designated_port] |
| VITE_X_API_KEY | API key required for fetching NLP predictions, and converting your interpretation in RDF and JSON to save them locally. Please contact us to generate one for you. |
Create an .env file in the gui folder and define the environment variable there.
Authentication is implemented as a dedicated API service in auth-service/app.py.
The eFLINT translation backend (flint-to-eflint/service/app.py) only validates signed session cookies and no longer stores user password hashes.
Authentication service environment variables:
| Variable | Explanation |
|---|---|
| AUTH_USERS_JSON | Preferred. JSON object mapping username to Argon2 hash, e.g. {"editor":"$argon2id$...","alice":"$argon2id$..."} |
| AUTH_SESSION_SECRET | Secret used to sign session cookies |
| AUTH_SESSION_TTL_SECONDS | Session lifetime in seconds (default 28800) |
| AUTH_COOKIE_NAME | Cookie name (default rule_editor_session) |
| AUTH_COOKIE_SECURE | Set true in HTTPS production environments |
| AUTH_ALLOWED_ORIGINS | Comma-separated CORS origins for GUI, e.g. http://localhost:5173,http://localhost:8888 |
eFLINT translation backend environment variables:
| Variable | Explanation |
|---|---|
| AUTH_SESSION_SECRET | Must match auth-service secret to verify signed cookies |
| AUTH_COOKIE_NAME | Must match auth-service cookie name |
| AUTH_ALLOWED_ORIGINS | Comma-separated CORS origins for GUI, e.g. http://localhost:5173,http://localhost:8888 |
Frontend environment variables (in gui/.env):
| Variable | Explanation |
|---|---|
| VITE_AUTH_ENABLED | Enable login gate (true / false, default true) |
| VITE_AUTH_API_BASE_URL | Base URL for auth endpoints (optional; empty means same origin) |
| VITE_EFLINT_API_BASE_URL | Base URL for /generate-eflint (optional; set this when eFLINT API is on another origin) |
| VITE_EFLINT_EXECUTE_URL | Base URL for reasoner endpoints (/execute, /repl/*) (optional; empty means same origin) |
| VITE_EFLINT_SERVER_BASE_URL | Base URL for eflint_server session endpoints (/sessions, /reset, /spec/register, /statements, /query/holds). Set explicitly for production/static hosting. |
| VITE_MONGO_API_BASE_URL | Base URL for Mongo intermediate API (optional; falls back to Netlify functions when empty) |
Generate an Argon2 hash locally:
python -c "from argon2 import PasswordHasher; print(PasswordHasher().hash('your-password'))"Streamlined user creation (recommended):
cd auth-service
pip install -r requirements.txt
# cp secrets.env.example secrets.env
python scripts/generate_auth_user.py --username editor --env-file .env.stack
python scripts/generate_auth_user.py --username alice --env-file .env.stackThen start auth service, backend, and frontend (example local development):
# auth-service
cd auth-service
uvicorn app:app --reload --host 0.0.0.0 --port 8101
# eFLINT backend
cd flint-to-eflint
pip install -r requirements.txt
uvicorn service.app:app --reload --host 0.0.0.0 --port 8000
# frontend
cd gui
npm install
npm run devThis updates AUTH_USERS_JSON in auth-service/secrets.env and lets you add users incrementally without manually editing Argon2 hashes.
MongoDB saves are now scoped per user.
- Every saved export stores a canonical
owner_username. - Save versioning is computed per
(owner_username, project_id). - Retrieval endpoints (projects list, versions list, task retrieval) are filtered by owner, so users only see their own saved artifacts.
- For backward compatibility, legacy records without
owner_usernameare matched usingmetadata.owner.
If you apply Mongo schema changes on an existing database, re-run the init/migration script from mongodb/init/01-init-rule-editor.js and backfill legacy documents where needed.
We welcome contributions of all kinds! Whether you're fixing a bug, adding features, improving documentation, or just suggesting an idea, we’re happy to collaborate.
- Fork this repository.
- Clone your fork (git clone https://gitlab.com/your-username/ui/interpretation-editor.git)
- Create a branch for your changes:
git checkout -b my-feature-or-bugfix- Install dependencies
npm install- Make your changes (and add tests if possible).
- Commit your changes:
git commit -am 'Add some feature'- Push to your forked repository:
git push origin my-feature-or-bugfix- Open a Merge Request (GitLab guide)
This project is licensed under the Apache License 2.0.
See the LICENSE file for the details.
