Compare commits
1 Commits
master
...
feature/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
d221594a0a
|
343
doc/nostr/nip-place-reviews.md
Normal file
343
doc/nostr/nip-place-reviews.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
# NIP-XX: Place Reviews
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
|
This NIP defines a standardized event format for decentralized place reviews using Nostr. Reviews are tied to real-world locations (e.g. OpenStreetMap POIs) via tags, and include structured, multi-aspect ratings, a binary recommendation signal, and optional contextual metadata.
|
||||||
|
|
||||||
|
The design prioritizes:
|
||||||
|
|
||||||
|
* Small event size
|
||||||
|
* Interoperability across clients
|
||||||
|
* Flexibility for different place types
|
||||||
|
* Efficient geospatial querying using geohashes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event Kind
|
||||||
|
|
||||||
|
`kind: 30315` (suggested; subject to coordination)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
Additional tags MAY be included by clients but are not defined by this specification.
|
||||||
|
|
||||||
|
This NIP reuses and builds upon existing Nostr tag conventions:
|
||||||
|
|
||||||
|
* `i` tag: see NIP-73 (External Content Identifiers)
|
||||||
|
* `g` tag: geohash-based geotagging (community conventions)
|
||||||
|
|
||||||
|
Where conflicts arise, this NIP specifies the behavior for review events.
|
||||||
|
|
||||||
|
### Required
|
||||||
|
|
||||||
|
#### `i` — Entity Identifier
|
||||||
|
|
||||||
|
Identifies the reviewed place using an external identifier. OpenStreetMap data is the default:
|
||||||
|
|
||||||
|
```
|
||||||
|
["i", "osm:<type>:<id>"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
* For OSM POIs, `<type>` MUST be one of: `node`, `way`, `relation`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
["i", "osm:node:123456"]
|
||||||
|
["i", "osm:way:987654"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Geospatial Tags
|
||||||
|
|
||||||
|
#### `g` — Geohash
|
||||||
|
|
||||||
|
Geohash tags are used for spatial indexing and discovery.
|
||||||
|
|
||||||
|
##### Requirements
|
||||||
|
|
||||||
|
* Clients MUST include at least one high-precision geohash (length ≥ 9)
|
||||||
|
|
||||||
|
##### Recommendations
|
||||||
|
|
||||||
|
Clients SHOULD include geohashes at the following resolutions:
|
||||||
|
|
||||||
|
* length 4 — coarse (city-scale discovery)
|
||||||
|
* length 6 — medium (default query level, ~1 km)
|
||||||
|
* length 7 — fine (neighborhood, ~150 m)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
["g", "thrr"]
|
||||||
|
["g", "thrrn5"]
|
||||||
|
["g", "thrrn5k"]
|
||||||
|
["g", "thrrn5kxyz"]
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Querying
|
||||||
|
|
||||||
|
Geospatial queries are performed using the `g` tag.
|
||||||
|
|
||||||
|
* Clients SHOULD query using a single geohash precision level per request
|
||||||
|
* Clients MAY include multiple geohash values in a filter to cover a bounding box
|
||||||
|
* Clients SHOULD limit the number of geohash values per query (e.g. ≤ 30)
|
||||||
|
* Clients MAY reduce precision or split queries when necessary
|
||||||
|
|
||||||
|
Note: Other queries (e.g. fetching reviews for a specific place) are performed using the `i` tag and are outside the scope of geospatial querying.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Content (JSON)
|
||||||
|
|
||||||
|
The event `content` MUST be valid JSON matching the following schema.
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "ratings"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"ratings": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["quality"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"quality": { "$ref": "#/$defs/score" },
|
||||||
|
"value": { "$ref": "#/$defs/score" },
|
||||||
|
"experience": { "$ref": "#/$defs/score" },
|
||||||
|
"accessibility": { "$ref": "#/$defs/score" },
|
||||||
|
"aspects": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 1,
|
||||||
|
"maxProperties": 20,
|
||||||
|
"additionalProperties": { "$ref": "#/$defs/score" },
|
||||||
|
"propertyNames": {
|
||||||
|
"pattern": "^[a-z][a-z0-9_]{1,31}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recommend": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"familiarity": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["low", "medium", "high"],
|
||||||
|
"description": "User familiarity: low = first visit; medium = occasional; high = frequent"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"visited_at": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1440
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1000
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z]{2}(-[A-Z]{2})?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"score": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
### Restaurant Review Event
|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
|
||||||
|
```
|
||||||
|
[
|
||||||
|
["i", "osm:node:123456"],
|
||||||
|
["g", "thrr"],
|
||||||
|
["g", "thrrn5"],
|
||||||
|
["g", "thrrn5k"],
|
||||||
|
["g", "thrrn5kxyz"]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Content
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"ratings": {
|
||||||
|
"quality": 9,
|
||||||
|
"value": 8,
|
||||||
|
"experience": 9,
|
||||||
|
"accessibility": 7,
|
||||||
|
"aspects": {
|
||||||
|
"food": 9,
|
||||||
|
"service": 6,
|
||||||
|
"ambience": 8,
|
||||||
|
"wait_time": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recommend": true,
|
||||||
|
"familiarity": "medium",
|
||||||
|
"context": {
|
||||||
|
"visited_at": 1713200000,
|
||||||
|
"duration_minutes": 90,
|
||||||
|
"party_size": 2
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"text": "Excellent food with bold flavors. Service was a bit slow, but the atmosphere made up for it.",
|
||||||
|
"language": "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Semantics
|
||||||
|
|
||||||
|
### Ratings
|
||||||
|
|
||||||
|
* Scores are integers from 1 to 10
|
||||||
|
* `quality` is required and represents the core evaluation of the place
|
||||||
|
* Other fields are optional and context-dependent
|
||||||
|
|
||||||
|
### Aspects
|
||||||
|
|
||||||
|
* Free-form keys allow domain-specific ratings
|
||||||
|
* Clients MAY define and interpret aspect keys
|
||||||
|
* Clients SHOULD reuse commonly established aspect keys where possible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendation Signal
|
||||||
|
|
||||||
|
The `recommend` field represents a binary verdict:
|
||||||
|
|
||||||
|
* `true` → user recommends the place
|
||||||
|
* `false` → user does not recommend the place
|
||||||
|
|
||||||
|
Clients SHOULD strongly encourage users to provide this value.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Familiarity
|
||||||
|
|
||||||
|
Represents user familiarity with the place:
|
||||||
|
|
||||||
|
* `low` → first visit or limited exposure
|
||||||
|
* `medium` → occasional visits
|
||||||
|
* `high` → frequent or expert-level familiarity
|
||||||
|
|
||||||
|
Clients MAY use this signal for weighting during aggregation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Optional metadata about the visit.
|
||||||
|
|
||||||
|
* `visited_at` is a Unix timestamp
|
||||||
|
* `duration_minutes` represents time spent
|
||||||
|
* `party_size` indicates group size
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interoperability
|
||||||
|
|
||||||
|
This specification defines a content payload only.
|
||||||
|
|
||||||
|
* In Nostr: place identity is conveyed via tags
|
||||||
|
* In other protocols (e.g. ActivityPub, AT Protocol): identity MUST be mapped to the equivalent field (e.g. `object`)
|
||||||
|
|
||||||
|
Content payloads SHOULD NOT include place identifiers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
### No Place Field in Content
|
||||||
|
|
||||||
|
Avoids duplication and inconsistency with tags.
|
||||||
|
|
||||||
|
### Multi-Aspect Ratings
|
||||||
|
|
||||||
|
Separates concerns (e.g. quality vs service), improving signal quality.
|
||||||
|
|
||||||
|
### Recommendation vs Score
|
||||||
|
|
||||||
|
Binary recommendation avoids averaging pitfalls and improves ranking.
|
||||||
|
|
||||||
|
### Familiarity
|
||||||
|
|
||||||
|
Provides a human-friendly proxy for confidence without requiring numeric input.
|
||||||
|
|
||||||
|
### Geohash Strategy
|
||||||
|
|
||||||
|
Multiple resolutions balance:
|
||||||
|
|
||||||
|
* efficient querying
|
||||||
|
* small event size
|
||||||
|
* early-stage discoverability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
* Standardized aspect vocabularies
|
||||||
|
* Reputation and weighting models
|
||||||
|
* Indexing/aggregation services
|
||||||
|
* Cross-protocol mappings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
* Clients SHOULD validate all input
|
||||||
|
* Malicious or spam reviews may require external moderation or reputation systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
This NIP is public domain.
|
||||||
6
doc/nostr/notes.md
Normal file
6
doc/nostr/notes.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Notes
|
||||||
|
|
||||||
|
- NIP-73 for external IDs ("osm:node:123456"): https://github.com/nostr-protocol/nips/blob/744bce8fcae0aca07b936b6662db635c8b4253dd/73.md
|
||||||
|
- Places NIP-XX draft PR: https://github.com/nostr-protocol/nips/pull/927
|
||||||
|
- NPM package for generating multi-resolution geotags: https://sandwichfarm.github.io/nostr-geotags/#md:nostr-geotags
|
||||||
|
- AppleSauce docs for AI agents: https://applesauce.build/introduction/mcp-server.html
|
||||||
251
doc/nostr/ranking.md
Normal file
251
doc/nostr/ranking.md
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
# Ranking Algorithm
|
||||||
|
|
||||||
|
Your inputs:
|
||||||
|
|
||||||
|
* many users
|
||||||
|
* partial ratings
|
||||||
|
* different priorities
|
||||||
|
|
||||||
|
Your output:
|
||||||
|
|
||||||
|
> “Best place *for this user right now*”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Normalize scores
|
||||||
|
|
||||||
|
Convert 1–10 → 0–1:
|
||||||
|
|
||||||
|
```text
|
||||||
|
normalized_score = (score - 1) / 9
|
||||||
|
```
|
||||||
|
|
||||||
|
Why:
|
||||||
|
|
||||||
|
* easier math
|
||||||
|
* comparable across aspects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Per-aspect aggregation (avoid averages trap)
|
||||||
|
|
||||||
|
Instead of mean, compute:
|
||||||
|
|
||||||
|
### A. Positive ratio
|
||||||
|
|
||||||
|
```text
|
||||||
|
positive = score >= 7
|
||||||
|
negative = score <= 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```text
|
||||||
|
positive_ratio = positive_votes / total_votes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Confidence-weighted score
|
||||||
|
|
||||||
|
Use something like a **Wilson score interval** (this is key):
|
||||||
|
|
||||||
|
* prevents small-sample abuse
|
||||||
|
* avoids “1 review = #1 place”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Build aspect scores
|
||||||
|
|
||||||
|
For each aspect:
|
||||||
|
|
||||||
|
```text
|
||||||
|
aspect_score = f(
|
||||||
|
positive_ratio,
|
||||||
|
confidence,
|
||||||
|
number_of_reviews
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can approximate with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
aspect_score = positive_ratio * log(1 + review_count)
|
||||||
|
```
|
||||||
|
|
||||||
|
(Simple, works surprisingly well)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: User preference weighting
|
||||||
|
|
||||||
|
User defines:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"quality": 0.5,
|
||||||
|
"value": 0.2,
|
||||||
|
"service": 0.2,
|
||||||
|
"speed": 0.1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```text
|
||||||
|
final_score = Σ (aspect_score × weight)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Context filtering (this is your unfair advantage)
|
||||||
|
|
||||||
|
Filter reviews before scoring:
|
||||||
|
|
||||||
|
* time-based:
|
||||||
|
|
||||||
|
* “last 6 months”
|
||||||
|
* context-based:
|
||||||
|
|
||||||
|
* lunch vs dinner
|
||||||
|
* solo vs group
|
||||||
|
|
||||||
|
This is something centralized platforms barely do.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Reviewer weighting (later, but powerful)
|
||||||
|
|
||||||
|
Weight reviews by:
|
||||||
|
|
||||||
|
* consistency
|
||||||
|
* similarity to user preferences
|
||||||
|
* past agreement
|
||||||
|
|
||||||
|
This gives you:
|
||||||
|
|
||||||
|
> “people like you liked this”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Example end-to-end
|
||||||
|
|
||||||
|
### Raw reviews:
|
||||||
|
|
||||||
|
| User | Food | Service |
|
||||||
|
| ---- | ---- | ------- |
|
||||||
|
| A | 9 | 4 |
|
||||||
|
| B | 8 | 5 |
|
||||||
|
| C | 10 | 3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Derived:
|
||||||
|
|
||||||
|
* food → high positive ratio (~100%)
|
||||||
|
* service → low (~33%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User preferences:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"food": 0.8,
|
||||||
|
"service": 0.2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
→ ranks high
|
||||||
|
|
||||||
|
Another user:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"food": 0.3,
|
||||||
|
"service": 0.7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
→ ranks low
|
||||||
|
|
||||||
|
👉 Same data, different truth
|
||||||
|
That’s your killer feature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Critical design choices (don’t skip these)
|
||||||
|
|
||||||
|
## A. No global score in protocol
|
||||||
|
|
||||||
|
Let clients compute it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. Embrace incomplete data
|
||||||
|
|
||||||
|
Most reviews will have:
|
||||||
|
|
||||||
|
* 1–3 aspects only
|
||||||
|
|
||||||
|
That’s fine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C. Time decay (important)
|
||||||
|
|
||||||
|
Recent reviews should matter more:
|
||||||
|
|
||||||
|
```text
|
||||||
|
weight = e^(-λ × age)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D. Anti-gaming baseline
|
||||||
|
|
||||||
|
Even in nostr:
|
||||||
|
|
||||||
|
* spam will happen
|
||||||
|
|
||||||
|
Mitigation later:
|
||||||
|
|
||||||
|
* require minimum interactions
|
||||||
|
* reputation layers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. What you’ve built (zooming out)
|
||||||
|
|
||||||
|
This is not a review system.
|
||||||
|
|
||||||
|
It’s:
|
||||||
|
|
||||||
|
> A decentralized, multi-dimensional reputation graph for real-world places
|
||||||
|
|
||||||
|
That’s much bigger.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Next step (if you want to go deeper)
|
||||||
|
|
||||||
|
We can design:
|
||||||
|
|
||||||
|
### A. Query layer
|
||||||
|
|
||||||
|
* how clients fetch & merge nostr reviews efficiently
|
||||||
|
|
||||||
|
### B. Anti-spam / trust model
|
||||||
|
|
||||||
|
* web-of-trust
|
||||||
|
* staking / reputation
|
||||||
|
|
||||||
|
### C. OSM integration details
|
||||||
|
|
||||||
|
* handling duplicates
|
||||||
|
* POI identity conflicts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If I had to pick one next:
|
||||||
|
👉 **trust/reputation system** — because without it, everything you built *will* get gamed.
|
||||||
110
doc/nostr/ratings.md
Normal file
110
doc/nostr/ratings.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Canonical Aspect Vocabulary (v0.1)
|
||||||
|
|
||||||
|
You want a **soft standard**, not a rigid schema.
|
||||||
|
|
||||||
|
Think:
|
||||||
|
|
||||||
|
* “recommended keys” (clients SHOULD use)
|
||||||
|
* not “required keys” (protocol enforces)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. Core universal aspects (keep this small)
|
||||||
|
|
||||||
|
These should work for *any* place:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"quality", // core offering (food, repair, exhibits, etc.)
|
||||||
|
"value", // value for money/time
|
||||||
|
"experience", // comfort, usability, vibe
|
||||||
|
"accessibility" // ease of access, inclusivity
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why these work
|
||||||
|
|
||||||
|
* **quality** → your “product” abstraction (critical)
|
||||||
|
* **value** → universally meaningful signal
|
||||||
|
* **experience** → captures everything “soft”
|
||||||
|
* **accessibility** → often ignored but high utility
|
||||||
|
|
||||||
|
👉 Resist adding more. Every extra “universal” weakens the concept.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. Common cross-domain aspects (recommended pool)
|
||||||
|
|
||||||
|
Not universal, but widely reusable:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"service", // human interaction
|
||||||
|
"speed", // waiting time / turnaround
|
||||||
|
"cleanliness",
|
||||||
|
"safety",
|
||||||
|
"reliability",
|
||||||
|
"atmosphere"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
These apply to:
|
||||||
|
|
||||||
|
* restaurants, garages, clinics, parks, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C. Domain-specific examples (NOT standardized)
|
||||||
|
|
||||||
|
Let clients define freely:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"restaurant": ["food", "drinks"],
|
||||||
|
"bar": ["drinks", "music"],
|
||||||
|
"garage": ["work_quality", "honesty"],
|
||||||
|
"park": ["greenery", "amenities"],
|
||||||
|
"museum": ["exhibits", "crowding"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D. Key rule (this prevents chaos)
|
||||||
|
|
||||||
|
👉 **Aspect keys MUST be lowercase snake_case**
|
||||||
|
|
||||||
|
👉 **Meaning is defined socially, not technically**
|
||||||
|
|
||||||
|
To reduce fragmentation:
|
||||||
|
|
||||||
|
* publish a **public registry (GitHub repo)**
|
||||||
|
* clients can:
|
||||||
|
|
||||||
|
* suggest additions
|
||||||
|
* map synonyms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E. Optional normalization hint (important later)
|
||||||
|
|
||||||
|
Allow this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"aspect_aliases": {
|
||||||
|
"food": "quality",
|
||||||
|
"work_quality": "quality"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Not required, but useful for aggregation engines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Map familiarity in UI to:
|
||||||
|
|
||||||
|
* high: “I know this place well”
|
||||||
|
* medium: “Been a few times”
|
||||||
|
* low: “First visit”
|
||||||
92
doc/nostr/review-schema.json
Normal file
92
doc/nostr/review-schema.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.org/schemas/place-review.json",
|
||||||
|
"title": "Decentralized Place Review (Nostr/Event Content)",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "place", "ratings"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
|
||||||
|
"ratings": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["quality"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"quality": { "$ref": "#/$defs/score" },
|
||||||
|
"value": { "$ref": "#/$defs/score" },
|
||||||
|
"experience": { "$ref": "#/$defs/score" },
|
||||||
|
"accessibility": { "$ref": "#/$defs/score" },
|
||||||
|
|
||||||
|
"aspects": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 1,
|
||||||
|
"maxProperties": 20,
|
||||||
|
"additionalProperties": { "$ref": "#/$defs/score" },
|
||||||
|
"propertyNames": {
|
||||||
|
"pattern": "^[a-z][a-z0-9_]{1,31}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"recommend": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the user recommends this place to others"
|
||||||
|
},
|
||||||
|
|
||||||
|
"familiarity": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["low", "medium", "high"],
|
||||||
|
"description": "User familiarity with the place. Suggested interpretation: 'low' = first visit or very limited experience; 'medium' = visited a few times or moderate familiarity; 'high' = frequent visitor or strong familiarity."
|
||||||
|
},
|
||||||
|
|
||||||
|
"context": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"visited_at": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1440
|
||||||
|
},
|
||||||
|
"party_size": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"review": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1000
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z]{2}(-[A-Z]{2})?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"score": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user