Skip to content

Commit c5daaa2

Browse files
authored
Merge pull request #174 from hypercerts-org/actor-new-props
NON-BREAKING: add optional longDescription and visibility fields to organization
2 parents 7016442 + c456cda commit c5daaa2

7 files changed

Lines changed: 141 additions & 16 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hypercerts-org/lexicon": minor
3+
---
4+
5+
Add optional `longDescription` and `visibility` fields to `app.certified.actor.organization` lexicon. `longDescription` uses the description union pattern (inline text/markdown or strongRef to a rich-text document) for consistency with activity, collection, and attachment.

ERD.puml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ dataclass organization {
5050
urls[]?
5151
location?
5252
foundedDate?
53+
longDescription?
54+
visibility?
5355
createdAt
5456
!endif
5557
}

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -266,15 +266,15 @@ await agent.api.com.atproto.repo.createRecord({
266266

267267
### Certified (`app.certified.*`)
268268

269-
| Lexicon | NSID | Description |
270-
| -------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
271-
| **Location** | `app.certified.location` | Geographic reference using the [Location Protocol](https://spec.decentralizedgeo.org) (coordinates, GeoJSON, H3, WKT, etc.). |
272-
| **Profile** | `app.certified.actor.profile` | User account profile with display name, bio, avatar, and banner. |
273-
| **Organization** | `app.certified.actor.organization` | Organization metadata: legal structure, URLs, location, founding date. |
274-
| **Badge Definition** | `app.certified.badge.definition` | Defines a badge with type, title, icon, and optional issuer allowlist. |
275-
| **Badge Award** | `app.certified.badge.award` | Awards a badge to a user, project, or activity. |
276-
| **Badge Response** | `app.certified.badge.response` | Recipient accepts or rejects a badge award. |
277-
| **EVM Link** | `app.certified.link.evm` | Verifiable ATProto DID ↔ EVM wallet link via EIP-712 signature. Extensible for future proof methods (e.g. ERC-1271, ERC-6492). |
269+
| Lexicon | NSID | Description |
270+
| -------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
271+
| **Location** | `app.certified.location` | Geographic reference using the [Location Protocol](https://spec.decentralizedgeo.org) (coordinates, GeoJSON, H3, WKT, etc.). |
272+
| **Profile** | `app.certified.actor.profile` | User account profile with display name, bio, avatar, and banner. |
273+
| **Organization** | `app.certified.actor.organization` | Organization metadata: legal structure, URLs, location, founding date, optional long description, and discoverability visibility. |
274+
| **Badge Definition** | `app.certified.badge.definition` | Defines a badge type with title, icon, and optional issuer allowlist. |
275+
| **Badge Award** | `app.certified.badge.award` | Awards a badge to a user, project, or activity. |
276+
| **Badge Response** | `app.certified.badge.response` | Recipient accepts or rejects a badge award. |
277+
| **EVM Link** | `app.certified.link.evm` | Verifiable ATProto DID ↔ EVM wallet link via EIP-712 signature. Extensible for future proof methods (e.g. ERC-1271, ERC-6492). |
278278

279279
> **Full property tables**[SCHEMAS.md](SCHEMAS.md)
280280

SCHEMAS.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -424,13 +424,15 @@ A location represented as a string, e.g. coordinates or a small GeoJSON string.
424424

425425
#### Properties
426426

427-
| Property | Type | Required | Description | Comments |
428-
| ------------------ | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
429-
| `organizationType` | `string[]` || Legal or operational structures of the organization (e.g. 'nonprofit', 'ngo', 'government', 'social-enterprise', 'cooperative'). | maxLength: 10 |
430-
| `urls` | `ref[]` || Additional reference URLs (social media profiles, contact pages, donation links, etc.) with a display label for each URL. | |
431-
| `location` | `ref` || A strong reference to the location where the organization is based. The record referenced must conform with the lexicon app.certified.location. | |
432-
| `foundedDate` | `string` || When the organization was established. Stored as datetime per ATProto conventions (no date-only format exists). Clients should use midnight UTC (e.g., '2005-01-01T00:00:00.000Z'); consumers should treat only the date portion as canonical. | |
433-
| `createdAt` | `string` || Client-declared timestamp when this record was originally created. | |
427+
| Property | Type | Required | Description | Comments |
428+
| ------------------ | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
429+
| `organizationType` | `string[]` || Legal or operational structures of the organization (e.g. 'nonprofit', 'ngo', 'government', 'social-enterprise', 'cooperative'). | maxLength: 10 |
430+
| `urls` | `ref[]` || Additional reference URLs (social media profiles, contact pages, donation links, etc.) with a display label for each URL. | |
431+
| `location` | `ref` || A strong reference to the location where the organization is based. The record referenced must conform with the lexicon app.certified.location. | |
432+
| `foundedDate` | `string` || When the organization was established. Stored as datetime per ATProto conventions (no date-only format exists). Clients should use midnight UTC (e.g., '2005-01-01T00:00:00.000Z'); consumers should treat only the date portion as canonical. | |
433+
| `longDescription` | `union` || Long-form description of the organization, such as its mission, history, or detailed project narrative. An inline string for plain text or markdown, a Leaflet linear document record embedded directly, or a strong reference to an existing document record. | |
434+
| `visibility` | `string` || Controls whether the organization or project is publicly discoverable on platforms that honor this setting. | Known values: `public`, `unlisted` |
435+
| `createdAt` | `string` || Client-declared timestamp when this record was originally created. | |
434436

435437
#### Defs
436438

lexicons/app/certified/actor/organization.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@
3838
"format": "datetime",
3939
"description": "When the organization was established. Stored as datetime per ATProto conventions (no date-only format exists). Clients should use midnight UTC (e.g., '2005-01-01T00:00:00.000Z'); consumers should treat only the date portion as canonical."
4040
},
41+
"longDescription": {
42+
"type": "union",
43+
"refs": [
44+
"org.hypercerts.defs#descriptionString",
45+
"pub.leaflet.pages.linearDocument#main",
46+
"com.atproto.repo.strongRef"
47+
],
48+
"description": "Long-form description of the organization, such as its mission, history, or detailed project narrative. An inline string for plain text or markdown, a Leaflet linear document record embedded directly, or a strong reference to an existing document record."
49+
},
50+
"visibility": {
51+
"type": "string",
52+
"knownValues": ["public", "unlisted"],
53+
"description": "Controls whether the organization or project is publicly discoverable on platforms that honor this setting."
54+
},
4155
"createdAt": {
4256
"type": "string",
4357
"format": "datetime",

scripts/check-lexicon-style.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,18 @@ class StyleChecker {
608608

609609
// Check if this is an external ref (e.g., "com.atproto.repo.strongRef" or "org.hypercerts.defs#uri")
610610
if (typeof ref === "string" && !ref.startsWith("#")) {
611+
// Known-external lexicon namespaces are not present in the local lexicons/ directory.
612+
// Skip resolution for these — they are valid by convention (the same namespaces skipped
613+
// during file-level style checks).
614+
const knownExternalPrefixes = [
615+
"pub.leaflet.",
616+
"app.bsky.",
617+
"com.atproto.",
618+
];
619+
if (knownExternalPrefixes.some((prefix) => ref.startsWith(prefix))) {
620+
return;
621+
}
622+
611623
const resolvedDef = this.resolveExternalRef(ref);
612624
if (resolvedDef) {
613625
if (!resolvedDef.type) {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, it, expect } from "vitest";
2+
import { validate, ids } from "../generated/lexicons.js";
3+
import * as Organization from "../generated/types/app/certified/actor/organization.js";
4+
5+
describe("app.certified.actor.organization", () => {
6+
it("should accept a minimal valid record (only required createdAt)", () => {
7+
const result = Organization.validateMain({
8+
$type: ids.AppCertifiedActorOrganization,
9+
createdAt: "2024-01-01T00:00:00.000Z",
10+
});
11+
expect(result.success).toBe(true);
12+
});
13+
14+
it("should accept record with visibility set to 'public'", () => {
15+
const result = Organization.validateMain({
16+
$type: ids.AppCertifiedActorOrganization,
17+
visibility: "public",
18+
createdAt: "2024-01-01T00:00:00.000Z",
19+
});
20+
expect(result.success).toBe(true);
21+
if (result.success) {
22+
expect(result.value.visibility).toBe("public");
23+
}
24+
});
25+
26+
it("should accept record with visibility set to 'unlisted'", () => {
27+
const result = Organization.validateMain({
28+
$type: ids.AppCertifiedActorOrganization,
29+
visibility: "unlisted",
30+
createdAt: "2024-01-01T00:00:00.000Z",
31+
});
32+
expect(result.success).toBe(true);
33+
if (result.success) {
34+
expect(result.value.visibility).toBe("unlisted");
35+
}
36+
});
37+
38+
it("should accept record with inline longDescription (descriptionString)", () => {
39+
const result = Organization.validateMain({
40+
$type: ids.AppCertifiedActorOrganization,
41+
longDescription: {
42+
$type: "org.hypercerts.defs#descriptionString",
43+
value: "Our mission is to restore mangrove ecosystems worldwide.",
44+
},
45+
createdAt: "2024-01-01T00:00:00.000Z",
46+
});
47+
expect(result.success).toBe(true);
48+
});
49+
50+
it("should accept record with all optional fields populated", () => {
51+
const result = Organization.validateMain({
52+
$type: ids.AppCertifiedActorOrganization,
53+
organizationType: ["nonprofit", "ngo"],
54+
urls: [{ url: "https://example.org", label: "Website" }],
55+
foundedDate: "2010-01-01T00:00:00.000Z",
56+
longDescription: {
57+
$type: "org.hypercerts.defs#descriptionString",
58+
value: "Full org description in markdown.",
59+
},
60+
visibility: "public",
61+
createdAt: "2024-01-01T00:00:00.000Z",
62+
});
63+
expect(result.success).toBe(true);
64+
});
65+
66+
it("should reject record missing required createdAt", () => {
67+
const result = validate(
68+
{ organizationType: ["nonprofit"] },
69+
ids.AppCertifiedActorOrganization,
70+
"main",
71+
false,
72+
);
73+
expect(result.success).toBe(false);
74+
if (!result.success) {
75+
expect(result.error).toBeDefined();
76+
}
77+
});
78+
79+
it("should reject record with invalid createdAt format", () => {
80+
const result = validate(
81+
{
82+
createdAt: "not-a-datetime",
83+
},
84+
ids.AppCertifiedActorOrganization,
85+
"main",
86+
false,
87+
);
88+
expect(result.success).toBe(false);
89+
});
90+
});

0 commit comments

Comments
 (0)