Digisky
Organisational psychometrics2026 · 141 commits over roughly 14 days

An assessment platform in 14 days

A multi-tenant assessment platform, wallet, scoring engine and reporting pipeline delivered in fourteen days, with every number in a report traceable to the key that produced it.

The problem

An organisation wanting to run a structured personality assessment across its staff needs far more than a questionnaire: tenancy, prepaid billing, assignment, a runner that survives an interrupted sitting, a scoring engine whose arithmetic can be re-derived years later, norm handling with provenance, and a report a person can read about themselves without being harmed by it. Most of that is invisible, and all of it is where the failures live.

141 · ~14 daysCommits · elapsed
~103,500 lines · 807Backend PHP · files
56 · 61Migrations · database tables
1,301Backend test cases
15Frontend test files
240 items · 5 domains · 30 facetsInstrument shape

Fourteen days, and most of it is not the questionnaire

Commits · elapsed 141 · ~14 days
Backend PHP · files ~103,500 lines · 807
Migrations · tables 56 · 61
Backend test cases 1,301
Frontend test files 15
Instrument 240 items · 5 domains · 30 facets

What sixty-one tables buys, beyond a form: multi-organisation tenancy where a cross-tenant read returns nothing rather than depending on a filter someone remembered to write; a prepaid wallet with a ledger; assignment to individuals and to groups; a runner that autosaves and resumes; a scoring engine with no database access at all, so it can be re-run on stored responses years later; norm handling that records which reference population produced each standard score; and a report rendered server-side as a PDF with the fonts embedded, because Persian needs contextual letter shaping and a renderer that does not do it produces disconnected, reversed letters that are legible to nobody.

The rule the whole system is built around

Every number in a report must be explainable and reproducible years later.

That sentence is written once in the architecture decision record and it forces most of the rest.

It is why the scoring engine is pure — it takes a test version and a set of responses and returns raw scores, touching nothing else. It is why a result is immutable and bound to the version it was scored under, so a stored report still renders under its own version’s bands after the instrument has been revised. It is why every standard score records where it came from: an exact table row, an interpolated one, or a mean and standard deviation. “The norm table probably had that row in 2026” is not an answer to a candidate’s lawyer.

"Someone is going to challenge this result in three years. Can you show me exactly how the number was produced?"

The audit that found the key was wrong

The strongest piece of work in this project is also the least flattering, which is usually how it goes.

The scoring key — which item counts toward which facet, and in which direction — was audited by reading what every one of the 240 Persian items actually says and comparing that to the direction it was keyed. Thirty-six items were keyed against the direction of their own text.

The worked example from the audit says it better than a summary can: a respondent answering strongly agree to “I am a very disciplined person” was scoring zero on that item for self-discipline instead of the maximum.

Two further items turned out to be translation defects that negate the item they render. They are keyed to the original meaning, and the cost of that — a respondent answering the Persian sentence is scored against its opposite on those two items — is written down rather than smoothed over, because fixing the wording changes a published instrument and detaches those items from the reference data collected on them.

The regression test that now guards this asserts the reverse-keyed items by number, not by any tidy rule. That detail is the whole lesson: the two earlier keys both passed the checks they were given, and were wrong anyway, because the checks were satisfiable by a pattern that looked orderly.

Where the model is allowed to go

The boundary is stated to users on the product’s own landing page rather than buried, on the reasoning that a buyer who discovers it later assumes it was hidden.

  1. The engine scores. It applies the version's recorded key, handles reverse-keyed items, produces the domain and facet scores, and determines each band.
  2. The model explains. It receives computed scores — never answers, never a name, never anything about the employer — and writes a plain-language reading of them.
  3. The model changes nothing. It cannot alter a score, invent a trait, give a diagnosis, or make a hiring decision.
  4. The system checks the output. Chart references the version does not have are dropped, importance values are clamped, malformed output degrades to prose rather than to an error, and clinically loaded terms the model used get their definitions appended server-side.

That last step exists because of a real incident. The prompt asked the model not to write its own definitions of clinically loaded facet names, and a live run produced exactly those words with no definition anywhere and no statement that neither is a diagnosis. A prompt is a request. So the definitions were moved into the payload as data, and a post-hoc scanner appends the glossary whether the model cooperated or not.

And what the model does not have

There is no human review of the generated interpretation. No approval state, no moderation queue — the analysis is written and the person is notified. Administrators can look afterwards and regenerate or clear it. That is observation, not review, and it is a real gap in a product that writes about people.

The gap we will not paper over

The system has no response-quality screening. The team’s own planning document states it in one sentence: a test answered “agree” 240 times currently produces a confident, wrong profile.

The indicators that would catch it — longest straight run, response variability, distinct options used, and a validity flag shown above everything else on the result page — are designed in detail and are not in the database. The only control actually running is a minimum-answered ratio.

This is the most important thing to know about the platform in its current state, so it is on this page twice: here, and in the limitations at the top.

Decisions

A domain is scored from its own items, never from the sum of its facets.

Alternatives: Sum the facet scores to produce the domain score

Summing facets looks equivalent and is not. An item can load onto a facet, onto its parent domain, onto both, or onto neither, each with its own weight and direction. Adding the facets double-counts every multi-loaded item and silently discards the weights. Both routes are computed and a validator raises when they disagree — silence means they agree, which is a much stronger statement than never having checked.

Item structure comes from the uploaded key as data, never from arithmetic on item numbers.

Alternatives: Derive domain and facet from item position with a modulo formula, Hard-code the reverse-scored item numbers in an array

Both alternatives were proposed and rejected in writing, for the same reason: an off-by-one anywhere produces a complete, plausible, wrong profile with nothing in the system able to detect it. The key is data, validated on upload against the declared number of facets per domain and items per facet, and editable by an administrator without a code change.

Reverse scoring is (minimum + maximum) − response, against the answer set's own range.

Alternatives: 4 − response, 6 − response

Both constants are correct exactly once, for one scale. Computing the reflection from the answer set's declared range means a five-point scale numbered one to five and a five-point scale numbered zero to four both score correctly, with no code change and no second implementation to keep in step.

A partly answered sitting is warned about, recorded, flagged, and scored anyway.

Alternatives: Halt scoring when more than a threshold of items are missing

Halting destroys the record of a test somebody actually sat. The runner refuses the first submission, shows only the blanks, and a second submission with an explicit acknowledgement is stored and scored with the flag attached. The only response-quality control actually running is a minimum-answered ratio of 0.75, which is stated in the limitations below because it is nowhere near sufficient.

Interpretation bands are half-open decimal intervals, tagged with the scale they apply to.

Alternatives: Inclusive integer bands

Inclusive integer bands leave holes. A score of 44.5 belonged to no band and rendered with no label at all — a report with a blank where an interpretation should be. Half-open decimal intervals tile the range with no gaps, a validator rejects a band set that leaves one, and each band declares whether it applies to a raw score, a standard score or a percentile so the two can never be read against each other.

The language model explains scores. It never produces them, and never sees who produced them.

Alternatives: Let the model read responses and characterise the person, Ask the model in the prompt to define its own terms

The scoring engine is authoritative and the model receives only computed scores — never the person's answers, name or contact details, and deliberately never anything about their employer, because a reading bent toward what an employer is currently worried about is no longer a reading of the person. Asking the model to define clinically loaded facet names failed in a live run: it produced the terms with no definition and no statement that they were not diagnoses. So the definitions travel with the scores as data, and a scanner appends the glossary server-side after the model answers, whether the model cooperated or not.

Limitations

  • There is no response-quality or validity screening. The team's own plan states the problem in writing: a test answered with the same option 240 times currently produces a confident, wrong profile. Straight-line detection, response variability and distinct-option counts are designed and not built. The only control running is a minimum-answered ratio of 0.75.
  • No reliability or validity coefficients are computed, displayed or claimed. Nothing in this system tells you how internally consistent a scale is on your population.
  • There is no test-retest or reliable-change scoring, because that needs test-retest reliability data the platform does not hold, and presenting a raw difference between two sittings as 'change' without it would be the same class of error the scoring rules exist to prevent.
  • There is no human review of model-generated interpretations. No approval state, no moderation queue. The analysis is generated and the person is notified. Administrators can observe after the fact and regenerate or clear, and that is not the same thing as review.
  • Facets are reported with a raw score, a standard score and a percentile, and no interpretation band, because the source material bands domains only and inventing facet bands would be fabricated interpretation.
  • Respondent sex is not collected, so the pooled norm group is always used even where sex-specific reference statistics exist in the table.
  • The instrument's licensing position is an open question that belongs to the product owner and is recorded as unresolved. Until it is settled it constrains what may be published about the instrument, and this page is written to that constraint.
  • The live database is a seeded demonstration environment, not a customer base. The numbers on this page describe engineering output and system capacity. They are not adoption figures and must not be read as any.