# WorkProof > WorkProof is for City of Espoo talent events. Companies see real work and score it; the AI never scores. A professional does a short real task from an Espoo company. A named person at the company scores the work from 1 to 5 on each of the task's skills, writes a reason, and chooses Meet or Not now. As a safeguard, the name stays hidden until the professional accepts. Tested: 0 of 79 planted identity details reached a reviewer. Show your work. Get the interview. A person at the company scores your real work; the AI never scores. - Live demo: https://workproof.wicflow.com - Source code: https://github.com/felixwickholm/workproof (MIT licence) - Entry: Junction 2026 (2026-11-13 to 2026-11-15), City of Espoo challenge "Rethinking Recruitment". First commit 2026-11-14. - Numbers checked on 2026-11-14 with `npm run evidence`. - Demo data only: 10 invented employers, 12 challenges, 62 pieces of work. ## Who it is for - **Professionals looking for work** (Job seeker, [/](https://workproof.wicflow.com/)): Pick a challenge from an Espoo company, do it in 10 to 15 minutes while thinking aloud, see what the company will read, answer two follow-up questions, then accept or decline an invitation. A declined company never sees the name. - **Companies hiring at the event** (Company, [/employer](https://workproof.wicflow.com/employer)): Paste a job-ad link or say the need in one sentence, approve a challenge with 3 to 5 core skills and a number of places, read the work without names in arrival order, score each core skill from 1 to 5 with a written reason, and invite the people to meet. - **City of Espoo event organisers** (City of Espoo, [/city](https://workproof.wicflow.com/city)): See outcomes, not sign-ups: challenges taken, scored, invited, talked, follow-ups agreed and hires. Make a challenge for a company from its job ad, draft a reminder to a company with work waiting, and export the list of talks. Slots and labels, never names. ## Key facts - Built during Junction 2026 (13 to 15 November 2026) for the City of Espoo challenge "Rethinking Recruitment". - Three views in one app: Job seeker, Company, City of Espoo. - Places per task: each task takes 5 to 30 answers (12 unless the company chooses), so an employer can read them all. The company sets the number before it approves the challenge, and each challenge page shows the places left. - A decision names a reviewer registered for the company. A company lists the people who may decide; a name that is not on the list is refused (HTTP 403) until that person adds themselves in one explicit, logged step, and a name that reads as software is refused outright (HTTP 400). This demo has no sign-in, so the name is not verified. - WorkProof has no automated decision feature. A decision is recorded with the name of a reviewer registered for the company and a written reason. This demo has no sign-in, so the name is not verified. - A named reviewer scores the work from 1 to 5 on each core skill of the task and writes why. The total shows out of 10 (the average points × 2), and a task's top work is sorted by these human totals. Software never gives or suggests a number. - Every reviewer reads the same line under every proof: "Personal details are removed by rules before review. Some can remain." No count of hidden details is shown to a reviewer, since a count would hint at a background. - Facts about the job (the work language, a language level, a licence the law asks for) are neither core skills nor set aside: they are kept under "Good to know", in fixed forms. A wish for a native speaker is set aside with "Ask for a language level, not a native speaker." - Table mode is a live loop: a visitor scans the QR code and does a 2-minute task; the host invites the proof with a two-sentence reason; the visitor accepts or declines on the phone. The big screen shows labels and tasks, never free text, and a name only after its person accepted. - A person can decline an invitation; the name stays hidden from that company. - A picture is sent only after the person confirms it shows only the work. A reviewer can report a picture as possibly identifying; it is then removed for every reader. - The city can act: it makes a challenge for a company from its job ad, drafts a reminder to a company with work waiting (nothing is sent) and exports the list of talks as a CSV file. The organiser sees slots and labels, never names. - Each visitor gets a private demo event behind a cookie. A request without that cookie reads the shared demo event and cannot change it. The visitor's private key travels in a request header, never in an address. - No login and no API keys are needed to run the demo. All people and companies are synthetic. - AI drafts tasks and two follow-up questions. WorkProof has no automated decision feature; each decision carries a reviewer's name and a written reason. - Known personal details are removed by rules before anyone reads the work. Rules can miss some; pictures are checked by the person, not by software. Text is redacted by the same rules before it reaches a model. The built-in designer runs without any model. - Made for Espoo's talent events for business and tech people. - Four formats: Choose, Make, Fix, Read. The demo event has 30 challenges from 18 companies. Without an AI key the built-in designer still drafts: 49 professions have their own task today; any other gets the nearest one or a general planning task. - No decision without a reason: every recorded decision carries a written reason, and unanswered work stays visible to the city. Nothing forces an employer to decide. - Checked on 2026-11-14: 1936 of 1936 unit tests pass (npm test); 63 of 63 eval tests pass (npm run eval). ## The challenge in the City of Espoo's words > How can recruitment and matchmaking events organized by City of Espoo help companies and talent relevantly find and connect with each other, despite AI-generated job search materials and AI-assisted recruitment processes are becoming common? > …solutions that help create stronger signal and less noise throughout the process of registering and attending an event… > …automated profiling or automated decision-making about individuals without human involvement should not be a part of the solution. ## How WorkProof meets each judging criterion | Criterion | What we built | Where to see it | Where in code | Evidence | |---|---|---|---|---| | Impact & Relevance (45%) | Companies register with a short real challenge in one of four formats (Choose, Make, Fix, Read); people answer with work and think aloud while working; a person reviews with known personal details removed and every recorded decision carries a written reason; the city sees each step from challenge taken to scored, invited, talked, follow-up agreed and hired, and the work still waiting on each challenge. | [/seeker](https://workproof.wicflow.com/seeker), [/employer](https://workproof.wicflow.com/employer), [/city](https://workproof.wicflow.com/city) | `src/lib/service/views`, `src/lib/service/decisions.ts`, `src/lib/service/meetings.ts` | Flow eval: every step from task to a meeting both sides confirmed passes (evals/flow.eval.ts). | | Innovation & User Experience (35%) | A short real task to score; the blind proof with a slider between what was written and what the company reads; the name shown only after the person accepts; Second look shares a proof with a second employer with the same skills, with consent; a 1-minute guided tour and a Show me demo in each view; a 2-minute task on a phone. | [/](https://workproof.wicflow.com/), [/try](https://workproof.wicflow.com/try), [/my-work](https://workproof.wicflow.com/my-work) | `src/lib/blind`, `src/lib/matching/second-look.ts`, `src/components/proof/blind-compare.tsx` | Story eval: every story stage rebuilds the visitor's own demo event (evals/story.eval.ts); end-to-end run on a 390x844 screen (evals/e2e). | | Responsible & Feasible Execution (20%) | AI never scores or ranks people: a guard checks every AI output; known personal details are removed by rules before review; every decision needs a registered reviewer name and written feedback (not verified without sign-in); runs with no keys; request limits; Docker build. | [/city](https://workproof.wicflow.com/city), [/for-judges](https://workproof.wicflow.com/for-judges) | `src/lib/guard`, `src/lib/blind`, `src/app/api/decisions/route.ts`, `src/lib/http/limits.ts`, `Dockerfile` | Guard: 70 of 70 planted judging phrases rejected; 575 of 575 text fields of drafted tasks and follow-up questions pass; blind layer: 0 of 79 planted identity strings reach a reviewer; 0 automated decisions. | ## How it answers each problem in the brief | Problem named in the brief | What WorkProof does | Where in code | Evidence | |---|---|---|---| | Companies struggle to describe their needs clearly and early enough. | One sentence becomes a task with 3 to 5 core skills; extra wishes are set aside; a person approves it. | `src/app/company/start-step.tsx`, `src/app/company/draft-step.tsx`, `src/lib/ai/scripted` | Task generator eval: 20 of 20 drafts have 3 to 5 core skills; 3 of 3 planted biased wishes set aside. | | CVs and messages are generated and analysed with AI, so matching rests on synthetic representations. | The company scores real work on its own task. The person then answers two follow-up questions about their own choices, and the meeting starts from the work. | `src/lib/ai/scripted/follow-ups.ts`, `src/lib/service/submissions.ts` | Flow eval: two follow-ups about the answer; follow-ups read only the redacted answer (privacy eval). | | Companies are overwhelmed by large volumes of irrelevant approaches. | Each proof is work on the company's own task; the queue is in arrival order and holds blind proofs only. | `src/lib/service/views/queue.ts` | Queues in arrival order: yes (no-automated-decisions eval). | | Companies assume there are no suitable candidates in the city's pool. | The city makes a challenge from a company's job ad, and people do it before the company opens it. In the demo, Kide Silicon's challenge already has 20 pieces of work. The company's home data also counts proofs handed in for tasks that ask for the same skills. Counts only, never a list of people. | `src/lib/service/views/employer.ts`, `src/components/city/make-challenge.tsx` | Unit tests in src/lib/service/views/employer.test.ts. | | Matching should not reinforce bias by nationality, language, education or career path. | Known names, contacts, places, schools, employers and residence status are removed by rules before review, and some can remain; tasks cannot ask for protected attributes. | `src/lib/blind`, `src/lib/guard/protected.ts` | 0 of 79 planted strings reach the reviewer; 54 of 54 and 69 of 72 identity forms hidden. | | No automated profiling or automated decision-making about individuals. | WorkProof has no automated decision feature: a decision is recorded with a registered reviewer's name, the reviewer's own points for each core skill and a written reason; a guard blocks scores and ranks in AI output; every decision is in the audit log. | `src/lib/guard/no-judgement.ts`, `src/app/api/decisions/route.ts`, `src/lib/service/decisions.ts`, `src/lib/domain/scoring.ts` | 70 of 70 planted judging phrases rejected; 0 automated decisions in the audit log. | | Few hires per company, and a company may not answer. | Nothing forces an answer, so the city sees the answers each employer still owes. After 48 hours without an answer, or after a not now, the proof can go to another employer whose task shares at least 2 core skills, if the person allowed it (Second look). | `src/lib/matching/second-look.ts`, `src/lib/service/sharing.ts` | Flow eval: company B sees the proof through Second look; company C without shared skills cannot. | | The city cannot measure whether events lead to meetings. | Both sides confirm a meeting took place; the city sees answers given, reply time, meetings confirmed and answers owed per employer. | `src/lib/service/views/city.ts`, `src/lib/service/views/insights.ts`, `src/app/api/meetings` | Flow eval: both sides confirm the meeting; city totals change and show 0 automated decisions. | | The event platform may crash when everyone logs in at the start. | Work is done over the days before the event, with no login rush; request rate and body size limits. | `src/lib/http/limits.ts`, `src/lib/service/capacity.ts`, `src/lib/service/views/insights.ts` | Privacy eval: the body cap answers HTTP 413 and the rate limit HTTP 429. | | International recruitment adds language, permit and relocation questions. | Facts the company writes about the job (a work language, a language level, a licence) are kept in fixed forms under "Good to know" on the draft and on the challenge page. A wish for a native speaker is set aside with "Ask for a language level, not a native speaker." | `src/lib/guard/job-facts.ts`, `src/app/company/draft-step.tsx`, `src/app/challenges/[id]/challenge-detail.tsx` | Task generator eval: 6 of 6 needs carry their facts as requirements only. | | Solutions should minimise personal data processing. | No account; the person's key stays in the browser; accepting shows the name to that company only, and the organiser sees slots and labels, never names; WorkProof stores only the text of a spoken answer. | `src/lib/client/local.ts`, `src/lib/service/meetings.ts`, `src/lib/voice` | 0 of 37 public responses carry a talent id; identity hidden until accepted. | ## Shaped at the Espoo booth On 13 November we asked City of Espoo staff if a work task could replace the CV. They had thought of something similar "on a smaller scale" and named three risks. Our answers: - **A company hires only a few.** With consent, one proof can reach a second employer with the same core skills (Second look), after 48 hours without an answer or after a Not now. - **Friction must stay low.** No login and no account. The demo challenges take 10 or 15 minutes. The stated length is capped at 30. - **AI can do the task.** AI is allowed. Two follow-up questions ask about the person's own choices, and the meeting starts from the work. ## What the AI does and never does Does: - Drafts a task from a company's one-sentence need, with 3 to 5 core skills. A person approves every company task. Practice tasks are generated without approval and go to no company. - Writes two follow-up questions about the person's own choices in their answer. - Every AI output passes the no-judgement guard; on any failure the scripted designer is used. Never does: - Scores, ranks, rates or recommends a person. - Receives a person's name or contact field. Text is redacted by the same rules before it reaches a model. The built-in designer runs without any model. Rules can miss some details in free text. - Decides Meet or Not now. WorkProof has no automated decision feature; a decision carries a registered reviewer's name and written feedback. - Analyses a voice or a face. A spoken answer becomes text in the browser or once on the server, and WorkProof stores only the text. ## Guardrails | Rule | Enforced in | Checked by | |---|---|---| | Software never scores, ranks or rates a person. Only a named reviewer gives points, 1 to 5 per core skill, with a written reason. AI output with a score, rank or verdict is rejected. | `src/lib/domain/scoring.ts`, `src/lib/service/decisions.ts`, `src/lib/guard/no-judgement.ts`, `src/lib/ai/guarded.ts` | `evals/no-automated-decisions.eval.ts`, `src/lib/domain/scoring.test.ts`, `src/lib/guard/no-judgement.test.ts` | | Every decision carries the name of a reviewer registered for the company and at least 4 words of feedback. Another name is refused with HTTP 403, a name that reads as software with HTTP 400. Without sign-in the name is not verified. | `src/lib/domain/decision-rules.ts`, `src/lib/service/reviewers.ts` | `evals/no-automated-decisions.eval.ts`, `src/lib/service/reviewers.test.ts` | | Reviewers get a blind proof built by an allow-list that names every field. Identity lives only on the talent record. | `src/lib/blind/project.ts`, `src/lib/domain/types.ts` | `src/lib/blind/project.test.ts`, `evals/blind-review.eval.ts`, `evals/privacy.eval.ts` | | A company sees the name only after the person accepts the invitation. | `src/lib/service/meetings.ts`, `src/lib/service/views/queue.ts` | `evals/blind-review.eval.ts`, `evals/flow.eval.ts` | | Work waiting to be read is in arrival order. The only other order is a task's top work, by the reviewers' own totals. | `src/lib/service/views/queue.ts`, `src/lib/service/views/leaderboard.ts` | `evals/no-automated-decisions.eval.ts`, `src/lib/service/views/leaderboard.test.ts` | | Each task takes 5 to 30 answers, so an employer can read them all. | `src/lib/domain/places.ts` | `src/lib/domain/places.test.ts`, `evals/flow.eval.ts` | | Audio is never stored. Only the transcript is sent with a submission. | `src/lib/http/schemas.ts`, `src/lib/voice/server/elevenlabs.ts` | `src/lib/voice/server/elevenlabs.test.ts` | | The person's private key is never in an address. A request without the visitor's sandbox cookie reads the shared demo event and cannot change it. | `src/lib/http/talent-key.ts`, `src/lib/store/index.ts` | `src/lib/http/talent-key.test.ts`, `src/lib/store/index.test.ts` | ## Architecture One Next.js app. Pages and the JSON API call the service rules. The rules pass work through the blind layer before anyone reads it (rules remove known personal details; some can remain), check every AI output with the guard, and keep each visitor's demo copy in memory. - [src/app](https://github.com/felixwickholm/workproof/tree/main/src/app): Next.js routes: pages and the JSON API under src/app/api - [src/lib/service](https://github.com/felixwickholm/workproof/tree/main/src/lib/service): Rules for tasks, submissions, decisions, meetings and Second look; view builders for each mode - [src/lib/blind](https://github.com/felixwickholm/workproof/tree/main/src/lib/blind): The blind layer: rules remove known personal details before any reviewer or model reads the work; some can remain - [src/lib/guard](https://github.com/felixwickholm/workproof/tree/main/src/lib/guard): The no-judgement guard: rejects scores, ranks and protected attributes in AI output - [src/lib/ai](https://github.com/felixwickholm/workproof/tree/main/src/lib/ai): Scripted task designer (default) and Claude and Gemini adapters, all behind the guard - [src/lib/matching/second-look.ts](https://github.com/felixwickholm/workproof/tree/main/src/lib/matching/second-look.ts): Second look: compares tasks by shared core skills, never people - [src/lib/voice](https://github.com/felixwickholm/workproof/tree/main/src/lib/voice): Voice input: Web Speech API, ElevenLabs or typed; WorkProof stores only the transcript - [src/lib/store](https://github.com/felixwickholm/workproof/tree/main/src/lib/store): In-memory store and a private demo copy (sandbox) per visitor - [src/lib/seed](https://github.com/felixwickholm/workproof/tree/main/src/lib/seed): The synthetic event: 18 employers, 30 challenges, 76 pieces of work from 46 people, the scores and decisions - [evals](https://github.com/felixwickholm/workproof/tree/main/evals): Eval suites: task generator, blind review, no automated decisions, privacy, flow, story ## Data and privacy - No account. The person's private key is kept in their own browser. - Reviewers and AI models read work only after the blind layer: known personal details are removed by rules, and some can remain. Responses to companies and the city carry no talent id. - Accepting shows the name and contact to that company only. The organiser sees slots and labels, never names. - Second look is off until the person turns it on, and can be withdrawn. - Voice is turned into text; WorkProof stores only the text. Pictures are re-encoded in the browser and the server refuses a JPEG that still carries EXIF data. - Each visitor's demo copy lives in memory and is dropped after 12 hours. Every person, company, task, proof and decision in the demo is invented, including Mikko Lahtinen and the ten employers of "Espoo Talent Day". Email addresses use example.org and phone numbers are placeholders. Any likeness to a real person or company is accidental. The seed is in src/lib/seed. ## Evidence, checked on 2026-11-14 - `npm test`: 1936 of 1936 unit tests pass, in 129 files. - `npm run eval`: 63 of 63 eval tests pass, in 7 files. The live-AI file is skipped without a key. - Blind review, harder case set: the 3 known misses are another spelling of the author's own name, an employer that is on no list, a third person's name in running text. | Suite | What is counted | Result | File | |---|---|---|---| | Task generator | Drafts with a title, a brief, 3 to 5 core skills, minutes and a review guide | 20 of 20 drafts in shape | `evals/task-generator.eval.ts` | | Task generator | Planted biased wishes set aside (age, native speaker, top university) | 3 of 3 baited drafts set it aside | `evals/task-generator.eval.ts` | | Task generator | Planted identity strings in published task cards | 0 planted strings found in 47 published cards from 47 needs | `evals/task-generator.eval.ts` | | Blind review | Planted identity strings that reached the reviewer payload | 0 of 79 planted strings found | `evals/blind-review.eval.ts` | | Blind review | Identity forms hidden, first case set | 54 of 54 cases fully hidden | `evals/blind-review.eval.ts` | | Blind review | Identity forms hidden, second case set | 69 of 72 cases fully hidden, 3 known limits | `evals/blind-review.eval.ts` | | No automated decisions | Planted judging phrases rejected by the guard | 70 of 70 judging phrases rejected | `evals/no-automated-decisions.eval.ts` | | No automated decisions | Neutral task phrases let through by the guard | 33 of 33 neutral phrases passed | `evals/no-automated-decisions.eval.ts` | | No automated decisions | Text fields of drafted tasks and follow-up questions that pass the guard | 575 of 575 text fields from 32 designer outputs (20 drafts, 12 follow-up sets) | `evals/no-automated-decisions.eval.ts` | | No automated decisions | HTTP status of 2 decisions sent without a reviewer or feedback; stored decisions with a named reviewer | refused: HTTP 400 for a decision without a name, HTTP 400 without feedback; 34 stored decisions all named | `evals/no-automated-decisions.eval.ts` | | No automated decisions | Automated decisions in the city totals and the audit log | 0 automated decisions; 0 decision audit lines by a non-human | `evals/no-automated-decisions.eval.ts` | | No automated decisions | Decisions signed by software or by a name the company has not registered | refused: HTTP 400 for "AI bot", HTTP 403 for an unregistered name, HTTP 400 for a bot as reviewer; accepted after the explicit step: HTTP 200 | `evals/no-automated-decisions.eval.ts` | | Table screen | Names, emails and numbers typed on phones that reach the public big screen | 0 of 10 names, emails and numbers found; extra fields: none | `evals/table.eval.ts` | | Consent and privacy | Public responses that carry a talent id | 0 of 37 talent ids found in company and city responses | `evals/privacy.eval.ts` | | Consent and privacy | HTTP status for 5 bad pictures, then for 2 clean JPEGs; the description runs through the blind layer | refused: HTTP 400, HTTP 400, HTTP 400, HTTP 400, HTTP 400 for EXIF, a /demo path, PNG, video, 1 byte over 300 kB; accepted: HTTP 200 at 300 kB, HTTP 200 clean | `evals/privacy.eval.ts` | | Flow | Full API flow from task to a meeting both sides confirmed | 28 of 28 steps | `evals/flow.eval.ts` | | Demo story | Story stages rebuild the visitor's own demo event | 6 of 6 steps | `evals/story.eval.ts` | Every row `npm run eval` printed in that run: | Eval and check | Result | |---|---| | 1 Task generator › shape (title, brief, 3-5 skills, minutes, guide) | 20 of 20 drafts in shape | | 1 Task generator › no protected attribute in visible fields | 0 protected attributes found in 20 drafts | | 1 Task generator › bias bait set aside | 3 of 3 baited drafts set it aside | | 1 Task generator › fantasy profiles cut to <= 4 skills | 3 of 3 fantasy profiles cut | | 1 Task generator › drafts pass the guard | 0 judging phrases found in 20 drafts | | 1 Task generator › sentence-form needs keep their skills | 4 of 4 needs kept their skills | | 1 Task generator › adversarial needs: planted strings in published cards | 0 planted strings found in 47 published cards from 47 needs | | 1 Task generator › job facts published as requirements | 6 of 6 needs carry their facts as requirements only | | 2 Blind review › planted strings in reviewer payload | 0 of 79 planted strings found | | 2 Blind review › identity keys in proof payloads | no identity keys found | | 2 Blind review › identity hidden until accepted | hidden, then shown | | 2 Blind review › review cases hidden | 54 of 54 cases fully hidden | | 2 Blind review › second review cases hidden | 69 of 72 cases fully hidden, 3 known limits | | 2 Blind review › visitor inputs from the table | 40 of 40 forms fully hidden; 16 of 16 technical texts unchanged | | 2 Blind review › technical text left intact | 5 of 5 answers unchanged | | 2 Blind review › reviewer payload: [hidden] only, no count | one token, no count, no categories | | 2 Blind review › unique proof labels | 261 distinct labels for 261 proofs | | 3 No automated decisions › (a) guard: judging rejected | 70 of 70 judging phrases rejected | | 3 No automated decisions › (a) guard: neutral passed | 33 of 33 neutral phrases passed | | 3 No automated decisions › (b) designer output passes the guard | 575 of 575 text fields from 32 designer outputs (20 drafts, 12 follow-up sets) | | 3 No automated decisions › (b2) 'At a glance' is structure only | 12 of 12 lines describe shape only | | 3 No automated decisions › (c) queues in arrival order | every queue of 10 companies | | 3 No automated decisions › (d) decisions need reviewer and feedback | refused: HTTP 400 for a decision without a name, HTTP 400 without feedback; 34 stored decisions all named | | 3 No automated decisions › (e) automated decisions | 0 automated decisions; 0 decision audit lines by a non-human | | 3 No automated decisions › (f) decisions name a registered person | refused: HTTP 400 for "AI bot", HTTP 403 for an unregistered name, HTTP 400 for a bot as reviewer; accepted after the explicit step: HTTP 200 | | 3 No automated decisions › (g) scores only from a registered person | refused: HTTP 400 without scores, HTTP 400 with a skill missing, HTTP 400 from a software name; accepted: a named reviewer's 6 of 10 | | 3 No automated decisions › (h) related parts carry no score | 0 numbers and 0 judging phrases in 24 marked sentences from 37 proofs | | 3 No automated decisions › (i) portfolio shows own scores, never a rank | 7 cards newest first, 6 reviewer scores shown to the owner, 0 rank keys, skill bars as task counts; a link shows no name, contact or key, the score and decision only when shown, and stops when ended | | 4 Consent and privacy › accept and confirm need the right party | refused: HTTP 400 with no key, HTTP 403 with a wrong key, HTTP 403 for a guessed talent confirm, HTTP 403 for another company's confirm | | 4 Consent and privacy › talent ids in public responses | 0 of 37 talent ids found in company and city responses | | 4 Consent and privacy › private key in a header only | header read, path form gone, key absent from the audit log | | 4 Consent and privacy › hidden count shown to the author only | 0 reviewer responses with a count or categories; author sees the count: true | | 4 Consent and privacy › identity locked after first event proof | kept | | 4 Consent and privacy › server rejects biased or judging cards | refused: HTTP 400, HTTP 400, HTTP 400 for biased, fake-skill and judging cards; HTTP 200 for the clean card | | 4 Consent and privacy › reviewer '.' and dotted feedback rejected | refused: HTTP 400 for reviewer ".", HTTP 400 for dotted feedback | | 4 Consent and privacy › follow-ups read the redacted answer | no identity | | 4 Consent and privacy › Second look: opt-in, owner-only, withdrawable | off by default: true; HTTP 403 for a stranger's switch; HTTP 403 for a decision after withdrawal | | 4 Consent and privacy › company's own sentence kept from talent | rebuilt need only | | 4 Consent and privacy › body cap and rate limit | HTTP 413 for a text body over its cap, HTTP 413 for a submission over its cap; request 61 from one address: HTTP 429; story step after that: HTTP 200 | | 4 Consent and privacy › pictures: clean JPEG only, description blind | refused: HTTP 400, HTTP 400, HTTP 400, HTTP 400, HTTP 400 for EXIF, a /demo path, PNG, video, 1 byte over 300 kB; accepted: HTTP 200 at 300 kB, HTTP 200 clean | | 4 Consent and privacy › pictures: the person's own check is required | refused: HTTP 400 without the check, HTTP 400 unchecked; accepted: HTTP 200 checked | | 4 Consent and privacy › pictures: a report removes the picture for everyone | refused: HTTP 403 for a company that cannot read it, HTTP 400 without a name; reported: HTTP 200; picture in responses after: false | | 4 Consent and privacy › a submission with invented questions is refused | HTTP 400 without issued questions, HTTP 400 with invented ones, HTTP 409 for changed words, HTTP 200 with the issued ones, HTTP 409 for a second proof to the same task | | 4 Consent and privacy › follow-up answers are required | event: HTTP 400 with none, HTTP 400 with one, HTTP 400 with a blank one, HTTP 200 with two; practice: HTTP 400 with none, HTTP 200 with one | | 4 Consent and privacy › practice proof keeps no name | HTTP 200; the given name in the store: nowhere | | 4 Consent and privacy › decline keeps the name hidden | HTTP 403 for a stranger; status after decline: declined, after a later accept: declined; name in responses: false | | 4 Consent and privacy › Second look withdrawal reaches decided proofs | work shown to the Second look employer after withdrawal: false; its decision kept: true | | 4 Consent and privacy › identity strings sent to a model | 0 of 48 checks found a planted string in 6 requests (draft, practice task, follow-ups; Claude and Gemini) | | 4 Consent and privacy › city: slots and labels, never names | 0 names, contacts or keys in city responses, task leaderboards, share matches and the meeting export over 4 story stages, "met" included | | 4 Consent and privacy › top proofs and shared work stay blind | 0 identity strings in the top list and the receiving queue; shared: HTTP 200; scores sent along: false | | 5 Flow (API) › seed has 10 companies | ok | | 5 Flow (API) › taster is a 2-minute task | ok | | 5 Flow (API) › AI drafts a Problem Card | ok | | 5 Flow (API) › a human approves and it is published | ok | | 5 Flow (API) › two follow-ups about the answer | ok | | 5 Flow (API) › submission returns a blind proof | ok | | 5 Flow (API) › Second look is off until the person turns it on | ok | | 5 Flow (API) › company A sees it in its own queue | ok | | 5 Flow (API) › company A passes with feedback, once | ok | | 5 Flow (API) › company B sees it through Second look | ok | | 5 Flow (API) › company C without shared skills cannot see or decide | ok | | 5 Flow (API) › company B picks Meet and a slot is set | ok | | 5 Flow (API) › identity hidden while invited | ok | | 5 Flow (API) › passport shows both responses | ok | | 5 Flow (API) › talent accepts and identity appears | ok | | 5 Flow (API) › both sides confirm the meeting | ok | | 5 Flow (API) › city totals change and show 0 automated decisions | ok | | 5 Flow (API) › places outside 5 to 30 are refused | ok | | 5 Flow (API) › the employer's number of places is published with the task | ok | | 5 Flow (API) › all five places take work | ok | | 5 Flow (API) › the sixth answer is refused with 409 and no waiting list | ok | | 5 Flow (API) › v3: a named reviewer scores the work and holds it, no meeting yet | ok | | 5 Flow (API) › v3: the scored proof is on the task's top list, blind | ok | | 5 Flow (API) › v3: the hold becomes an invitation | ok | | 5 Flow (API) › v3: the person accepts, and only then the company sees the name | ok | | 5 Flow (API) › v3: both sides confirm the talk | ok | | 5 Flow (API) › v3: a follow-up is agreed and a hire recorded by the company | ok | | 5 Flow (API) › v3: the city counts every step for the task, with no name | ok | | 6 Demo story (API) › session: persona and stage | Mikko Lahtinen, invited | | 6 Demo story (API) › unknown stage refused | HTTP 400 for an unknown stage | | 6 Demo story (API) › before and sent | 2 proofs before, 3 after sending; story proof queued: true | | 6 Demo story (API) › invited: open invitation from the story company | Halla Supply wants to meet you. | | 6 Demo story (API) › met: accepted, name shown, talk and follow-up counted | proofs met: 4 before, 5 after; follow-ups: 3 before, 4 after | | 6 Demo story (API) › persona hidden from employer and city | hidden | | 7 Table screen › no free text on the public screen | 0 of 10 names, emails and numbers found; extra fields: none | | 7 Table screen › name only after the person's yes | second invite HTTP 409; stranger read HTTP 403, accept HTTP 403; name on screen before yes: false, after: Mikko | | 7 Table screen › decline stores nothing | declined; a later accept HTTP 409; name on screen: false; event proof invite HTTP 409 | ## How to verify - `npm install`: Installs dependencies (Node.js 22 or newer). - `npm test`: Unit tests next to the code. - `npm run eval`: The eval suites in evals/; prints one row per check. - `npx tsc --noEmit`: Type check. - `npm run lint`: ESLint. - `npm run dev`: The app on http://localhost:3000. - `npm run eval:e2e`: Playwright through the real interface at 390x844; needs the app running. In the running app: - [/try](https://workproof.wicflow.com/try): Do a 2-minute task on a phone and see the blind proof a company would read. - [/table](https://workproof.wicflow.com/table): Invite that proof with a two-sentence reason, then accept on the phone: only now the name shows on the big screen. - [/city](https://workproof.wicflow.com/city): See what came of the event, from challenges taken to scores, invitations and talks, and the work still waiting. ## Pages - [2-minute task](https://workproof.wicflow.com/try): Do a short task on a phone and see the blind proof a company would read - [Job seeker](https://workproof.wicflow.com/): Mikko's home, the first screen: invitation, open challenges, his work - [Challenges](https://workproof.wicflow.com/challenges): Every open challenge at the demo event, in four formats: Choose, Make, Fix, Read - [My work](https://workproof.wicflow.com/my-work): The person's own work, what was hidden, the reviewer's scores and reason, and a portfolio to share - [Company](https://workproof.wicflow.com/employer): Halla Supply's challenges, each with its top work by the reviewers' scores and the work waiting in arrival order - [New challenge](https://workproof.wicflow.com/company): A job-ad link or one sentence becomes a challenge with 3 to 5 core skills; a person approves it - [Score work](https://workproof.wicflow.com/company/review): Read the work without the name, give 1 to 5 points per skill with a written reason, then invite or not now - [City of Espoo](https://workproof.wicflow.com/city): Outcomes, the Espoo flow map, process safeguards and audit log - [Table mode](https://workproof.wicflow.com/table): Big screen with a QR code: a phone joins the same demo event - [Evidence for judges](https://workproof.wicflow.com/for-judges): Criteria, evidence, evals and limits - [How it works](https://workproof.wicflow.com/how-it-works): the product in six plain steps - [Short map](https://workproof.wicflow.com/llms.txt): the same facts, shorter ## Limits - Company and city actions have no sign-in. Any caller can act as any company or as the city in the demo; a pilot needs sign-in and ownership checks. - Pictures are not analysed. The person confirms that the picture shows only the work, and a reviewer can report one; a reported picture is removed for every reader. Nothing checks what a picture shows automatically. - AI-written work is not solved. Our untested assumption: follow-up answers about a person's own choices, and a meeting that starts from the work, make AI-only answers easy to notice. This is not measured yet. The product cannot prove who typed an answer. - Impact is not measured yet. Every outcome in the demo is invented data; the first real test is a pilot event. - Reviewing takes employer time. A task takes 5 to 30 answers, but reading still takes a person's time. - Nothing forces an employer to answer. Every recorded decision needs a written reason, and unanswered work stays visible to the city. - The blind layer is rules, not a guarantee. It misses a lowercase name with no cue word, a bare name with no introduction, another person's name, another spelling of the author's name, and part of an employer name that is not on the list. It over-hides a one-word capitalised sign-off and a capitalised value after "name:" outside a code block. - Video upload is off in the demo; seed proofs carry sample clips, visitors answer in writing, by voice or with a photo. - No ESCO skill ids yet; skills come from the project's own vocabulary. - No Tavata or Brella integration. The meeting list exports as a CSV file; nothing connects to an event tool directly. - The demo store is in memory, one private copy per visitor, reset after 12 hours. - Live AI runs only with an API key; without one the scripted task designer is used.