Skip to content

feat(individuality): add the personhood read layer - #300

Merged
Imod7 merged 14 commits into
mainfrom
domi/individuality-read-layer
Aug 18, 2026
Merged

feat(individuality): add the personhood read layer#300
Imod7 merged 14 commits into
mainfrom
domi/individuality-read-layer

Conversation

@Imod7

@Imod7 Imod7 commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds @parity/product-sdk-individuality, a new published package that reads a person's personhood state on the individuality chain from a DotNS username, at one pinned finalized block. Ports the derivation and the read from humanity-spa.

Part of #286. Refs #287, does not close it. The eligibility half is held behind #291, and #291 is not the only gap: five items from the issue body sit outside this PR, mapped item by item in the issue analysis.

Two of those are deliberate divergences from the issue text rather than omissions, worth stating so a reviewer diffing the PR against the issue does not read them as oversights:

  • The Lite signal comes from PeopleLite.LitePeople presence, not Resources.Consumers(account).credibility. The issue names the latter. Both exist in the descriptor, so this is a choice: it follows the reference implementation in humanity-spa, which is the code this ports and the only version verified against the runtime. The alias is read from both pallets' AccountToAlias for the same reason.
  • A user is resolved by DotNS username only, not by account or product account. People.AccountToPersonalId is also not read. Both are additive and neither blocks the seven-state answer.

Changes

New package, product-sdk/packages/individuality/:

  • src/types.ts: the seven-state PersonhoodState union (NotEnrolled, Lite, Candidate, MembershipReady, Member, Caution, Suspended), wrapped by UsernameUnowned | Resolved, plus FinalizedSnapshot, AbsenceGracePolicy, PersonhoodParticipant and PersonhoodInputs. Imports nothing, so both other modules can sit on it.
  • src/derive.ts: the pure state machine, no I/O and no chain types. 15 tests.
  • src/decode.ts: raw PAPI values to domain shapes. Rejects unknown streak and recognition variants, and enforces the runtime's grace invariants (window <= 8, allowedMisses < window) so a wrong byte order fails loudly instead of making Caution unreachable. 19 tests.
  • src/read.ts: the batch. Pins one finalized block, resolves Resources.UsernameOwnerOf, runs six parallel reads, then a conditional alias-keyed Score.Participants lookup. Returns a Result. 18 tests.
  • src/errors.ts: ProductIndividualityError and IndividualityDecodeError, both implementing the SdkError marker. 3 tests.
  • src/index.ts: the public surface, exporting the derivation separately from the read.
  • package.json at 0.0.0 so the changeset publishes 0.1.0. Depends on @parity/product-sdk-errors, @parity/result and polkadot-api.
  • tsconfig.json, tsconfig.typecheck.json, tsup.config.ts, vitest.config.ts: copied from packages/statement-store.

Umbrella wiring, mirroring cloud-storage: dependency and ./individuality export in packages/sdk/package.json, the build entry in packages/sdk/tsup.config.ts, and packages/sdk/src/individuality/index.ts.

Also in packages/sdk: src/individuality/contract.test.ts, type-level assertions that a real getChainAPI client for both paseo and devnet still satisfies the read's structural chain type, plus a negative control that fails if that type ever stops constraining. The assertions gate CI through pnpm typecheck, and the one runtime test only stops vitest reporting an empty suite. It lives in the umbrella package rather than beside the type because the same assertions are vacuous inside the individuality package, see Risks.

Docs and release:

  • skills/product-sdk-individuality/SKILL.md: quick start, the state table, the traps.
  • README.md, product-sdk/README.md, CLAUDE.md: register the package and skill in the three hand-maintained lists.
  • pending-changesets/individuality-read-layer.md: minor for the package and the umbrella.

Why these changes

humanity-spa already had this logic, verified against the runtime, so anything else needing the same answer would have had to copy it. This lifts it into the SDK, keeps the derivation importable on its own for #291, and takes an already connected chain client so the caller picks the chain. That last choice matters beyond tidiness: the original plan resolved the chain itself, and with chain.getChainInfo unreachable at the pinned truapi version it called for a static chain table behind a seam. An injected client needs neither, which is why #298 landing requires no change here.

Testing

cd product-sdk
pnpm install
pnpm check
pnpm build
pnpm typecheck
pnpm -r --no-bail test

Before: the package did not exist on main, so nothing covered this. The source vectors lived in humanity-spa, 12 for the derivation and 18 for decode and assembly.

After: 55 tests pass in packages/individuality (15 derive, 19 decode, 18 read, 3 errors) and 19 in packages/sdk, up from 18 with the contract assertion. pnpm check clean over 296 files. pnpm build and pnpm typecheck clean.

Risks

  • Score.PersonhoodThreshold is a u8, and nothing can guard that. PAPI types u8 and u32 alike as number, so the wrong width typechecks and passes tests. Checked against the committed metadata, and commented at the point of use.
  • Score.AbsenceGraceRatio byte order is (allowed_misses, window). The metadata tuple is anonymous, so the order comes from the pallet doc comment. This one is guarded: the decode enforces allowedMisses < window, so a swapped order throws instead of silently making Caution unreachable.
  • Resources.UsernameOwnerOf is read in two places, here and at resolvePeopleUsernameOwner in packages/sdk/src/identity/dotns.ts. Deliberate: that one cannot pin a block and returns hex rather than the SS58String the next reads need.
  • The chain parameter is structural, not a descriptor. Its compile-time guard sits in packages/sdk because the same assertion is vacuous inside the individuality package, where the descriptor types do not fully resolve. Both halves were tested before choosing the placement.
  • Not an authorization oracle. A client-side read in a client-side library. A backend trusting "the SDK said Member" is trivially spoofed.
  • Chain targeting is unresolved. The package reads whichever chain the caller's getChainAPI(env) selected, which is the right layering but means this PR does not pick one.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

📦 Bundle size impact

Comparing 2026-08-18T10:10:54.219Z2026-08-18T10:10:49.633Z

Package Entry Bundled before Bundled after Δ Ship gzip Δ Shake ratio
🟢 @parity/product-sdk ./individuality new entry
🟢 @parity/product-sdk-individuality new package

Thresholds — warn: ≥10% or ≥5.0 KB · fail: ≥20% or ≥15.0 KB (bundled). Percentage only applies once the baseline is ≥ 10 KB.

Comment thread product-sdk/packages/individuality/src/derive.ts
Comment thread product-sdk/packages/individuality/src/read.ts
Comment thread product-sdk/packages/individuality/src/read.ts
Comment thread product-sdk/packages/individuality/src/decode.ts

@TarikGul TarikGul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All small nits, you can decide if they are important enough.
Nice job

@Imod7
Imod7 merged commit 31cf3d9 into main Aug 18, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants