One binary: the lint and the trainer are commands of portal
Test / test (pull_request) Successful in 1m7s

portal serve runs a site; portal lint is what iris was; portal plan,
build, report and the rest are what portal-trainer was. One source at
one version, so the three never disagree about what content means.
Both histories are merged in (subtree). The release attaches the bare
binary for a content repo's CI. The trainer's plans, intakes, engines
and reference prompt come along; its runs stay in the old repo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-30 15:29:30 +02:00
co-authored by Claude Fable 5.1
parent d0d1cbf23d
commit 96b3746643
1733 changed files with 183 additions and 150652 deletions
+487
View File
@@ -0,0 +1,487 @@
# A building authority, written by hand to find where the plan language
# stops. 14 needs, 7 roles, 12 buckets, two needs sharing a bucket in
# each direction (public and internal), a decision no case officer may
# sign, a group outside the building that moves records and nothing
# else, a stage a deadline moves rather than a person, and four things
# the plan language cannot say at all - each named in the run's README
# rather than smuggled past.
business: Vestmark kommune - Building and Planning
language: en
contact: { name: Solveig Hauge, email: byggesak@vestmark.example }
mail: { from: "Vestmark Building and Planning <byggesak@vestmark.example>", reply_to: byggesak@vestmark.example }
about: >
The building authority for twenty-one thousand people. Anything built,
demolished, divided or changed in use is decided here: the drawings are
checked against the zoning map, the neighbours and the other
authorities have their say, and then the head of section signs. The
same office inspects what is actually built, signs off finished work,
and deals with building nobody applied for.
# Two features, two roles, no button. The authority exists for the
# person who wants to build; the next one it serves is the neighbour
# who finds out what is planned next door. Both have a move of their
# own after sending: the applicant withdraws, the objector withdraws,
# and both buckets are open to read.
does:
- id: we-decide-what-may-be-built
for: small-works-application
text: >
Everything built in Vestmark is decided here. You send the drawings,
the neighbours and the road, fire and heritage authorities are heard,
and you get a written decision you can hold us to.
- id: your-neighbours-case-is-readable
for: objection
text: >
If a notice landed in your postbox, the case behind it is open to
read here, and what you write to us has to be answered by name in
the decision.
doors:
- id: want-to-build
who: an owner or a firm with something to build, divide or change
- id: building-near-me
who: someone who lives next to what is being built
groups:
- { id: front_desk, label: Front desk }
- { id: case_officers, label: Case officers }
- { id: section_head, label: Head of section }
- { id: inspectors, label: Inspectors }
- { id: appeals_board, label: Appeals board, invited_by: [section_head] }
- { id: agencies, label: Hearing bodies, invited_by: [case_officers, section_head] }
- { id: approved_firms, label: Approved firms, invited_by: [section_head] }
invites_by: [section_head, case_officers]
needs:
# --- the public: something they want to build ----------------------
- id: pre-application-meeting
door: want-to-build
who: someone with an idea for a building and no drawings yet
wants: to hear what we will allow before paying an architect
lands_in: meetings
handled_by: case_officers
stages: [open, scheduled, met, advised, cancelled]
done_when: [advised, cancelled]
success: [advised]
# They can call it off themselves; the front desk does it when they ring.
moves_by: { cancelled: [submitter, front_desk] }
watched_by: [front_desk]
mails: { open: submitter, scheduled: submitter, advised: submitter }
stages_text: { met: we have met, advised: written advice sent }
max_fields: 5
collects:
- { name: name }
- { name: email, type: email }
- { name: property_address }
- { name: what_you_want, type: textarea }
- { name: phone, type: tel, optional: true }
- { name: site, type: location, optional: true }
- id: small-works-application
door: want-to-build
who: an owner applying for something small enough to file themselves
wants: permission to build it, and to know where the case stands
lands_in: applications
handled_by: case_officers
stages: [open, incomplete, complete, on_hearing, recommended, approved, approved_with_conditions, refused, withdrawn]
done_when: [approved, approved_with_conditions, refused, withdrawn]
success: [approved, approved_with_conditions]
skippable: [incomplete]
# A case is sent back for the drawing it was missing, and the
# hearing turns things up that send it back again. It may be
# withdrawn from anywhere, and refused only once we have seen
# everything: not on the day it arrives.
reached_from:
incomplete: [open, complete, on_hearing]
refused: [complete, on_hearing, recommended]
# No case officer signs their own case: only the head of section
# moves one into a decision. The applicant answers a request for
# missing papers, and may withdraw right up to the signature.
moves_by:
complete: [submitter, case_officers]
approved: [section_head]
approved_with_conditions: [section_head]
refused: [section_head]
withdrawn: [submitter]
watched_by: [public, front_desk, inspectors, section_head]
mails:
open: submitter
incomplete: submitter
on_hearing: submitter
approved: submitter
approved_with_conditions: submitter
refused: submitter
stages_text:
incomplete: papers missing
complete: papers complete
on_hearing: out to the neighbours and the agencies
recommended: recommendation written
approved_with_conditions: approved on conditions
max_fields: 6
collects:
- { name: email, type: email }
- { name: property_address }
- { name: property_number }
- { name: what_is_planned, type: textarea }
- { name: neighbours_notified, type: select, options: ["Yes", Not yet] }
- { name: site, type: location, optional: true }
- { name: drawings_url, optional: true }
- id: exemption-request
door: want-to-build
who: someone whose plans break a rule in the zoning map
wants: to be allowed it anyway, with reasons on the record
requires: An application already with us, and the case number from its receipt.
lands_in: exemptions
handled_by: case_officers
stages: [open, county_asked, county_answered, recommended, granted, denied, withdrawn]
done_when: [granted, denied, withdrawn]
success: [granted]
# The county answers from outside the building; nothing may be
# granted before it has. The head of section signs, as for a permit.
moves_by:
county_answered: [agencies]
granted: [section_head]
denied: [section_head]
withdrawn: [submitter]
watched_by: [public, section_head]
mails: { open: submitter, county_asked: submitter, granted: submitter, denied: submitter }
stages_text: { county_asked: with the county, county_answered: the county has answered }
max_fields: 4
collects:
- { name: email, type: email }
- { name: case_number }
- { name: which_rule }
- { name: why_it_should_be_allowed, type: textarea }
- { name: site, type: location, optional: true }
- id: completion-request
door: want-to-build
who: an owner or firm whose building work is finished
wants: the certificate that lets the building be used
requires: A case number, and the work finished or close enough to move in.
lands_in: completions
handled_by: case_officers
stages: [open, checked, temporary_permission, certificate_issued, refused]
done_when: [certificate_issued, refused]
success: [certificate_issued]
# Most go straight through; the ones who want to move in before the
# last of it is done detour past a temporary permission.
skippable: [temporary_permission]
moves_by:
temporary_permission: [section_head]
certificate_issued: [section_head]
refused: [section_head]
watched_by: [public, inspectors]
turned_down_from: [open, checked]
mails: { open: submitter, temporary_permission: submitter, certificate_issued: submitter, refused: submitter }
stages_text: { checked: checked against the inspections, temporary_permission: temporary permission to use, certificate_issued: certificate issued }
max_fields: 5
collects:
- { name: email, type: email }
- { name: case_number }
- { name: property_address }
- { name: work_finished, type: date }
- { name: asking_for, type: select, options: [Completion certificate, Temporary permission to move in] }
- { name: what_is_outstanding, type: textarea, optional: true }
# --- the public: someone else is building ---------------------------
- id: objection
door: building-near-me
who: a neighbour who got a notice about what is planned next door
wants: what they think on the record, and answered in the decision
requires: The notice that came in the post, which has the case number on it.
lands_in: objections
handled_by: case_officers
stages: [open, registered, answered_by_applicant, addressed, withdrawn]
done_when: [addressed, withdrawn]
success: [addressed]
# The firm that filed the case answers the objection; the neighbour
# can take it back if they are satisfied.
moves_by: { answered_by_applicant: [approved_firms, case_officers], withdrawn: [submitter] }
watched_by: [public, section_head]
mails: { open: submitter, answered_by_applicant: submitter, addressed: submitter }
stages_text: { registered: on the case file, answered_by_applicant: the applicant has answered, addressed: answered in the decision }
max_fields: 4
collects:
- { name: email, type: email }
- { name: case_number }
- { name: your_property }
- { name: your_objection, type: textarea }
- id: case-status-enquiry
door: building-near-me
who: someone waiting on a case and getting nothing back
wants: to be told where it actually stands
lands_in: enquiries
handled_by: front_desk
stages: [open, in_hand, answered]
done_when: [answered]
success: [answered]
moves_by: { in_hand: [front_desk, case_officers] }
watched_by: [case_officers]
mails: { open: submitter, answered: submitter }
max_fields: 3
collects:
- { name: email, type: email }
- { name: case_number_or_address }
- { name: what_you_want_to_know, type: textarea }
- id: illegal-building-report
door: building-near-me
who: someone who can see building going up that nobody was told about
wants: us to go and look, and to hear that we did
lands_in: illegal_building
handled_by: inspectors
stages: [open, inspected, owner_asked, explained, resolved, fined, dropped]
done_when: [resolved, fined, dropped]
success: [resolved, dropped]
# An inspector goes and looks; the letter to the owner is a case
# officer's, and so is the fine.
moves_by: { owner_asked: [case_officers], explained: [case_officers], fined: [section_head] }
watched_by: [case_officers, section_head]
turned_down_from: [open, inspected]
mails: { open: submitter, inspected: submitter, resolved: submitter, dropped: submitter }
stages_text: { owner_asked: the owner has been written to, explained: the owner has answered, dropped: nothing to pursue }
max_fields: 3
collects:
- { name: email, type: email }
- { name: address }
- { name: what_you_saw, type: textarea }
- { name: where, type: location, optional: true }
# --- the public: after a decision ------------------------------------
- id: appeal
who: anyone with an interest who will not accept a decision of ours
wants: the decision looked at again, by us and then by the board
requires: A decision from us less than three weeks old, and its case number.
lands_in: appeals
handled_by: case_officers
stages: [open, reconsidered, changed_by_us, before_the_board, upheld, overturned, withdrawn]
done_when: [changed_by_us, upheld, overturned, withdrawn]
success: [changed_by_us, upheld]
# If we change our own mind that is the end of it, and it happens
# while we are looking - not after the board has the case. The
# board can only decide what has reached it.
reached_from:
changed_by_us: [reconsidered]
upheld: [before_the_board]
overturned: [before_the_board]
# We look at ourselves first and the head of section decides whether
# we were wrong. What we will not change goes to seven elected
# people who meet once a month and move nothing else in this office.
moves_by:
changed_by_us: [section_head]
before_the_board: [section_head]
upheld: [appeals_board]
overturned: [appeals_board]
withdrawn: [submitter]
watched_by: [public, front_desk, section_head]
mails:
open: submitter
reconsidered: submitter
changed_by_us: submitter
before_the_board: submitter
upheld: submitter
overturned: submitter
stages_text:
reconsidered: we are looking at it again
changed_by_us: we changed our own decision
before_the_board: with the appeals board
upheld: the board kept our decision
overturned: the board overturned us
max_fields: 5
collects:
- { name: email, type: email }
- { name: case_number }
- { name: property }
- { name: what_you_disagree_with, type: textarea }
- { name: your_interest, type: select, options: [I applied, I am a neighbour, Other] }
# --- the public: a firm that wants to file here -----------------------
- id: firm-approval
who: an architect or engineering firm that files for its clients
wants: to be on the list, and then to file without asking again
lands_in: firms
handled_by: case_officers
stages: [open, papers_missing, checked, approved, declined]
done_when: [approved, declined]
success: [approved]
skippable: [papers_missing]
moves_by: { checked: [submitter, case_officers], approved: [section_head], declined: [section_head] }
watched_by: [section_head, front_desk]
turned_down_from: [open, papers_missing, checked]
mails: { open: submitter, papers_missing: submitter, approved: submitter, declined: submitter }
stages_text: { papers_missing: waiting on papers, checked: papers checked }
max_fields: 6
collects:
- { name: firm_name }
- { name: org_number }
- { name: email, type: email }
- { name: qualified_for, type: select, multiple: true, options: [Design, Execution, Independent control, Site management] }
- { name: insurance_expires, type: date }
- { name: insurance_document, optional: true }
# --- approved firms ----------------------------------------------------
- id: permit-application
who: an approved firm filing on behalf of an owner
wants: a decision inside twelve weeks, and to know when the clock stops
enters_as: approved_firms
lands_in: applications
handled_by: case_officers
stages: [open, incomplete, complete, on_hearing, recommended, approved, approved_with_conditions, refused, withdrawn]
done_when: [approved, approved_with_conditions, refused, withdrawn]
success: [approved, approved_with_conditions]
skippable: [incomplete]
# A case is sent back for the drawing it was missing, and the
# hearing turns things up that send it back again. It may be
# withdrawn from anywhere, and refused only once we have seen
# everything: not on the day it arrives.
reached_from:
incomplete: [open, complete, on_hearing]
refused: [complete, on_hearing, recommended]
moves_by:
complete: [submitter, case_officers]
approved: [section_head]
approved_with_conditions: [section_head]
refused: [section_head]
withdrawn: [submitter]
watched_by: [public, front_desk, inspectors, section_head]
mails:
open: submitter
incomplete: submitter
on_hearing: submitter
approved: submitter
approved_with_conditions: submitter
refused: submitter
stages_text:
incomplete: papers missing
complete: papers complete
on_hearing: out to the neighbours and the agencies
recommended: recommendation written
approved_with_conditions: approved on conditions
max_fields: 6
collects:
- { name: email, type: email }
- { name: property_address }
- { name: property_number }
- { name: what_is_planned, type: textarea }
- { name: neighbours_notified, type: select, options: ["Yes", Not yet] }
- { name: site, type: location, optional: true }
- { name: drawings_url, optional: true }
# --- the front desk -----------------------------------------------------
- id: log-counter-enquiry
who: whoever is on the counter with a queue and a caller on the line
wants: the question written down and sent to whoever has the case
enters_as: front_desk
lands_in: enquiries
handled_by: front_desk
stages: [open, in_hand, answered]
done_when: [answered]
success: [answered]
moves_by: { in_hand: [front_desk, case_officers] }
watched_by: [case_officers]
mails: { open: submitter, answered: submitter }
max_fields: 3
collects:
- { name: email, type: email }
- { name: case_number_or_address }
- { name: what_you_want_to_know, type: textarea }
# --- case officers ------------------------------------------------------
- id: request-hearing
who: a case officer sending a case out to the authority that must see it
wants: an answer by the deadline, or a record that none came
enters_as: case_officers
lands_in: hearings
handled_by: case_officers
stages: [open, answered, no_answer, cancelled]
done_when: [answered, no_answer, cancelled]
success: [answered]
# This is how an agency sees what it has been asked. Nobody presses
# a button for the deadline passing.
moves_by: { answered: [agencies] }
watched_by: [agencies, section_head, front_desk]
attended_by: >
the six-week deadline: when it passes with nothing from the agency
the case goes to no_answer and the application goes on without one
stages_text: { no_answer: the deadline passed with no answer }
max_fields: 4
collects:
- { name: case_number }
- { name: which_authority, type: select, options: [Road authority, Fire service, Heritage office, The county] }
- { name: what_we_ask, type: textarea }
- { name: answer_by, type: date }
- { name: site, type: location, optional: true }
# --- the authorities outside the building --------------------------------
- id: hearing-answer
who: a road, fire, heritage or county officer who has been asked about a case
wants: their answer on the file, in their own words
requires: The hearing letter we sent you, which has the case number on it.
enters_as: agencies
lands_in: hearing_answers
handled_by: case_officers
stages: [open, read, followed, set_aside]
done_when: [followed, set_aside]
success: [followed]
watched_by: [section_head, front_desk]
stages_text: { read: read by the case officer, followed: followed in the decision, set_aside: set aside with reasons }
max_fields: 4
collects:
- { name: case_number }
- { name: which_authority, type: select, options: [Road authority, Fire service, Heritage office, The county] }
- { name: our_answer, type: select, options: [No objection, Objection, Conditions required] }
- { name: what_we_say, type: textarea }
# --- inspectors -----------------------------------------------------------
- id: inspection-finding
who: an inspector on a scaffold with gloves on and the wind blowing
wants: what they found logged before they climb down
enters_as: inspectors
lands_in: inspections
handled_by: inspectors
stages: [open, as_approved, deviation, put_right, escalated]
done_when: [as_approved, put_right, escalated]
success: [as_approved, put_right]
# Anything that has to become a fine or a refusal leaves the
# inspectors and becomes a case officer's problem.
moves_by: { escalated: [case_officers, section_head] }
watched_by: [case_officers, section_head]
stages_text: { as_approved: built as approved, deviation: built differently, put_right: put right, escalated: handed to a case officer }
max_fields: 3
collects:
- { name: case_number }
- { name: site_address }
- { name: what_i_found, type: textarea }
- { name: where, type: location, optional: true }
# Held out from every model that writes words.
# arriving with one real thing, in their own register: a homeowner who
# has never read a regulation, a firm that files for a living, an
# inspector on a roof in the wind, a politician reading a bundle.
personas:
- { id: garage-owner, need: pre-application-meeting, text: "We want to put a double garage where the old shed is. Before I pay an architect I'd like to know whether you'll even allow it, because the neighbour says it's too close to the road." }
- { id: shop-to-flats, need: pre-application-meeting, text: "I own the old grocery on Torget and want to make four flats out of it. There are a lot of questions - parking, fire, the facade is listed. Can we sit down with someone?" }
- { id: firm-files-extension, need: permit-application, text: "We are the responsible applicant for an extension at Bjorkeveien 14. Drawings, site plan and the neighbour notices are all ready. We file maybe forty of these a year." }
- { id: owner-small-window, need: permit-application, text: "I want to put a dormer window in my roof. The man at the counter said I have to apply even though it's small." }
- { id: missing-drawings, need: permit-application, text: "You wrote that our section drawing is missing and the case is on hold. I've got it here now, where do I send it so the clock starts again?" }
- { id: too-close-to-boundary, need: exemption-request, text: "The garage ends up one and a half metres from the boundary and the plan says four. We are asking you to allow it anyway - the ground falls away steeply everywhere else on the plot." }
- { id: neighbour-view, need: objection, text: "I got a notice about the extension at number 14. It takes the afternoon sun off my entire terrace and I want that on the record before you decide anything." }
- { id: neighbour-parking, need: objection, text: "The flats they want to build in the old grocery have four parking places for four flats and everyone here already parks on the street. I object." }
- { id: road-authority, need: hearing-answer, text: "Regarding your case 2026/418: the access onto the county road is within the sight line and we cannot accept it as drawn. Our formal reply is attached." }
- { id: fire-service, need: hearing-answer, text: "We have looked at the conversion on Torget. We have no objection provided the stairwell is upgraded. Reference your case 2026/501." }
- { id: refused-applicant, need: appeal, text: "You refused our exemption on the ninth. We don't accept the reasoning - you've allowed the same thing twice in the same street. We're appealing." }
- { id: neighbour-appeals, need: appeal, text: "You approved the extension next door even though I objected. I'm appealing the decision. What do I need to send?" }
- { id: board-member, need: appeal, text: "I sit on the appeals board. I need the case, our own decision, and what the neighbours wrote, before Tuesday." }
- { id: inspector-on-roof, need: inspection-finding, text: "I'm up on the scaffold at Bjorkeveien. The fire wall isn't built to the drawing at all. I've got gloves on and it's blowing. I want this logged before I climb down." }
- { id: inspector-all-good, need: inspection-finding, text: "Been out at Torget. Everything is as approved. Nothing to report but I have to record that I was there." }
- { id: firm-wants-certificate, need: completion-request, text: "The work at Bjorkeveien 14 is finished and inspected. We're asking for the completion certificate so the owners can get their mortgage released." }
- { id: wants-to-move-in, need: completion-request, text: "The house is done apart from the outside steps, which we can't pour until spring. The family has sold their flat. Can we get a temporary permission to move in?" }
- { id: something-behind-fence, need: illegal-building-report, text: "There's a large structure going up behind the fence at Solbakken 7. Nobody has had a notice about it and it's been going on for two weeks." }
- { id: owner-explains, need: illegal-building-report, text: "I got your letter about the terrace. I genuinely did not know it needed an application, it's been there since we moved in. What happens now?" }
- { id: new-firm, need: firm-approval, text: "We are a three-person architecture firm that has just started. We want to be approved so we can file applications for our clients in your municipality." }
- { id: firm-insurance-lapsed, need: firm-approval, text: "You wrote that our liability insurance certificate has expired. We have renewed it, here it is." }
- { id: counter-phone-call, need: log-counter-enquiry, text: "A woman rang about the case behind her house. Wants to know where it stands. I'm at the counter and there's a queue - I need to write it down and send it to whoever has that case." }
- { id: where-is-my-case, need: case-status-enquiry, text: "I sent in my application five weeks ago and I've heard nothing. Is anyone looking at it? Is it in the six weeks with the neighbours or is it waiting on me?" }
+294
View File
@@ -0,0 +1,294 @@
# A contractor's whole operation, to stress the builder and the
# harness: 13 needs, 6 roles, 9 buckets, needs sharing a bucket,
# skippable stages, and a front door that had to be tiered.
business: Nordvik Bygg
language: en
contact: { name: Ingrid Nordvik, email: ingrid@nordvikbygg.example }
about: >
Twelve people who survey old buildings, draw up rehabilitation plans,
handle the permit application, and do the building work with their
own carpenters plus subcontractors for electrical, plumbing and
scaffolding. Project managers win and run the jobs, site managers run
each building site day to day, and one safety officer owns everything
reported from site.
# Six public needs is one over what a front door carries, so the
# public comes through three doors: a building of theirs, work with
# us, and a site of ours next to them (that one is a need on its own).
doors:
- id: your-building
who: an owner, a housing board or a past customer with a building
- id: work-with-us
who: a trade firm, a carpenter or an apprentice who wants work from us
needs:
# --- the public ---------------------------------------------------
- id: survey-enquiry
door: your-building
who: a housing board or private owner with a problem in a building
wants: someone qualified to come and look at it and say what it takes
lands_in: enquiries
handled_by: project_managers
stages: [open, surveyed, quoted, won, lost]
done_when: [won, lost]
success: [won]
# Whoever went to the building marks it surveyed; site managers
# see what is coming their way.
moves_by: { surveyed: [project_managers, site_managers] }
watched_by: [site_managers]
max_fields: 4
collects:
- { name: name }
- { name: phone, type: tel }
- { name: address }
- { name: problem, type: textarea }
- { name: access, optional: true }
- id: build-enquiry
door: your-building
who: an owner who already knows what they want built or changed
wants: drawings, a permit application and a price
lands_in: enquiries
handled_by: project_managers
stages: [open, surveyed, quoted, won, lost]
done_when: [won, lost]
success: [won]
# Whoever went to the building marks it surveyed; site managers
# see what is coming their way.
moves_by: { surveyed: [project_managers, site_managers] }
watched_by: [site_managers]
max_fields: 5
collects:
- { name: name }
- { name: phone, type: tel }
- { name: address }
- { name: project, type: textarea }
- { name: timeframe, type: select, options: [This year, Next year, No fixed date] }
- id: warranty-claim
door: your-building
who: a past customer who has found a fault in work we handed over
wants: us to come back, look at it, and put it right
lands_in: claims
handled_by: project_managers
stages: [open, inspected, accepted, fixed, rejected]
done_when: [fixed, rejected]
success: [fixed]
# The site manager inspects and puts it right; whether it is ours
# to fix is the project manager's call.
moves_by: { inspected: [site_managers], fixed: [site_managers] }
max_fields: 4
collects:
- { name: name }
- { name: phone, type: tel }
- { name: address }
- { name: fault, type: textarea }
- { name: handed_over, type: date, optional: true }
- id: neighbour-complaint
who: someone living or working next to one of our building sites
wants: the noise, dust or blocked access dealt with, and an answer
lands_in: complaints
handled_by: site_managers
stages: [open, answered]
done_when: [answered]
success: [answered]
watched_by: [project_managers]
max_fields: 3
collects:
- { name: site_address }
- { name: what_is_wrong, type: textarea }
- { name: reach_me_on }
- id: subcontractor-application
door: work-with-us
who: a trade firm doing electrical, plumbing or scaffolding work
wants: to be approved once, and then be called when a job needs them
lands_in: subcontractors
handled_by: project_managers
stages: [open, awaiting_papers, approved, declined]
done_when: [approved, declined]
success: [approved]
watched_by: [site_managers]
skippable: [awaiting_papers]
max_fields: 5
collects:
- { name: company }
- { name: registration_number }
- { name: trade, type: select, options: [Electrical, Plumbing, Scaffolding, Sheet metal, Other] }
- { name: contact_email, type: email }
- { name: insurance_confirmed, type: select, options: ["Yes", Not yet] }
- { name: trade_approval, optional: true }
- id: job-application
door: work-with-us
who: a carpenter, bricklayer or apprentice looking for work
wants: to be considered, without writing a letter
lands_in: applicants
handled_by: project_managers
stages: [open, interviewing, offered, hired, declined]
done_when: [hired, declined]
success: [hired]
max_fields: 3
collects:
- { name: name }
- { name: phone, type: tel }
- { name: trade, type: select, options: [Carpenter, Bricklayer, Apprentice, Other] }
- { name: experience, type: textarea, optional: true }
# --- clients with a job under way ---------------------------------
- id: change-request
who: a client whose job is under way and who wants something changed
wants: a price for the change, and the change done if they accept it
enters_as: clients
lands_in: change_orders
handled_by: project_managers
stages: [open, priced, approved, completed, declined]
done_when: [completed, declined]
success: [completed]
# The project manager prices it, the client says yes or no through
# their own link, the site manager does the work.
moves_by: { approved: [submitter], declined: [submitter, project_managers], completed: [site_managers] }
watched_by: [site_managers]
max_fields: 3
collects:
- { name: email, type: email }
- { name: job_address }
- { name: change, type: textarea }
- { name: needed_by, type: date, optional: true }
# --- project managers ---------------------------------------------
- id: log-phone-enquiry
who: a project manager who just took a call or a text about a building
wants: the enquiry written down before it gets lost
enters_as: project_managers
lands_in: enquiries
handled_by: project_managers
stages: [open, surveyed, quoted, won, lost]
done_when: [won, lost]
success: [won]
# Whoever went to the building marks it surveyed; site managers
# see what is coming their way.
moves_by: { surveyed: [project_managers, site_managers] }
watched_by: [site_managers]
max_fields: 4
collects:
- { name: caller }
- { name: phone, type: tel }
- { name: address }
- { name: problem, type: textarea }
- id: post-job-request
who: a project manager with work to hand to a subcontractor
wants: approved subcontractors to see the job and answer with a price
enters_as: project_managers
lands_in: job_requests
handled_by: project_managers
stages: [open, awarded, withdrawn]
done_when: [awarded, withdrawn]
success: [awarded]
# This is how subcontractors see what there is to bid on.
watched_by: [subcontractors, site_managers]
max_fields: 4
collects:
- { name: site_address }
- { name: trade, type: select, options: [Electrical, Plumbing, Scaffolding, Sheet metal, Other] }
- { name: work, type: textarea }
- { name: start, type: date }
# --- approved subcontractors --------------------------------------
- id: submit-bid
who: an approved subcontractor who has seen a job they can take
wants: to say yes with a price and a start date
enters_as: subcontractors
lands_in: bids
handled_by: project_managers
stages: [open, shortlisted, accepted, declined]
done_when: [accepted, declined]
success: [accepted]
watched_by: [site_managers]
skippable: [shortlisted]
max_fields: 3
collects:
- { name: job }
- { name: price }
- { name: can_start, type: date }
# --- on site --------------------------------------------------------
- id: report-site-issue
who: anyone on site, ours or a subcontractor's, on a phone with gloves on
wants: to report a hazard, a near miss or wrong materials before they forget
enters_as: crew
lands_in: site_issues
handled_by: safety_officer
stages: [open, in_hand, closed]
done_when: [closed]
success: [closed]
# A site manager can take an issue in hand on the spot; only the
# safety officer closes it.
moves_by: { in_hand: [safety_officer, site_managers] }
watched_by: [project_managers]
max_fields: 3
collects:
- { name: kind, type: select, options: [Hazard, Near miss, Wrong materials, Other] }
- { name: where }
- { name: what_happened, type: textarea }
- id: order-materials
who: a carpenter or bricklayer who is about to run out of something
wants: it ordered and on site before work stops
enters_as: crew
lands_in: material_orders
handled_by: site_managers
stages: [open, ordered, delivered, refused]
done_when: [delivered, refused]
success: [delivered]
# The crew confirms it arrived.
moves_by: { delivered: [crew, site_managers] }
max_fields: 3
collects:
- { name: site_address }
- { name: what, type: textarea }
- { name: needed_by, type: date }
- id: safety-round-finding
who: the safety officer walking a site on the weekly round
wants: each thing found written down where the reported issues already are
enters_as: safety_officer
lands_in: site_issues
handled_by: safety_officer
stages: [open, in_hand, closed]
done_when: [closed]
success: [closed]
# A site manager can take an issue in hand on the spot; only the
# safety officer closes it.
moves_by: { in_hand: [safety_officer, site_managers] }
watched_by: [project_managers]
max_fields: 3
collects:
- { name: kind, type: select, options: [Hazard, Near miss, Wrong materials, Other] }
- { name: where }
- { name: what_happened, type: textarea }
# No model that writes words ever sees these.
personas:
- { id: housing-board-damp, need: survey-enquiry, text: "I chair the board of a housing cooperative from the seventies. Two ground-floor flats have damp in the walls and we don't know if it's drainage or the facade. We need someone to tell us." }
- { id: buyer-before-purchase, need: survey-enquiry, text: "We are about to buy an old timber house. Before we sign we want a professional to go through it and tell us what is wrong with it." }
- { id: attic-conversion, need: build-enquiry, text: "We want to turn our attic into two bedrooms and a bathroom. We need drawings and someone who handles the application to the municipality." }
- { id: shop-to-flat, need: build-enquiry, text: "I own a former shop on the ground floor and want to make it a flat. I have no idea what is allowed. Can you sort that out and give me a price?" }
- { id: leaking-bathroom, need: warranty-claim, text: "You redid our bathroom two years ago. Now water is coming through the ceiling of the flat below. I want you to come and fix it." }
- { id: cracked-render, need: warranty-claim, text: "The render you put on our gable wall last summer has cracked right across. That can't be right after one winter." }
- { id: noisy-mornings, need: neighbour-complaint, text: "I live next door to the block you are working on. You start drilling at half past six every morning and I have a baby. Who do I talk to?" }
- { id: blocked-driveway, need: neighbour-complaint, text: "Your skip and a van have been parked across the entrance to our shop's loading bay for three days. We can't get deliveries." }
- { id: electrician-firm, need: subcontractor-application, text: "We are a four-person electrical firm, fully certified. We would like to get on your list so you call us when you need electricians." }
- { id: scaffolder, need: subcontractor-application, text: "I run a scaffolding company. How do I get to do jobs for you?" }
- { id: carpenter-jobseeker, need: job-application, text: "I'm a certified carpenter with eight years on rehabilitation jobs. Are you hiring?" }
- { id: apprentice, need: job-application, text: "I finish vocational school this spring and need an apprenticeship as a bricklayer." }
- { id: extra-window, need: change-request, text: "You are in the middle of our attic conversion. We have decided we want a roof window over the stairs as well. What would that cost?" }
- { id: pm-took-a-call, need: log-phone-enquiry, text: "A woman from a housing board in Storgata just rang about water in their basement. I'm in the van. I need to note it before I forget her number." }
- { id: pm-needs-plumber, need: post-job-request, text: "The bathroom job in Kirkeveien needs a plumber for two weeks from the first of next month. I want prices from the ones we have approved." }
- { id: plumber-bidding, need: submit-bid, text: "We are an approved plumbing firm. I saw you need someone in Kirkeveien next month. We can do it, I have a price." }
- { id: loose-railing, need: report-site-issue, text: "The railing on the third level of the scaffold on the north side is loose. Nobody got hurt but someone will." }
- { id: wrong-membrane, need: report-site-issue, text: "The bathroom membrane delivered today is not the one in the specification. We stopped work on it." }
- { id: out-of-battens, need: order-materials, text: "We have enough roof battens for today and maybe half of tomorrow. Need another two hundred metres on site by Thursday." }
- { id: weekly-round, need: safety-round-finding, text: "On my Friday round I found the first-aid box on level two empty and an extension lead run through a puddle." }
+122
View File
@@ -0,0 +1,122 @@
# The contractor from intakes/contractor.yaml as a plan: the needs spec
# plus what each form collects. Everything structural in the generated
# site comes from this file; a model only writes the words.
business: Nordvik Bygg
language: en
contact: { name: Ingrid Nordvik, email: ingrid@nordvikbygg.example }
about: >
Twelve people who survey old buildings, draw up rehabilitation plans,
handle the permit application, and do the building work with their
own carpenters plus subcontractors for electrical, plumbing and
scaffolding.
needs:
- id: survey-enquiry
who: a housing board or private owner with a problem in a building
wants: someone qualified to come and look at it and say what it takes
lands_in: enquiries
handled_by: project_managers
stages: [open, surveyed, quoted, won, lost]
done_when: [won, lost]
success: [won]
max_fields: 4
collects:
- { name: name }
- { name: phone, type: tel }
- { name: address }
- { name: problem, type: textarea }
- { name: access, optional: true }
- id: build-enquiry
who: an owner who already knows what they want built or changed
wants: drawings, a permit application and a price
lands_in: enquiries
handled_by: project_managers
stages: [open, surveyed, quoted, won, lost]
done_when: [won, lost]
success: [won]
max_fields: 5
collects:
- { name: name }
- { name: phone, type: tel }
- { name: address }
- { name: project, type: textarea }
- { name: timeframe, type: select, options: [This year, Next year, No fixed date] }
- id: log-phone-enquiry
who: a project manager who just took a call or a text about a building
wants: the enquiry written down before it gets lost
enters_as: project_managers
lands_in: enquiries
handled_by: project_managers
stages: [open, surveyed, quoted, won, lost]
done_when: [won, lost]
success: [won]
max_fields: 4
collects:
- { name: caller }
- { name: phone, type: tel }
- { name: address }
- { name: problem, type: textarea }
- id: subcontractor-application
who: a trade firm doing electrical, plumbing or scaffolding work
wants: to be approved once, and then be called when a job needs them
lands_in: subcontractors
handled_by: project_managers
stages: [open, awaiting_papers, approved, declined]
done_when: [approved, declined]
success: [approved]
skippable: [awaiting_papers]
max_fields: 5
collects:
- { name: company }
- { name: registration_number }
- { name: trade, type: select, options: [Electrical, Plumbing, Scaffolding, Sheet metal, Other] }
- { name: contact_email, type: email }
- { name: insurance_confirmed, type: select, options: ["Yes", Not yet] }
- { name: trade_approval, optional: true }
- id: job-application
who: a carpenter, bricklayer or apprentice looking for work
wants: to be considered, without writing a letter
lands_in: applicants
handled_by: project_managers
stages: [open, interviewing, hired, declined]
done_when: [hired, declined]
success: [hired]
max_fields: 3
collects:
- { name: name }
- { name: phone, type: tel }
- { name: trade, type: select, options: [Carpenter, Bricklayer, Apprentice, Other] }
- { name: experience, type: textarea, optional: true }
- id: report-site-issue
who: anyone on site, ours or a subcontractor's, on a phone with gloves on
wants: to report a hazard, a near miss or wrong materials before they forget
enters_as: crew
lands_in: site_issues
handled_by: safety_officer
stages: [open, in_hand, closed]
done_when: [closed]
success: [closed]
max_fields: 3
collects:
- { name: kind, type: select, options: [Hazard, Near miss, Wrong materials, Other] }
- { name: where }
- { name: what_happened, type: textarea }
# Held out from every model that writes words: these are the personas
# from sectors/construction, which no generator has seen.
personas:
- { id: housing-board-damp, need: survey-enquiry, text: "I chair the board of a housing cooperative from the seventies. Two ground-floor flats have damp in the walls and we don't know if it's drainage or the facade. We need someone to tell us." }
- { id: buyer-before-purchase, need: survey-enquiry, text: "We are about to buy an old timber house. Before we sign we want a professional to go through it and tell us what is wrong with it." }
- { id: attic-conversion, need: build-enquiry, text: "We want to turn our attic into two bedrooms and a bathroom. We need drawings and someone who handles the application to the municipality." }
- { id: shop-to-flat, need: build-enquiry, text: "I own a former shop on the ground floor and want to make it a flat. I have no idea what is allowed. Can you sort that out and give me a price?" }
- { id: electrician-firm, need: subcontractor-application, text: "We are a four-person electrical firm, fully certified. We would like to get on your list so you call us when you need electricians." }
- { id: scaffolder, need: subcontractor-application, text: "I run a scaffolding company. How do I get to do jobs for you?" }
- { id: carpenter-jobseeker, need: job-application, text: "I'm a certified carpenter with eight years on rehabilitation jobs. Are you hiring?" }
- { id: apprentice, need: job-application, text: "I finish vocational school this spring and need an apprenticeship as a bricklayer." }
- { id: loose-railing, need: report-site-issue, text: "The railing on the third level of the scaffold on the north side is loose. Nobody got hurt but someone will." }
- { id: wrong-membrane, need: report-site-issue, text: "The bathroom membrane delivered today is not the one in the specification. We stopped work on it." }
+119
View File
@@ -0,0 +1,119 @@
# Fotballgruppa i et idrettslag, på norsk: to dører på forsiden
# (foresatte, og de som vil bidra), roller som overlapper på én
# innmelding (treneren godkjenner, kassereren ser betalingen), og en
# intern melding fra trenerne til styret.
business: Lia IL Fotball
language: no
contact: { name: Kari Lien, email: kari@liail.example }
mail: { from: "Liail IL <post@liail.example>", reply_to: kari@liail.example }
about: >
Fotballgruppa i et idrettslag på bygda. Rundt hundre og tjue unger
fra seks til seksten år, fordelt på lag etter årskull, drevet av
foreldre på fritida.
doors:
- id: foresatte
who: en forelder eller foresatt til et barn som vil spille, eller allerede spiller
- id: bidra
who: noen som vil hjelpe til i klubben, eller en bedrift som vil støtte den
needs:
- id: innmelding
who: en forelder som vil ha ungen med på et lag
wants: at barnet får plass på laget for sitt årskull
door: foresatte
lands_in: innmeldinger
handled_by: trenere
stages: [open, godkjent, betalt, trukket]
stages_text: { open: "mottatt", godkjent: "godkjent av treneren", betalt: "betalt", trukket: "trukket" }
mails: { open: submitter, godkjent: submitter, betalt: submitter }
done_when: [betalt, trukket]
success: [betalt]
# Treneren godkjenner plassen, kassereren ser at det er betalt.
moves_by: { betalt: [kasserer] }
watched_by: [kasserer]
max_fields: 5
collects:
- { name: barnets_navn }
- { name: fodselsar, type: select, options: ["2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", "2018", "2019", "2020"] }
- { name: foresatt }
- { name: telefon, type: tel }
- { name: email, type: email }
- { name: spilt_for, optional: true }
- id: avgiftssak
who: en forelder som sliter med treningsavgiften
wants: å dele den opp, eller slippe den
door: foresatte
lands_in: avgiftssaker
handled_by: styret
stages: [open, vurdert, innvilget, avslatt]
stages_text: { open: "mottatt", vurdert: "vurdert", innvilget: "innvilget", avslatt: "avslått" }
done_when: [innvilget, avslatt]
success: [innvilget]
max_fields: 3
collects:
- { name: barnets_navn }
- { name: foresatt }
- { name: hva_gjelder_det, type: textarea }
- { name: telefon, type: tel, optional: true }
- id: frivillig
who: en som vil hjelpe til, som trener, lagleder, sjåfør eller på dugnad
wants: å bli brukt der det trengs
door: bidra
lands_in: frivillige
handled_by: styret
stages: [open, kontaktet, plassert, avslatt]
stages_text: { open: "mottatt", kontaktet: "kontaktet", plassert: "satt opp", avslatt: "takket nei" }
done_when: [plassert, avslatt]
success: [plassert]
max_fields: 3
collects:
- { name: navn }
- { name: telefon, type: tel }
- { name: hva_kan_du, type: select, options: [Trene et lag, Være lagleder, Kjøre til kamper, Dugnad, Annet] }
- id: sponsor
who: en bedrift i bygda som vil støtte klubben
wants: en prat om hva de kan bidra med, og en avtale
door: bidra
lands_in: sponsorer
handled_by: styret
stages: [open, i_dialog, avtalt, avslatt]
stages_text: { open: "mottatt", i_dialog: "i dialog", avtalt: "avtalt", avslatt: "avslått" }
done_when: [avtalt, avslatt]
success: [avtalt]
max_fields: 3
collects:
- { name: bedrift }
- { name: kontaktperson }
- { name: telefon, type: tel }
- { name: hva_tenker_dere, type: textarea, optional: true }
- id: baneproblem
who: en trener som ser at noe på anlegget ikke er i orden
wants: at styret får vite det og ordner det
enters_as: trenere
lands_in: baneproblemer
handled_by: styret
stages: [open, tatt_tak_i, ordnet]
stages_text: { open: "mottatt", tatt_tak_i: "tatt tak i", ordnet: "ordnet" }
done_when: [ordnet]
success: [ordnet]
max_fields: 2
collects:
- { name: hvor }
- { name: hva, type: textarea }
# Holdt unna alle modeller som skriver ord.
personas:
- { id: ny-i-bygda, need: innmelding, text: "Vi flyttet hit i sommer og sønnen vår på ni har spilt fotball før. Hvordan får vi ham med på et lag her?" }
- { id: tvillinger, need: innmelding, text: "Jeg har tvillingjenter på sju som maser om å begynne. Er det plass, og hva koster det?" }
- { id: alenefar, need: avgiftssak, text: "Jeg er alene med tre unger og treningsavgiften for alle tre blir for mye på en gang. Går det an å dele den opp?" }
- { id: permittert, need: avgiftssak, text: "Jeg ble permittert i vinter. Kan dattera mi fortsette å spille selv om vi ikke klarer avgiften akkurat nå?" }
- { id: pensjonist, need: frivillig, text: "Jeg er pensjonert og spilte selv i mange år. Har dere bruk for en som kan ta de minste et par kvelder i uka?" }
- { id: stor-bil, need: frivillig, text: "Jeg kan ikke trene noen, men jeg har stor bil og kan kjøre til bortekamper." }
- { id: rorlegger, need: sponsor, text: "Jeg driver rørleggerfirma her i bygda og vil gjerne ha logoen vår på draktene til et lag. Hvem snakker jeg med?" }
- { id: kafeen, need: sponsor, text: "Vi har kafé i sentrum og kan bidra med noe til cupen i mai, kanskje vafler eller litt penger." }
- { id: lyset-slukker, need: baneproblem, text: "Lyset på kunstgresset slår seg av halv åtte og vi trener til åtte. Ungene løper i mørket." }
+94
View File
@@ -0,0 +1,94 @@
# A machine hire yard: two doors (hiring, and an account for firms),
# one aggregate walked by two roles (the counter reserves, the yard
# hands out and takes back), and the yard's own damage report.
business: Fjellstad Hire
language: en
contact: { name: Ola Fjellstad, email: ola@fjellstadhire.example }
about: >
Machine hire: mini diggers, lifts, compactors, generators. A counter,
a yard that delivers, collects and fixes, and an office.
doors:
- id: hiring
who: someone who needs a machine, or has one of ours that has stopped
- id: firms
who: a firm that hires often and wants an account with us
needs:
- id: hire-request
who: a private person or a firm who needs a machine for a while
wants: the right machine, from a date, for as long as they need it
door: hiring
lands_in: hires
handled_by: counter
stages: [open, reserved, out, returned, cancelled]
done_when: [returned, cancelled]
success: [returned]
# The counter reserves; the yard hands it out and takes it back.
moves_by: { out: [yard], returned: [yard] }
max_fields: 5
collects:
- { name: name }
- { name: phone, type: tel }
- { name: what, type: textarea }
- { name: from, type: date }
- { name: until, type: date }
- { name: deliver_to, optional: true }
- id: breakdown
who: someone whose hired machine has stopped working
wants: it running again, or swapped, today
door: hiring
lands_in: breakdowns
handled_by: yard
stages: [open, in_hand, resolved]
done_when: [resolved]
success: [resolved]
max_fields: 3
collects:
- { name: phone, type: tel }
- { name: machine }
- { name: what_happened, type: textarea }
- { name: where, optional: true }
- id: account-request
who: a firm that hires often
wants: to send anyone to collect, and get one invoice a month
door: firms
lands_in: accounts
handled_by: office
stages: [open, checked, approved, declined]
done_when: [approved, declined]
success: [approved]
max_fields: 4
collects:
- { name: company }
- { name: registration_number }
- { name: contact }
- { name: phone, type: tel }
- { name: invoice_email, type: email, optional: true }
- id: damage-on-return
who: someone in the yard who finds damage when a machine comes back
wants: it written down before the customer has left
enters_as: yard
lands_in: damage_reports
handled_by: office
stages: [open, invoiced, written_off]
done_when: [invoiced, written_off]
success: [invoiced]
max_fields: 3
collects:
- { name: machine }
- { name: hire_or_customer }
- { name: damage, type: textarea }
# Held out from every model that writes words.
personas:
- { id: garden-wall, need: hire-request, text: "I'm building a stone wall in the garden and need a small digger for a weekend. I've never driven one." }
- { id: roofer-lift, need: hire-request, text: "We're a roofing crew and need a scissor lift on site in Dalen from Monday for about two weeks." }
- { id: stuck-compactor, need: breakdown, text: "The plate compactor I picked up this morning won't start. I've got a crew standing around." }
- { id: lift-error, need: breakdown, text: "The lift you delivered keeps showing an error and stops halfway up. It's on our site at the school." }
- { id: landscaper-account, need: account-request, text: "We're a landscaping firm hiring from you almost every month and I'm tired of paying by card each time. Can we get invoiced monthly?" }
- { id: builder-account, need: account-request, text: "Small building firm, six people. We'd like to be able to send whoever is nearest to pick things up and have it on our account." }
- { id: yard-window, need: damage-on-return, text: "The mini digger from the Berg job came back with a smashed rear window and hydraulic oil everywhere." }
+113
View File
@@ -0,0 +1,113 @@
# A physiotherapy clinic: two doors on the front, patients and the
# people who send them; the reception handles everything, therapists
# write the report back and notice who did not turn up.
business: Bakken Fysioterapi
language: en
contact: { name: Marit Bakken, email: marit@bakkenfysio.example }
about: >
Six physiotherapists and a receptionist. People come with pain,
injuries and after operations, on their own or sent by a doctor,
and we treat them over a course of sessions.
doors:
- id: patients
who: someone with pain or an injury, or a patient who already has a time here
- id: referrers
who: a doctor, a hospital ward or an insurer sending someone to us
needs:
- id: new-patient
who: someone with something that hurts
wants: a first appointment with a therapist who suits them
door: patients
lands_in: requests
handled_by: reception
stages: [open, offered, booked, declined]
done_when: [booked, declined]
success: [booked]
max_fields: 4
collects:
- { name: name }
- { name: phone, type: tel }
- { name: what_hurts, type: textarea }
- { name: when, type: select, options: [Mornings, Afternoons, Any time] }
- { name: sent_by_doctor, type: select, options: ["Yes", "No"], optional: true }
- id: move-or-cancel
who: a patient who already has a time and cannot come
wants: the time moved, or the slot freed
door: patients
lands_in: cancellations
handled_by: reception
stages: [open, rebooked, released]
done_when: [rebooked, released]
success: [rebooked]
max_fields: 3
collects:
- { name: name }
- { name: phone, type: tel }
- { name: appointment_day, type: date }
- { name: new_time_wish, optional: true }
- id: referral
who: a GP or a hospital ward sending a patient
wants: the patient seen, and a report back when the course ends
door: referrers
lands_in: referrals
handled_by: reception
stages: [open, accepted, booked, reported, returned]
done_when: [reported, returned]
success: [reported]
# The therapist who treated them writes the report; they see the
# referral from the start.
moves_by: { reported: [therapists] }
watched_by: [therapists]
max_fields: 5
collects:
- { name: patient_name }
- { name: patient_phone, type: tel }
- { name: referrer }
- { name: what_for, type: textarea }
- { name: urgency, type: select, options: [This week, Within a month, Routine] }
- id: insurance-approval
who: the receptionist, before a course that an insurer has to approve
wants: the request sent and its answer kept where the patient's case is
enters_as: reception
lands_in: approvals
handled_by: reception
stages: [open, sent, approved, refused]
done_when: [approved, refused]
success: [approved]
max_fields: 3
collects:
- { name: patient_name }
- { name: insurer }
- { name: sessions }
- { name: case_number, optional: true }
- id: no-show
who: a therapist whose patient did not turn up and did not call
wants: the reception to ring them and get them back in
enters_as: therapists
lands_in: no_shows
handled_by: reception
stages: [open, contacted, rebooked, closed]
done_when: [rebooked, closed]
success: [rebooked]
max_fields: 2
collects:
- { name: patient_name }
- { name: appointment_day, type: date }
- { name: note, optional: true }
# Held out from every model that writes words.
personas:
- { id: runner-knee, need: new-patient, text: "I've been running for years and the outside of my right knee has started hurting after about five kilometres. I'd like someone to have a look, preferably in the mornings." }
- { id: post-surgery-mother, need: new-patient, text: "My mother had a hip replacement three weeks ago and the hospital said she needs training. She can't do much on her own and I'll drive her." }
- { id: sick-child, need: move-or-cancel, text: "I have a session on Thursday at ten but my daughter has a fever and I can't come. Can I move it?" }
- { id: sent-abroad, need: move-or-cancel, text: "Work is sending me abroad next week so my appointment on the 3rd is off. Just free the slot, I'll be in touch when I'm back." }
- { id: gp-shoulder, need: referral, text: "I'm a GP. A patient of mine has a frozen shoulder that isn't improving with what I can offer. I'd like her to start with you and hear how it goes." }
- { id: hospital-discharge, need: referral, text: "Orthopaedic ward here. We're discharging a man after an ankle fracture on Friday and need follow-up rehab arranged near where he lives. He'll need a report for his sick leave." }
- { id: reception-approval, need: insurance-approval, text: "The patient in room two has a policy that covers twelve sessions if the insurer signs off first. I need to send that off and keep track of the answer." }
- { id: therapist-no-show, need: no-show, text: "My two o'clock didn't turn up and didn't call. Second time. Someone needs to ring him." }
+331
View File
@@ -0,0 +1,331 @@
# Tomter Vel, en velforening: fire dører på forsiden (medlemskap,
# melde fra, bidra, naboer imellom) og høringsinnspill rett på
# forsiden, siden den som har en mening om en sak ikke ser seg selv
# som frivillig. Komiteene er gruppene: en søknad om plass i en
# komité velger blant de samme sju som har hver sin pult. Trafikk,
# anlegg og oppslagstavla kan alle lese; kontaktopplysninger vises
# ikke der. Nabohjelp begge veier. Oppslagstavla for deling av utstyr
# står ute med en gang: en nabo som kan låne ut, eller vil låne, sier
# fra rett fra oppslaget, og den som la det ut hører fra dem. Køen er
# innboksen til den som la ut; siden holder ikke rede på hvem som ble
# valgt. Redaktøren, de som står som ansvarlige, tar ned det som står
# igjen.
business: Tomter Vel
language: no
# Styret svarer for sidene som gruppe: ingen konto for innboksen, og
# hvert styremedlem kan foreslå endringer på dem.
contact: { name: Styret i Tomter Vel, email: post@tomtervel.no, group: styret }
# Portal holder gruppene selv: en invitasjon er en plass som venter til
# personen logger inn, og en person er telefonnummeret sitt først. Med
# Vipps-innlogging kan alle logge inn med en gang; til da lager Kanidm
# kontoen ved siden av.
people: memberships
# Avsenderen for all e-post fra siden (gdo på kasse leverer); det
# kasses postfix kan sende for i dag.
mail: { from: "Tomter Vel <tomtervel@klingenbergbygg.no>", reply_to: post@tomtervel.no }
about: >
Velforeningen for Tomter, stiftet i 1947. Sju komiteer drevet på
fritida: arrangementer, elvestien, lekeplassene, høringssvar,
miljøgata, pendlerforhold og trafikk. 157 husstander er medlemmer.
# Komiteene, som gruppene heter, med navnet folk kjenner dem under.
groups:
- { id: arrangementer, label: Arrangementer }
- { id: elvesti, label: Elvesti }
- { id: horingsinstans, label: Høringsinstans }
- { id: lekeplasser, label: Leke- og møteplasser }
- { id: miljogate, label: Miljøgate }
- { id: pendlerforhold, label: Pendlerforhold }
- { id: trafikk, label: Trafikkforhold }
# Styret kan invitere inn i alle grupper som har en pult; hver gruppe kan
# invitere inn i seg selv. Portal oppretter kontoen og en engangslenke.
invites_by: [styret]
# Forsiden åpner beskjedent: to trekk, én rolle hver, ingen knapp,
# før tilbudene lenger ned på samme side. De to velet er til for: en
# husstand som blir medlem, og en nabo som trenger en hånd. Begge har
# noe å gjøre etterpå: medlemmet melder seg ut selv, den som fikk
# hjelp sier selv fra at det er ordnet.
does:
- id: medlem
for: bli-medlem
text: >
Velet taler for Tomter overfor kommunen, holder lekeplassene og
stien i stand og samler naboene på 17. mai og dugnad. 157
husstander er med. Du melder deg inn her, og ut igjen selv når
du vil.
- id: nabo
for: trenger-hjelp
text: >
Trenger du en hånd med snømåking, handling eller skyss, kobler
nabohjelpsgruppa deg med en nabo som kan. Du sier selv fra når
det er ordnet.
doors:
- id: medlem
who: en husstand i Tomter som vil bli medlem, betale kontingent, eller melde flytting, ny e-post eller utmelding
- id: melde-fra
who: noen som har sett noe farlig i trafikken, eller noe ødelagt på lekeplassen eller stien
- id: bidra
who: noen som vil stille opp på 17. mai, dugnad eller et arrangement, eller sitte i en komité
- id: naboer
who: en nabo som trenger en hånd med snømåking, handling eller skyss, kan gi en, eller har noe å låne ut
needs:
- id: bli-medlem
who: en husstand i Tomter
wants: å bli medlem og få betalt kontingenten
lands_in: husstander
door: medlem
handled_by: kasserer
stages: [open, betalt, utmeldt]
stages_text: { open: "mottatt", betalt: "betalt", utmeldt: "meldt ut" }
done_when: [betalt, utmeldt]
success: [betalt]
# Medlemmet melder seg ut selv, gjennom sin egen lenke.
moves_by: { utmeldt: [submitter, kasserer] }
mails: { open: submitter, betalt: submitter }
# Betalt kontingent er medlemskapet: husstanden får konto og plass
# blant medlemmene i samme trekk, og det som er medlemmenes eget
# (komitéplass) ligger bak innloggingen.
grants: { betalt: medlemmer }
max_fields: 3
collects:
- { name: name }
- { name: adresse }
- { name: email, type: email }
- { name: telefon, type: tel, optional: true }
- id: endre-medlemskap
who: et medlem som har flyttet, byttet e-post eller vil melde seg ut
wants: at velet har riktige opplysninger om dem
lands_in: medlemsendringer
door: medlem
handled_by: kasserer
stages: [open, oppdatert]
stages_text: { open: "mottatt", oppdatert: "oppdatert" }
done_when: [oppdatert]
success: [oppdatert]
max_fields: 3
collects:
- { name: navn }
- { name: adresse }
- { name: hva_er_nytt, type: textarea }
- id: trafikkfare
who: en som har sett et farlig sted i trafikken
wants: at velet tar det opp med kommunen eller den som eier veien
lands_in: trafikkmeldinger
door: melde-fra
handled_by: trafikk
stages: [open, tatt_opp, ordnet, avvist]
stages_text: { open: "mottatt", tatt_opp: "tatt opp med kommunen", ordnet: "ordnet", avvist: "avvist" }
done_when: [ordnet, avvist]
success: [ordnet]
# Alle kan se hva som er meldt og hvor det står; e-posten vises ikke.
watched_by: [styret, public]
max_fields: 2
collects:
- { name: hvor }
- { name: hva, type: textarea }
- { name: epost, type: email, optional: true }
- id: anleggsfeil
who: en som ser at noe på lekeplassen eller stien er ødelagt
wants: at det blir fikset før noen skader seg
lands_in: anleggsfeil
door: melde-fra
handled_by: lekeplasser
stages: [open, tatt_tak_i, utbedret]
stages_text: { open: "mottatt", tatt_tak_i: "tatt tak i", utbedret: "utbedret" }
done_when: [utbedret]
success: [utbedret]
watched_by: [public]
max_fields: 2
collects:
- { name: hvor, type: select, options: [Lekeplassen, Elvestien, Skøytebanen, Et annet sted] }
- { name: hva, type: textarea }
# Rett på forsiden: den som har en mening om en sak er ikke frivillig.
- id: horingsinnspill
who: en beboer som har en mening om en sak kommunen har på høring
wants: at velet tar det med i sitt svar
lands_in: innspill
handled_by: horingsinstans
watched_by: [styret]
stages: [open, tatt_med, ikke_tatt_med]
stages_text: { open: "mottatt", tatt_med: "tatt med i høringssvaret", ikke_tatt_med: "ikke tatt med" }
done_when: [tatt_med, ikke_tatt_med]
success: [tatt_med]
max_fields: 3
collects:
- { name: navn }
- { name: sak }
- { name: innspill, type: textarea }
- id: hjelpe-til
who: en som vil hjelpe til på et arrangement eller en dugnad
wants: å bli satt opp på noe som passer dem
lands_in: frivillige
door: bidra
handled_by: arrangementer
stages: [open, kontaktet, satt_opp, takket_nei]
stages_text: { open: "mottatt", kontaktet: "kontaktet", satt_opp: "satt opp", takket_nei: "takket nei" }
done_when: [satt_opp, takket_nei]
success: [satt_opp]
max_fields: 3
collects:
- { name: navn }
- { name: telefon, type: tel }
- { name: hva, type: select, multiple: true, options: [17. mai, Juletregranstenning, Skøytedag, Dugnad på elvestien, Det som trengs] }
# Komitéplass er for medlemmer: døra viser veien, skjemaet ligger på
# medlemmenes side bak innloggingen.
- id: komite
who: et medlem som vil sitte i en komité
wants: en plass i den komiteen som driver med det de bryr seg om
lands_in: komitesoknader
door: bidra
enters_as: medlemmer
handled_by: styret
stages: [open, plassert, avslatt]
stages_text: { open: "mottatt", plassert: "plassert i en komité", avslatt: "avslått" }
done_when: [plassert, avslatt]
success: [plassert]
max_fields: 3
collects:
- { name: navn }
- { name: epost, type: email }
- { name: komite, type: select, multiple: true, options_from: groups }
# Nabohjelp: den som trenger en hånd, og den som kan gi en. En
# nabohjelpsgruppe kobler dem; den som fikk hjelp sier fra selv at
# det er ordnet.
- id: trenger-hjelp
who: en beboer som ikke får til noe selv for tiden, som snømåking, handling eller en skyss
wants: at en nabo stiller opp
lands_in: hjelpebehov
# Et behov ingen har tatt tak i på en uke går opp til styret: samme
# sak, samme knapper, på styrets pult i tillegg.
escalate: { open: { days: 7, to: styret } }
door: naboer
handled_by: nabohjelp
stages: [open, formidlet, ordnet, ikke_lost]
stages_text: { open: "mottatt", open_escalated: "hos styret, siden ingen tok den på en uke", formidlet: "formidlet til en nabo", ordnet: "ordnet", ikke_lost: "ikke løst" }
done_when: [ordnet, ikke_lost]
success: [ordnet]
moves_by: { ordnet: [submitter, nabohjelp] }
mails: { open: submitter, formidlet: submitter, ordnet: submitter }
max_fields: 4
collects:
- { name: navn }
- { name: email, type: email }
- { name: adresse }
- { name: hva, type: textarea }
- { name: telefon, type: tel, optional: true }
- id: kan-hjelpe
who: en nabo som kan stille opp når noen trenger det
wants: å bli spurt når det passer
lands_in: hjelpere
door: naboer
handled_by: nabohjelp
stages: [open, registrert, trukket]
stages_text: { open: "mottatt", registrert: "registrert som hjelper", trukket: "trukket" }
done_when: [registrert, trukket]
success: [registrert]
moves_by: { trukket: [submitter, nabohjelp] }
max_fields: 4
collects:
- { name: navn }
- { name: email, type: email }
- { name: telefon, type: tel }
- { name: kan, type: select, multiple: true, options: [Snømåking, Handling, Kjøring, Småfiks i huset, En prat og litt selskap, Det som trengs] }
# Oppslagstavla: ut med en gang, styret hører om det. Svar går
# rett til den som la ut; køen er innboksen deres. Tas ned av den
# som la ut, av redaktøren, eller av klokka etter en måned.
- id: dele-utstyr
who: en nabo som har noe å låne ut, eller trenger å låne noe
wants: et oppslag naboene ser med en gang
lands_in: oppslag
door: naboer
handled_by: redaktor
stages: [open, tatt_ned]
stages_text: { open: "ute", tatt_ned: "tatt ned" }
done_when: [tatt_ned]
success: [tatt_ned]
moves_by: { tatt_ned: [submitter, redaktor] }
mails: { open: post@tomtervel.no }
deadlines: { open: { days: 30, to: tatt_ned } }
# Tavla er offentlig: det er den naboene leser; navn og e-post
# vises ikke der.
watched_by: [public]
public_states: [open]
max_fields: 4
collects:
- { name: navn, private: true }
- { name: email, type: email }
- { name: har_eller_trenger, type: select, options: [Jeg har noe å låne ut, Jeg trenger å låne noe] }
- { name: hva, type: textarea }
# Svaret på et oppslag: fra oppslagets egen rad på tavla, med
# e-post eller innlogget, rett til den som la ut. Redaktøren ser
# svarene og merker dem lest.
- id: melde-seg
who: en nabo som kan låne ut, eller vil låne, det et oppslag gjelder
wants: at den som la ut oppslaget får vite det
about: dele-utstyr
lands_in: svar
handled_by: redaktor
stages: [open, lest]
stages_text: { open: "sendt", lest: "lest" }
done_when: [lest]
success: [lest]
mails: { open: owner }
max_fields: 2
collects:
- { name: email, type: email }
- { name: hilsen, type: textarea, optional: true }
- id: utlegg
who: en i en komité som har lagt ut for noe
wants: pengene tilbake
enters_as: komiteer
lands_in: utlegg
handled_by: kasserer
stages: [open, godkjent, utbetalt, avvist]
stages_text: { open: "mottatt", godkjent: "godkjent", utbetalt: "utbetalt", avvist: "avvist" }
done_when: [utbetalt, avvist]
success: [utbetalt]
max_fields: 3
collects:
- { name: navn }
- { name: belop }
- { name: hva, type: textarea }
# Holdt unna alle modeller som skriver ord.
personas:
- { id: nyinnflyttet, need: bli-medlem, text: "Vi flyttet til Tomter i mars og har hørt at det finnes et vel. Hvordan blir vi med, og hva koster det?" }
- { id: glemte-a-betale, need: bli-medlem, text: "Jeg har vært medlem før men fikk ikke betalt i fjor. Kan jeg bare vippse, og til hvem?" }
- { id: ny-epost, need: endre-medlemskap, text: "Vi har byttet e-postadresse og får ikke lenger noe fra dere." }
- { id: flytter, need: endre-medlemskap, text: "Vi selger huset og flytter til Ski i august, så vi må ut av medlemslista." }
- { id: skolevei, need: trafikkfare, text: "Bilene kjører altfor fort i svingen ved skolen om morgenen, og det er ikke fortau der ungene går." }
- { id: busslomme, need: trafikkfare, text: "Det er umulig å se om det kommer noen når man skal ut fra busslomma ved butikken, hekken har blitt kjempehøy." }
- { id: huske, need: anleggsfeil, text: "Huska på lekeplassen henger i én kjetting. Sønnen min dro i den i går og den holdt så vidt." }
- { id: bru, need: anleggsfeil, text: "Planken på den lille brua på stien langs elva er råtten, jeg gikk nesten gjennom." }
- { id: blokker, need: horingsinnspill, text: "Kommunen skal bygge blokker på jordet bak stasjonen, og jeg synes det er alt for høyt. Kan velet si ifra?" }
- { id: togtider, need: horingsinnspill, text: "Ruter vil kutte det tidlige toget. Jeg pendler til Oslo hver dag og vil at velet skriver til dem om det." }
- { id: vafler, need: hjelpe-til, text: "Jeg kan steke vafler på 17. mai-frokosten hvis dere trenger folk." }
- { id: motorsag, need: hjelpe-til, text: "Har motorsag og ledig helg, kan ta trær langs elvestien hvis det er en dugnad." }
- { id: pendler, need: komite, text: "Jeg pendler og er lei av at ingen snakker med Ruter. Jeg vil gjerne være med i gjengen som jobber med det." }
- { id: planlegger, need: komite, text: "Jeg er arealplanlegger av yrke og kunne bidratt når kommunen sender saker på høring." }
- { id: tilhenger, need: dele-utstyr, text: "Jeg har en tilhenger som står ubrukt mesteparten av året, naboer kan gjerne låne den for en hundrelapp." }
- { id: stige, need: dele-utstyr, text: "Trenger en lang stige en dag i helga for å rense takrenna, er det noen i nærheten som har?" }
- { id: snomaking, need: trenger-hjelp, text: "Jeg er 84 og klarer ikke å måke innkjørselen lenger. Er det noen som kan hjelpe meg når det snør?" }
- { id: nyoperert, need: trenger-hjelp, text: "Jeg er nyoperert i hofta og kommer meg ikke til butikken på noen uker. Kunne noen handlet for meg?" }
- { id: hjemmekontor, need: kan-hjelpe, text: "Jeg jobber hjemmefra og kan godt måke eller handle for noen som ikke får det til selv." }
- { id: pensjonert-elektriker, need: kan-hjelpe, text: "Pensjonert elektriker, har bil og god tid. Si fra om noen trenger skyss eller en hånd med småting." }
- { id: kvittering, need: utlegg, text: "Jeg kjøpte pølser og kakao til skøytedagen for 1 840 kroner, her er kvitteringa, vil ha det igjen." }
+233
View File
@@ -0,0 +1,233 @@
# UHHM, uhhm.no: the studio that builds portal and carries redoal.
# Rebuilt from the live site (uhhm/questions) on 2026-09-23. The five
# buckets the site already holds keep their live state and event
# names (`events:`), so every record on the owners' desk stays where
# it is. Two are new: a business trying the trainer on its own
# workflow (`trials`, approved by a person before anything is built),
# and what reaches UHHM as the custodian of the redoal network
# (`custody_requests`, a desk of its own).
business: UHHM
language: en
contact: { name: Bendik Aagaard Lynghaug, email: bl@uhhm.no }
# Every mail the site sends comes from here (gdo on ergo delivers).
mail: { from: "UHHM <portal@uhhm.no>", reply_to: bl@uhhm.no }
about: >
UHHM builds software around a clear argument: infrastructure, tools
and products for organizations that can say what problem they have.
It also builds and carries the redoal network - the gesture math,
the relay, the first node, and the portal this site runs on - and
answers for what that network keeps.
groups:
- { id: owners, label: Owners }
# The people who answer for the redoal network: they host its first
# node and relay, keep what is left there, and decide what stays.
- { id: custodians, label: redoal custodians, invited_by: [owners] }
# Owners onboard into every desk; a custodian can vouch for another.
invites_by: [owners]
# The front door opens modestly: one or two features, one role each,
# and no button, before the offers further down the same page. The
# two roles UHHM exists for: an organization with a problem, and the
# people on the redoal network it keeps.
does:
- id: software
for: commission
text: >
Software built around a clear argument. Infrastructure, tools
and products for organizations that can say what problem they
have - and a working draft of your own workflow as a site,
before you pay for anything.
- id: redoal
for: custody
text: >
The redoal network, built and carried here: the gesture math,
the relay, the first node, and the portal this site runs on.
UHHM answers for what that network keeps.
# Things to look at before asking for anything.
shows:
- id: built
text: A few things already out in the world.
resource:
public: true
source: { kind: gitea_starred, username: bl }
jq: >
.[] | {name: .name, description: .description,
url: (if .website != null and .website != "" then .website
elif .private then null
else .html_url end)}
- id: voices
text: Live from the redoal relay - what has been left at places so far.
resource:
public: true
source: { kind: url, url: https://relay.redoal.com/pulse }
jq: >
{ "name": (.voices_echoed | tostring),
"description": "voices echoed so far" }
empty: The first voice has not echoed yet.
needs:
- id: commission
who: an organization with a problem it wants software to solve
wants: a considered reply to a brief, from people who read the whole thing
lands_in: projects
handled_by: owners
stages: [open, accepted, declined]
done_when: [accepted, declined]
success: [accepted]
events: { open: submitted }
stages_text: { open: "received", accepted: "taken on", declined: "not a fit for us" }
# The client plays too: they can withdraw the brief themselves.
moves_by: { declined: [submitter, owners] }
mails: { open: submitter, accepted: submitter, declined: submitter }
max_fields: 4
collects:
- { name: name }
- { name: email, type: email }
- { name: project, type: textarea }
- { name: organization }
- { name: org_contact, optional: true }
# The client feature: the trainer tried on a real business. A person
# approves each intake before anything is built from it.
- id: trial
who: someone who runs a business where requests, approvals and handovers get lost, and wants to see it as a site before committing to anything
wants: a first draft of a site for their own roles, built from their own words and approved by a person first
lands_in: trials
handled_by: owners
stages: [open, approved, previewed, declined]
done_when: [previewed, declined]
success: [previewed]
stages_text: { open: "received", approved: "approved for a draft", previewed: "a preview is ready", declined: "not a fit for us" }
mails: { open: submitter, approved: submitter, previewed: submitter, declined: submitter }
attended_by: >
approved -> previewed: the n8n workflow that builds the draft
site from an approved intake (docker, rauthy, portal content)
and sends the preview link. Until it exists, owners run
portal-trainer by hand and press the button.
max_fields: 6
collects:
- { name: business }
- { name: email, type: email }
- { name: what_we_do, type: textarea }
- { name: who_comes_to_us, type: textarea }
- { name: what_we_keep_track_of, type: textarea }
- { name: who_is_involved, type: textarea }
- { name: what_slows_us_down, type: textarea, optional: true }
- id: collaborate
who: someone who wants to work with us on what we build
wants: to be read as a person, and invited in if there is a fit
lands_in: applicants
handled_by: owners
stages: [open, invited, active, departed, declined]
done_when: [departed, declined]
success: [departed]
events: { open: applied, active: onboarded }
# Declined before starting; after that a person departs.
turned_down_from: [open, invited]
stages_text: { open: "received", invited: "invited in", active: "working with us", departed: "moved on", declined: "not a fit for us" }
max_fields: 3
collects:
- { name: name }
- { name: email, type: email }
- { name: interest, type: textarea }
- { name: note, type: textarea, optional: true }
- id: subscribe
who: someone who wants to hear from us when there is something to say
wants: a note now and then, and an easy way out
lands_in: subscribers
handled_by: owners
stages: [open, unsubscribed]
done_when: [unsubscribed]
events: { open: subscribed }
stages_text: { open: "on the list", unsubscribed: "off the list" }
moves_by: { unsubscribed: [submitter, owners] }
max_fields: 2
collects:
- { name: name }
- { name: email, type: email }
# This site proposes its own changes through itself.
- id: propose
who: a visitor who sees how a page on this site should change
wants: the change read by a person and, if approved, made
lands_in: development_proposals
handled_by: owners
stages: [open, approved, rejected]
done_when: [approved, rejected]
success: [approved]
events: { open: proposed }
stages_text: { open: "received", approved: "approved", rejected: "turned down" }
# The site changes in the open: anyone can read what was approved,
# without who proposed it.
watched_by: [public]
public_states: [approved]
max_fields: 3
collects:
- { name: submitted_by, private: true }
- { name: email, type: email, optional: true }
- { name: page, optional: true }
- { name: rationale, type: textarea }
- { name: proposed_yaml, type: textarea }
# The custodian role, on the front door: UHHM answers for the redoal
# network it hosts. Owners see these too, and move nothing.
- id: custody
who: someone who runs, builds on, or is affected by the redoal network and needs the people who keep its first node
wants: an answer from a custodian, and action where the network keeps something it should not
lands_in: custody_requests
handled_by: custodians
watched_by: [owners]
stages: [open, in_dialogue, resolved, declined]
done_when: [resolved, declined]
success: [resolved]
stages_text: { open: "received", in_dialogue: "in dialogue with a custodian", resolved: "resolved", declined: "closed without action" }
# The person affected says when it is resolved, not only the custodian.
moves_by: { resolved: [submitter, custodians] }
mails: { open: submitter, resolved: submitter, declined: submitter }
max_fields: 4
collects:
- { name: name }
- { name: email, type: email }
- { name: about, type: select, options: [Running a node of my own, Building on the network, Something the network keeps about me, Something else] }
- { name: what, type: textarea }
# Internal: a relationship worth tracking before it is a project.
- id: prospect
who: an owner who has met an organization worth following up
wants: it written down and walked through its states from the desk
enters_as: owners
lands_in: organizations
handled_by: owners
stages: [prospect, client, past_client]
done_when: [past_client]
skippable: [client]
stages_text: { prospect: "a prospect", client: "a client", past_client: "a past client" }
events: { prospect: identified, client: became_client, past_client: churned }
max_fields: 2
collects:
- { name: name }
- { name: contact }
- { name: note, type: textarea, optional: true }
# Held out from every model that writes words. The first eight are the
# 2026-09-21 set (runs/uhhm-2026-09-21); the rest are for what is new.
personas:
- { id: architecture-firm, need: commission, text: "We run a small architecture firm and our project files are scattered across three cloud drives. We want to pay someone to set up proper storage and backups we control ourselves." }
- { id: climbing-gym-founder, need: commission, text: "I have an idea for a booking app for climbing gyms and I'm looking for a team that can build the first version with me." }
- { id: municipality-buyer, need: commission, text: "Our municipality needs an internal tool for tracking building permits. We are looking for a vendor who can scope it and deliver it." }
- { id: exhibition-visitor, need: subscribe, text: "I saw your drawing network at an exhibition and liked it. I don't want to get involved, I'd just like to know when there is news." }
- { id: tech-journalist, need: subscribe, text: "I write about Nordic technology. Keep me posted on what you release so I can cover it." }
- { id: rust-developer, need: collaborate, text: "I'm a Rust developer between jobs and I like that you build in the open. I would love to contribute, and maybe join you." }
- { id: interaction-designer, need: collaborate, text: "I'm an interaction designer. I want to work with you on the drawing interface." }
- { id: student-intern, need: collaborate, text: "I'm a computer science student looking for a summer internship doing infrastructure work." }
- { id: plumber-owner, need: trial, text: "I run a plumbing company with four vans. Jobs come in by phone and text and some get lost. I'd like to see how this would look for us before I spend any money." }
- { id: dance-school, need: trial, text: "We are a dance school. Enrolments, cancellations and invoices all go through one email inbox that only I understand. Can you show me a version made for us?" }
- { id: node-runner, need: custody, text: "I run a small server in Berlin and want to host a redoal node that peers with yours. Who do I talk to?" }
- { id: recorded-name, need: custody, text: "Someone left a recording on redoal that says my full name and where I live. I want it gone, and I want to know who is responsible for that network." }
- { id: app-builder, need: custody, text: "I'm building a music app and would like to use redoal's gesture addresses in it. Is that possible, and who decides?" }
- { id: wording-fix, need: propose, text: "The subscribe page should say how often you actually write. I'd like to suggest that exact change to the page." }
- { id: rewrite, need: propose, text: "One page on this site reads wrong to me and I know exactly how I'd rewrite it. Where do I send the new text?" }