@the-rfp-hub/standard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARTIFACTS.md +85 -0
- package/CHANGELOG.md +264 -0
- package/LICENSE +121 -0
- package/NORMATIVE.md +77 -0
- package/PROCESS.md +239 -0
- package/README.md +83 -0
- package/conformance/README.md +52 -0
- package/conformance/v1.0.0/fail/deadline-fixed-date-null.json +22 -0
- package/conformance/v1.0.0/fail/deadline-fixed-without-date.json +21 -0
- package/conformance/v1.0.0/fail/eligibility-value-not-string.json +21 -0
- package/conformance/v1.0.0/fail/empty-sponsoring-organizations.json +11 -0
- package/conformance/v1.0.0/fail/missing-matching-type-block.json +14 -0
- package/conformance/v1.0.0/fail/missing-required-properties.json +5 -0
- package/conformance/v1.0.0/fail/missing-sponsoring-organizations.json +10 -0
- package/conformance/v1.0.0/fail/one-block-per-fundingtype.json +18 -0
- package/conformance/v1.0.0/fail/source-url-removed.json +17 -0
- package/conformance/v1.0.0/fail/status-not-in-enum.json +15 -0
- package/conformance/v1.0.0/fail/unknown-top-level-property.json +16 -0
- package/conformance/v1.0.0/fail/wrong-fundingtype-value.json +14 -0
- package/conformance/v1.0.0/fail/wrong-specversion.json +15 -0
- package/conformance/v1.0.0/pass/deadline-rolling-without-date.json +23 -0
- package/conformance/v1.0.0/pass/full-featured.json +137 -0
- package/conformance/v1.0.0/pass/grant-funding-mechanisms-array.json +27 -0
- package/conformance/v1.0.0/pass/grant.json +45 -0
- package/conformance/v1.0.0/pass/hackathon.json +67 -0
- package/conformance/v1.0.0/pass/minimal-required-only.json +15 -0
- package/conformance/v1.0.0/pass/self-identification.json +18 -0
- package/conformance/v1.0.0/pass/source-empty-object.json +30 -0
- package/dist/index.cjs +902 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +661 -0
- package/dist/index.d.ts +661 -0
- package/dist/index.js +896 -0
- package/dist/index.js.map +1 -0
- package/meta/rfphub-schema.meta.json +131 -0
- package/package.json +56 -0
- package/registries/deadline-labels.json +38 -0
- package/registries/eligibility-keys.json +46 -0
- package/registries/entry.schema.json +48 -0
- package/registries/index.json +44 -0
- package/registries/program-models.json +26 -0
- package/schemas/index.json +12 -0
- package/schemas/v1.0.0/BENCHMARK.md +160 -0
- package/schemas/v1.0.0/CROSSWALK.md +200 -0
- package/schemas/v1.0.0/FIELDS.md +530 -0
- package/schemas/v1.0.0/STATUS.md +81 -0
- package/schemas/v1.0.0/context.jsonld +86 -0
- package/schemas/v1.0.0/examples/01-hackathon-fundingmap_1200.json +181 -0
- package/schemas/v1.0.0/examples/02-hackathon-fundingmap_1197.json +122 -0
- package/schemas/v1.0.0/examples/03-hackathon-fundingmap_1095.json +114 -0
- package/schemas/v1.0.0/examples/04-grant-fundingmap_1459.json +67 -0
- package/schemas/v1.0.0/examples/05-hackathon-fundingmap_1210.json +97 -0
- package/schemas/v1.0.0/examples/06-hackathon-fundingmap_1340.json +91 -0
- package/schemas/v1.0.0/examples/07-hackathon-fundingmap_1093.json +89 -0
- package/schemas/v1.0.0/examples/08-hackathon-fundingmap_1091.json +89 -0
- package/schemas/v1.0.0/examples/09-hackathon-fundingmap_1094.json +89 -0
- package/schemas/v1.0.0/examples/10-grant-fundingmap_600.json +65 -0
- package/schemas/v1.0.0/examples/11-hackathon-fundingmap_1389.json +83 -0
- package/schemas/v1.0.0/examples/12-hackathon-fundingmap_1356.json +83 -0
- package/schemas/v1.0.0/examples/13-grant-fundingmap_1458.json +59 -0
- package/schemas/v1.0.0/examples/14-grant-fundingmap_1059.json +57 -0
- package/schemas/v1.0.0/examples/15-hackathon-fundingmap_1430.json +82 -0
- package/schemas/v1.0.0/examples/16-hackathon-fundingmap_1092.json +79 -0
- package/schemas/v1.0.0/examples/17-hackathon-fundingmap_1277.json +75 -0
- package/schemas/v1.0.0/examples/18-hackathon-fundingmap_1302.json +72 -0
- package/schemas/v1.0.0/examples/19-hackathon-fundingmap_1231.json +76 -0
- package/schemas/v1.0.0/examples/20-hackathon-fundingmap_1235.json +74 -0
- package/schemas/v1.0.0/examples/21-hackathon-fundingmap_1219.json +73 -0
- package/schemas/v1.0.0/examples/22-hackathon-fundingmap_1254.json +72 -0
- package/schemas/v1.0.0/examples/23-grant-fundingmap_491.json +57 -0
- package/schemas/v1.0.0/examples/24-hackathon-fundingmap_1358.json +73 -0
- package/schemas/v1.0.0/examples/25-grant-fundingmap_1021.json +57 -0
- package/schemas/v1.0.0/examples/26-bounty-fundingmap_1453.json +57 -0
- package/schemas/v1.0.0/examples/27-accelerator-fundingmap_1291.json +56 -0
- package/schemas/v1.0.0/examples/28-rfp-fundingmap_1242.json +43 -0
- package/schemas/v1.0.0/opportunity.schema.json +758 -0
- package/spec.config.json +11 -0
package/PROCESS.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Process — how the RFP Hub Standard changes
|
|
2
|
+
|
|
3
|
+
This document governs **the standard**: the schema, the registries, the conformance suite and
|
|
4
|
+
the JSON-LD context. It does not govern the npm packages that ship it — see
|
|
5
|
+
[Two version axes](#two-version-axes).
|
|
6
|
+
|
|
7
|
+
Who decides, and in what forum, is in [`GOVERNANCE.md`](../../GOVERNANCE.md). What is normative
|
|
8
|
+
in the first place is in [`NORMATIVE.md`](./NORMATIVE.md). This file is about *what counts as a
|
|
9
|
+
change* and *what has to be true before one ships*.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Two version axes
|
|
14
|
+
|
|
15
|
+
They are different numbers and they move independently. Conflating them is the most common way a
|
|
16
|
+
schema project loses the ability to explain itself.
|
|
17
|
+
|
|
18
|
+
| Axis | Where it lives | What it means | Who bumps it |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| **Spec version** | `spec.config.json` → the schema `$id`, `specVersion`, the `schemas/<version>/` directory | The version of **the data contract**. A document declares which one it conforms to. | This document. |
|
|
21
|
+
| **Package version** | `package.json` of `@the-rfp-hub/standard`, `rfphub-validate`, … | The version of **the npm distribution**. Bumped for any shipped change — new generated types, a fixed export map, a doc file added to the `files` array. | Changesets, ordinary semver. |
|
|
22
|
+
|
|
23
|
+
A publisher pins a **spec version**. A build pins a **package version**. The package version may
|
|
24
|
+
run far ahead of the spec version and that is not a problem — it is the point of separating them.
|
|
25
|
+
`specVersion` in a document always names the spec axis.
|
|
26
|
+
|
|
27
|
+
`specVersion` is `const: "1.0.0"` — a document names the exact spec version it conforms to. A
|
|
28
|
+
patch line (`pattern: ^1\.0\.\d+$`) was tried during the re-cut and reverted the same day; see
|
|
29
|
+
the worked example below, and [`adr/0003`](../../adr/0003-instance-self-identification-and-version-pattern.md).
|
|
30
|
+
The rule the pattern would have depended on still stands and is worth stating now, because it is
|
|
31
|
+
the precondition for ever reintroducing it: **a patch release of the spec is editorial by
|
|
32
|
+
definition** — it may correct prose, add registry entries or fix a description, and it may never
|
|
33
|
+
change what validates. If a proposed patch would change what validates, it is not a patch.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Feature stages
|
|
38
|
+
|
|
39
|
+
Every field carries a stage, recorded in the schema as `x-stability`. Four stages, no more:
|
|
40
|
+
|
|
41
|
+
| Stage | Annotation | What it means | How it moves on |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| **proposed** | not in the schema yet | An issue or PR describing a field, with a use case and at least one real document that needs it. Lives in the issue tracker, and may have conformance fixtures before it has a schema. | Editor accepts → merged as `experimental`. |
|
|
44
|
+
| **experimental** | `x-stability: provisional` | In the schema and valid to use, but resting on narrow evidence. **May change shape or be removed** without the ceremony a stable field gets. Consumers should tolerate its absence and its change. | **≥1 real publisher and ≥1 real consumer have shipped it.** Then it becomes `stable`. |
|
|
45
|
+
| **stable** | `x-stability: stable` | The default. Changes to it are changes to the standard and follow the breaking-change rules below. | Only to `deprecated`. |
|
|
46
|
+
| **deprecated** | `deprecated: true` (+ optional `x-deprecated: {since, replacedBy, note}`) | Still valid, still accepted, no longer recommended. Consumers must keep reading it. | Removal, no earlier than the next release. |
|
|
47
|
+
|
|
48
|
+
The promotion gate is deliberately **one publisher and one consumer**, not the five-implementation
|
|
49
|
+
bar larger projects use. At this size, five implementations is a gate nothing ever passes, and an
|
|
50
|
+
unpassable gate means everything stays experimental forever, which tells a reader nothing.
|
|
51
|
+
|
|
52
|
+
Fields currently at `provisional`: `serviceAgreement`, `milestones[]`, `grant.programModel`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What "breaking" means
|
|
57
|
+
|
|
58
|
+
Operationally, and in one sentence:
|
|
59
|
+
|
|
60
|
+
> **A change is breaking if a document that was valid under version N is invalid under version
|
|
61
|
+
> N+1 — or if a document that was invalid under N is valid under N+1.**
|
|
62
|
+
|
|
63
|
+
### It is bidirectional
|
|
64
|
+
|
|
65
|
+
The second half is the half most schema policies get wrong. **Loosening a constraint is breaking
|
|
66
|
+
too.** If a field's enum gains a value, or a `const` becomes a `pattern`, or a required property
|
|
67
|
+
becomes optional, then data that used to be rejected now validates — and every consumer that
|
|
68
|
+
relied on the rejection has silently changed behaviour. A validator is a contract in both
|
|
69
|
+
directions.
|
|
70
|
+
|
|
71
|
+
Worked example from this project's own history — **and note how it ended.** During the
|
|
72
|
+
2026-07-27 re-cut, `specVersion` was moved from `const: "1.0.0"` to `pattern: ^1\.0\.\d+$`.
|
|
73
|
+
Every previously valid document stayed valid; `{"specVersion": "1.0.7"}` used to fail and now
|
|
74
|
+
passed. Under this definition that is a **breaking change**, even though nothing was
|
|
75
|
+
invalidated and it reads to most people as a harmless relaxation.
|
|
76
|
+
|
|
77
|
+
Classifying it correctly is what made the next question askable: *what is this loosening buying
|
|
78
|
+
us right now?* The answer was nothing — there was no patch process, no editorial patch pending,
|
|
79
|
+
and no document anywhere carrying a `1.0.x` value other than `1.0.0`. **It was reverted the same
|
|
80
|
+
day** and `specVersion` is `const: "1.0.0"` again. The rule to reintroduce it is written above:
|
|
81
|
+
a patch release must be editorial by definition. Revisit at the first real editorial patch.
|
|
82
|
+
|
|
83
|
+
This example is kept because the revert is the more useful half of it. A definition of
|
|
84
|
+
"breaking" that only ever produces a note in a changelog is decoration; this one produced a
|
|
85
|
+
decision. See [`adr/0003`](../../adr/0003-instance-self-identification-and-version-pattern.md)
|
|
86
|
+
and [`CHANGELOG.md`](./CHANGELOG.md).
|
|
87
|
+
|
|
88
|
+
### The indeterminate-state carve-out
|
|
89
|
+
|
|
90
|
+
There is a third state between valid and invalid: **indeterminate** — behaviour the schema never
|
|
91
|
+
defined. If version N said nothing about a case, and version N+1 defines it, that is **not
|
|
92
|
+
breaking**, because there was no contract to break.
|
|
93
|
+
|
|
94
|
+
This matters here because much of the 2026-07-27 re-cut is exactly this shape. Enforcing
|
|
95
|
+
one-block-per-funding-type made previously-valid documents invalid — that is straightforwardly
|
|
96
|
+
breaking. But permitting `$schema`, `@context` and `@type` at the top level defined behaviour that
|
|
97
|
+
`additionalProperties: false` had left as a flat rejection with no stated intent; the schema had
|
|
98
|
+
never taken a position on self-identification. The carve-out is narrow and it is easy to abuse:
|
|
99
|
+
**"we never thought about it" is not the same as "the schema left it undefined."** If the previous
|
|
100
|
+
version's validator produced a definite answer, the state was not indeterminate.
|
|
101
|
+
|
|
102
|
+
### Consequences
|
|
103
|
+
|
|
104
|
+
- **Breaking changes require a new spec version** and a new `schemas/<version>/` directory. The
|
|
105
|
+
previous directory stays published, unedited, forever.
|
|
106
|
+
- **Non-breaking changes** — new optional fields, new registry entries, corrections to
|
|
107
|
+
descriptions — go into the current version on the current patch line.
|
|
108
|
+
- **Informative documents** may be corrected at any time without any of this
|
|
109
|
+
([`NORMATIVE.md`](./NORMATIVE.md)).
|
|
110
|
+
|
|
111
|
+
### In-place re-cuts
|
|
112
|
+
|
|
113
|
+
`v1.0.0` was re-cut in place on 2026-07-27: same version string, different bytes
|
|
114
|
+
([`adr/0001`](../../adr/0001-recut-v1.0.0-in-place.md)). That was defensible **only** because the
|
|
115
|
+
standard was unpublished and unadopted, and it is **the only one**.
|
|
116
|
+
|
|
117
|
+
The rule, stated so a future maintainer cannot reason their way around it:
|
|
118
|
+
|
|
119
|
+
> **A version directory may be edited in place only while its maturity is `draft` and no external
|
|
120
|
+
> consumer has adopted it. It may never be edited in place after it is declared `stable`.**
|
|
121
|
+
|
|
122
|
+
The mechanism, so the rule does not depend on memory: declaring a version stable adds a `FROZEN`
|
|
123
|
+
marker file to its directory, and
|
|
124
|
+
[`.github/workflows/spec-freeze.yml`](../../.github/workflows/spec-freeze.yml) fails any PR that
|
|
125
|
+
touches a version directory containing one. The marker is not present yet — it lands with the
|
|
126
|
+
promotion to `stable`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Deprecation
|
|
131
|
+
|
|
132
|
+
1. **Nothing in a registry is ever deleted.** A value that turns out to be wrong, redundant or
|
|
133
|
+
superseded gets `"status": "deprecated"` and a `replacedBy` pointer to its successor. Data
|
|
134
|
+
already published using it stays valid and stays interpretable. The registry file is an
|
|
135
|
+
append-and-annotate log, not a list of currently-good values — that list is the `active` array
|
|
136
|
+
in the generated [`registries/index.json`](./registries/index.json).
|
|
137
|
+
2. **A deprecated field stays valid and stays accepted** for at least one full release after the
|
|
138
|
+
release that deprecated it. A field deprecated in release N may be removed no earlier than
|
|
139
|
+
N+1. Consumers get a release cycle's notice, at minimum.
|
|
140
|
+
3. **Deprecation is announced, not discovered — and it is announced in the standard keyword
|
|
141
|
+
first.** JSON Schema 2020-12 has a native `deprecated` annotation, so **a deprecated element
|
|
142
|
+
sets `deprecated: true`**. That is the machine-readable signal, and it is the one a generic
|
|
143
|
+
validator or documentation generator already understands without knowing anything about this
|
|
144
|
+
standard. Our `x-deprecated: {since, replacedBy, note}` is a **supplement, not a substitute**:
|
|
145
|
+
it carries only the metadata the native keyword cannot express, and the metaschema rejects it
|
|
146
|
+
when `deprecated: true` is absent. Never invent an `x-` keyword for something 2020-12 already
|
|
147
|
+
says.
|
|
148
|
+
|
|
149
|
+
The announcement also goes in `CHANGELOG.md` — and for a field that disappears, in that
|
|
150
|
+
release's **field mapping table** (the v1.0.0 one is
|
|
151
|
+
[here](./CHANGELOG.md#field-mapping-old--new)). No row is ever removed from a mapping table:
|
|
152
|
+
a field that vanished from the schema keeps its row forever, because that row is what tells a
|
|
153
|
+
consumer holding old data what happened to it.
|
|
154
|
+
4. **Removal is a breaking change** and takes a new spec version, like any other.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Registering a value in a registry
|
|
159
|
+
|
|
160
|
+
The registries are how the standard keeps open fields comparable without closing them. Adding to
|
|
161
|
+
one is the cheapest possible contribution and it is designed to be.
|
|
162
|
+
|
|
163
|
+
There are **three**: `eligibility-keys`, `deadline-labels` and `program-models`. Those are the
|
|
164
|
+
vocabularies where two publishers writing different strings for the same concept produce
|
|
165
|
+
uncomparable data. `ecosystems` and `networks` are open lists too and deliberately have **no**
|
|
166
|
+
registry — a registry over chain names is read as an allowed-values list however carefully
|
|
167
|
+
[`NORMATIVE.md`](./NORMATIVE.md) words the distinction, and it would put this review process in
|
|
168
|
+
front of a newly launched chain for no interoperability gain.
|
|
169
|
+
|
|
170
|
+
### Criteria
|
|
171
|
+
|
|
172
|
+
A value is registered when it meets all three:
|
|
173
|
+
|
|
174
|
+
1. **In use by at least one real publisher.** Not hypothetical, not "we might need it". A link to
|
|
175
|
+
a live listing, a program's own documentation, or an export that carries the value.
|
|
176
|
+
2. **Clearly described.** One paragraph saying what the value means and when a publisher should
|
|
177
|
+
choose it *over its nearest neighbour*. If the description cannot distinguish it from an
|
|
178
|
+
existing entry, the existing entry is the answer.
|
|
179
|
+
3. **No vendor lock.** A value that only makes sense inside one platform's product belongs in
|
|
180
|
+
`extensions`, not in a shared vocabulary. Registered values must be meaningful to a publisher
|
|
181
|
+
who has never heard of whoever proposed them.
|
|
182
|
+
|
|
183
|
+
The registration policy is deliberately the **least strict one that works**: an editor reviews,
|
|
184
|
+
and the bar is the three criteria above, not consensus.
|
|
185
|
+
|
|
186
|
+
### Steps
|
|
187
|
+
|
|
188
|
+
1. **Open a GitHub issue** — which registry, the proposed key, the description, the evidence of
|
|
189
|
+
real use, and the nearest existing entry and why it does not fit.
|
|
190
|
+
2. **Editor review.** An editor either accepts, asks for a better distinction from an existing
|
|
191
|
+
entry, or declines with a reason on the issue. Registry proposals get a **shorter review
|
|
192
|
+
window** than schema changes — see [`GOVERNANCE.md`](../../GOVERNANCE.md) — because they add
|
|
193
|
+
documentation about values that are already valid data.
|
|
194
|
+
3. **PR** adding the entry to the registry file with `description`, `status: "active"`, `since`
|
|
195
|
+
(the spec version registering it), and `examples`. Run `pnpm codegen` so
|
|
196
|
+
`registries/index.json` regenerates, and `pnpm check`.
|
|
197
|
+
|
|
198
|
+
Registering a value **never changes what validates**. Unregistered values were valid before and
|
|
199
|
+
stay valid after; what changes is that the validator stops warning about this one.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Errata
|
|
204
|
+
|
|
205
|
+
An erratum is a defect in something already published. Triage every reported one into exactly one
|
|
206
|
+
of four labels — the point of the fourth is to notice when a series of small clarifications is
|
|
207
|
+
quietly redesigning the standard.
|
|
208
|
+
|
|
209
|
+
| Label | What it is | How it is fixed |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `erratum-editorial` | Typo, broken link, unclear sentence, stale number in an informative document. Changes no behaviour. | Ordinary PR, any time, no window, no version. |
|
|
212
|
+
| `erratum-technical` | The normative artifacts are internally inconsistent, or a description says something the schema does not do. A correct implementation could be misled. | Fix in the current version if the fix does not change what validates; otherwise it is a change, not an erratum. Record in `CHANGELOG.md`. |
|
|
213
|
+
| `held-for-next-cut` | Real defect, but fixing it would change what validates. Cannot ship inside the current version. | Stays open, labelled, and is picked up when the next spec version opens. Say so on the issue — a defect parked without a label reads as ignored. |
|
|
214
|
+
| `redesign` | The report is not a defect at all: it asks the standard to mean something different. | Not an erratum. Becomes a `proposed` feature, and if accepted an ADR. |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Release checklist
|
|
219
|
+
|
|
220
|
+
Before a spec release — a new version directory, or a patch line on the current one:
|
|
221
|
+
|
|
222
|
+
- [ ] `pnpm codegen && pnpm codegen:check` — every generated artifact in sync (types, registry
|
|
223
|
+
index, versions index, the generated FIELDS tables).
|
|
224
|
+
- [ ] `pnpm check` — publication rules: context↔schema drift, version-string agreement across
|
|
225
|
+
`spec.config.json` and everything stamped from it, source neutrality.
|
|
226
|
+
- [ ] `pnpm build && pnpm typecheck && pnpm test && pnpm lint` green.
|
|
227
|
+
- [ ] Conformance suite passes, and **every rule changed in this release has a case** in
|
|
228
|
+
`conformance/<version>/pass/` or `fail/` named after the rule.
|
|
229
|
+
- [ ] Examples re-validated against the released schema.
|
|
230
|
+
- [ ] `CHANGELOG.md` entry, grouped Schema / Context / Tooling / Docs, and explicitly listing any
|
|
231
|
+
**breaking** change under the bidirectional definition above — including loosenings.
|
|
232
|
+
- [ ] The release's field mapping table updated for anything renamed or removed; no row ever
|
|
233
|
+
deleted.
|
|
234
|
+
- [ ] `STATUS.md` for the version: maturity, supersedes/superseded-by, known issues.
|
|
235
|
+
- [ ] `schemas/index.json` lists the version and `latest` points at the right one.
|
|
236
|
+
- [ ] An ADR exists for any structural decision in the release.
|
|
237
|
+
- [ ] A changeset for the npm distribution — the **other** version axis.
|
|
238
|
+
- [ ] If the release declares a version `stable`: add its `FROZEN` marker and confirm the freeze
|
|
239
|
+
workflow rejects a test edit to that directory.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @the-rfp-hub/standard
|
|
2
|
+
|
|
3
|
+
The **canonical RFP Hub Standard** — a JSON Schema (draft 2020-12) describing an
|
|
4
|
+
Ethereum-ecosystem funding opportunity, plus the TypeScript types **generated from it**.
|
|
5
|
+
|
|
6
|
+
CC0-1.0. Zero runtime dependencies. This package is the single source of truth that the
|
|
7
|
+
validator, API, client, and agent libraries all build on.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { opportunitySchema, SPEC_VERSION, type Opportunity } from "@the-rfp-hub/standard";
|
|
13
|
+
|
|
14
|
+
const opp: Opportunity = {
|
|
15
|
+
specVersion: SPEC_VERSION,
|
|
16
|
+
id: "example:grant-1",
|
|
17
|
+
fundingType: "grant",
|
|
18
|
+
title: "Example Grants",
|
|
19
|
+
description: "…",
|
|
20
|
+
status: "open",
|
|
21
|
+
sponsoringOrganizations: [{ name: "Example Foundation" }],
|
|
22
|
+
source: {},
|
|
23
|
+
applicationUrl: "https://example.org/grants",
|
|
24
|
+
deadlines: [{ type: "fixed", date: "2026-12-31T23:59:59.000Z", label: "application" }],
|
|
25
|
+
grant: {},
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The raw schema file is also published and importable directly:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import schema from "@the-rfp-hub/standard/schemas/v1.0.0/opportunity.schema.json";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What this package ships
|
|
36
|
+
|
|
37
|
+
| Artifact | What it is |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `schemas/v1.0.0/opportunity.schema.json` | The **normative** schema. Everything else is derived from it or governed by it. |
|
|
40
|
+
| `schemas/v1.0.0/context.jsonld` | JSON-LD context. Term IRIs are versionless; the context *document* is what gets versioned. |
|
|
41
|
+
| `schemas/v1.0.0/examples/` | 28 curated real-world entries. |
|
|
42
|
+
| `conformance/v1.0.0/{pass,fail}/` | One document per rule, named after the rule. Run these against your own implementation — see [`conformance/README.md`](./conformance/README.md). |
|
|
43
|
+
| `registries/` | Three open vocabularies: eligibility keys, deadline labels, program models. The schema keeps these fields free-text; the registry fixes what each value means. `ecosystems` and `networks` are open too and deliberately have no registry — see [`ARTIFACTS.md`](./ARTIFACTS.md). |
|
|
44
|
+
| `meta/rfphub-schema.meta.json` | Metaschema constraining our schema file's shape and legalising `x-stability` / `x-since` / `x-deprecated`. |
|
|
45
|
+
| `spec.config.json` | The spec's identity. Every generated version string and URL is stamped from here. |
|
|
46
|
+
|
|
47
|
+
## Source of truth
|
|
48
|
+
|
|
49
|
+
`schemas/v1.0.0/opportunity.schema.json` is hand-authored and authoritative; `spec.config.json`
|
|
50
|
+
is the only place a version string or namespace IRI is hand-written. `pnpm codegen` stamps that
|
|
51
|
+
identity into the schema, the context and `SPEC_VERSION`, then generates
|
|
52
|
+
`src/generated/opportunity.ts`, `registries/index.json`, `schemas/index.json` and the field
|
|
53
|
+
tables in `FIELDS.md`. `pnpm codegen:check` fails CI when any of them is stale; `pnpm check` runs
|
|
54
|
+
the publication rules (context↔schema drift, version-string agreement, source neutrality). Never
|
|
55
|
+
edit a generated file by hand.
|
|
56
|
+
|
|
57
|
+
See [`schemas/v1.0.0/FIELDS.md`](./schemas/v1.0.0/FIELDS.md) for the full field reference (its
|
|
58
|
+
tables are generated from the schema), [`CHANGELOG.md`](./CHANGELOG.md) for the release record,
|
|
59
|
+
and [`schemas/v1.0.0/BENCHMARK.md`](./schemas/v1.0.0/BENCHMARK.md) for real-data validation
|
|
60
|
+
results.
|
|
61
|
+
|
|
62
|
+
## How this standard is governed
|
|
63
|
+
|
|
64
|
+
| Document | Answers |
|
|
65
|
+
|---|---|
|
|
66
|
+
| [`NORMATIVE.md`](./NORMATIVE.md) | Which artifacts carry authority, and which can be corrected any day without a release. |
|
|
67
|
+
| [`PROCESS.md`](./PROCESS.md) | Feature stages, what "breaking" means operationally, deprecation, how to register a vocabulary value, the release checklist. |
|
|
68
|
+
| [`ARTIFACTS.md`](./ARTIFACTS.md) | Every artifact this standard ships, plans to ship, or has declined — with the reason. |
|
|
69
|
+
| [`schemas/v1.0.0/STATUS.md`](./schemas/v1.0.0/STATUS.md) | Where this version stands: maturity, known issues, and the in-place re-cut. |
|
|
70
|
+
| [`CHANGELOG.md`](./CHANGELOG.md) | What changed and when — including the [field mapping table](./CHANGELOG.md#field-mapping-old--new) for the re-cut. No row is ever removed from it. |
|
|
71
|
+
|
|
72
|
+
Who decides, and the review windows, are in `GOVERNANCE.md` at the repository root.
|
|
73
|
+
|
|
74
|
+
> ⚠️ **v1.0.0 was re-cut in place on 2026-07-27** — the contents under this version string differ
|
|
75
|
+
> from those published before that date. See `STATUS.md` and the field mapping table in
|
|
76
|
+
> `CHANGELOG.md`.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
Dedicated to the public domain under **CC0 1.0 Universal** ([`LICENSE`](./LICENSE)). To the
|
|
81
|
+
extent possible under law, The RFP Hub contributors have waived all copyright and related
|
|
82
|
+
rights to the standard — the schema, generated types, and docs — so it can be embedded,
|
|
83
|
+
forked, and re-published without restriction.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# RFP Hub Standard conformance suite
|
|
2
|
+
|
|
3
|
+
A set of whole documents that an implementation of the RFP Hub Standard is expected to accept
|
|
4
|
+
or reject. It is shipped inside the `@the-rfp-hub/standard` npm package so that an external
|
|
5
|
+
implementer — a validator in another language, an ingestion pipeline, a publisher's export
|
|
6
|
+
job — can run the same cases the reference implementation runs.
|
|
7
|
+
|
|
8
|
+
## Layout
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
conformance/
|
|
12
|
+
v<spec version>/
|
|
13
|
+
pass/ documents that MUST validate
|
|
14
|
+
fail/ documents that MUST NOT validate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Every file is a complete opportunity document. **Each file is named after the rule it
|
|
18
|
+
exercises**, not after the record it contains: a red CI run therefore names the violated
|
|
19
|
+
constraint (`fail/deadline-fixed-without-date.json`) rather than a fixture number. Each
|
|
20
|
+
document's own `description` field explains what the case is testing and why the rule exists,
|
|
21
|
+
so the file is readable on its own.
|
|
22
|
+
|
|
23
|
+
## Running the suite
|
|
24
|
+
|
|
25
|
+
An implementation is conformant with respect to this suite when every document in `pass/`
|
|
26
|
+
validates against `schemas/<version>/opportunity.schema.json` and every document in `fail/`
|
|
27
|
+
does not. Nothing else is asserted: the suite says nothing about which error is reported, how
|
|
28
|
+
many errors are reported, or in what order.
|
|
29
|
+
|
|
30
|
+
The reference run lives in `packages/validate/test/conformance.test.ts`.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm --filter rfphub-validate test
|
|
34
|
+
# or, against the published package:
|
|
35
|
+
npx rfphub-validate node_modules/@the-rfp-hub/standard/conformance/v1.0.0/pass
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Scope
|
|
39
|
+
|
|
40
|
+
These cases cover **schema validity only** — the hard constraints in the JSON Schema. They do
|
|
41
|
+
not cover the advisory checks (`packages/validate/src/checks/`), which report *warnings* about
|
|
42
|
+
things the schema deliberately leaves open: unregistered eligibility keys, unregistered
|
|
43
|
+
deadline labels, unregistered program models, and milestone amounts with no envelope currency.
|
|
44
|
+
A document may be conformant and still raise warnings; that is the point of the split.
|
|
45
|
+
|
|
46
|
+
## Contributing a case
|
|
47
|
+
|
|
48
|
+
Add one file per rule, in whichever directory expresses the expectation, and give it the
|
|
49
|
+
rule's name. Prefer a case that isolates a single constraint: a document that violates four
|
|
50
|
+
rules at once proves nothing about which of the four an implementation enforces. Cases are
|
|
51
|
+
versioned with the spec — a new version gets a new directory, and existing directories are not
|
|
52
|
+
edited once their version is frozen.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:fixed-null-date",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Fixed deadline with a null date",
|
|
6
|
+
"description": "A deadline of type 'fixed' whose date is explicitly null. The if/then narrows date to a non-null string, without which this would slip through the required check.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"deadlines": [
|
|
15
|
+
{
|
|
16
|
+
"type": "fixed",
|
|
17
|
+
"date": null,
|
|
18
|
+
"label": "application"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"grant": {}
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:fixed-no-date",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Fixed deadline with no date",
|
|
6
|
+
"description": "A deadline of type 'fixed' with the date omitted. date is required when type is 'fixed' — schema-enforced via if/then, not a SHOULD.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"deadlines": [
|
|
15
|
+
{
|
|
16
|
+
"type": "fixed",
|
|
17
|
+
"label": "application"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"grant": {}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:eligibility-value",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Structured eligibility value",
|
|
6
|
+
"description": "eligibility keys are open, but values are plain strings. A nested object here is the structured decomposition the map deliberately does not provide.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"eligibility": {
|
|
15
|
+
"geography": {
|
|
16
|
+
"include": ["EU"],
|
|
17
|
+
"exclude": ["US"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"grant": {}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:empty-sponsor",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Empty sponsoring organisations array",
|
|
6
|
+
"description": "sponsoringOrganizations is present but empty. minItems is 1: the array being required would otherwise be satisfied by [], which asserts nothing.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [],
|
|
9
|
+
"source": {},
|
|
10
|
+
"grant": {}
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:no-block",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Missing the grant block",
|
|
6
|
+
"description": "fundingType is 'grant' but there is no grant block, so opportunity[opportunity.fundingType] would not resolve. Adapted from the pre-re-cut grant-no-block fixture.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {}
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:no-sponsor",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "No sponsoring organisations",
|
|
6
|
+
"description": "sponsoringOrganizations replaced the single required organization field and is itself required. A record with no issuer does not validate.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"source": {},
|
|
9
|
+
"grant": {}
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:two-blocks",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "A grant carrying an rfp block",
|
|
6
|
+
"description": "fundingType is 'grant' and the grant block is present, but an rfp block is present too. Pre-re-cut this validated. It must now fail: only the block matching fundingType may be present, so opportunity[opportunity.fundingType] is a guarantee.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"grant": {},
|
|
15
|
+
"rfp": {
|
|
16
|
+
"scope": "This block has no business being here."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:source-url",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Provenance still carrying a source URL",
|
|
6
|
+
"description": "source.url was removed by the re-cut and the provenance block is closed, so a pre-re-cut record still carrying it now fails. Link-back runs through applicationUrl alone.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {
|
|
14
|
+
"url": "https://example.org/original-posting"
|
|
15
|
+
},
|
|
16
|
+
"grant": {}
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:bad-status",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Bad status",
|
|
6
|
+
"description": "'Active' is not a status value. The enum is upcoming/open/closed/archived; there is deliberately no 'draft'. Adapted from the pre-re-cut bad-status fixture.",
|
|
7
|
+
"status": "Active",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"grant": {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:unknown-prop",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Leaks an internal field",
|
|
6
|
+
"description": "chainID is an internal field, not part of the neutral standard. The top level is closed; publisher-specific data belongs under extensions.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"grant": {},
|
|
15
|
+
"chainID": 1
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:bad-type",
|
|
4
|
+
"fundingType": "wishlist",
|
|
5
|
+
"title": "Not one of the six funding types",
|
|
6
|
+
"description": "'wishlist' was considered and dropped; the discriminator stays at six values. Service agreements are a top-level field, not a seventh type.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {}
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "2.0.0",
|
|
3
|
+
"id": "conformance:bad-specversion",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Wrong spec version",
|
|
6
|
+
"description": "specVersion is a const: this schema defines version 1.0.0 and nothing else. A patch-line pattern was tried during the re-cut and reverted the same day, so 1.0.1 fails here too — a spec patch would be a new cut, not a value a document may assert unilaterally.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"grant": {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"specVersion": "1.0.0",
|
|
3
|
+
"id": "conformance:rolling",
|
|
4
|
+
"fundingType": "grant",
|
|
5
|
+
"title": "Rolling applications",
|
|
6
|
+
"description": "A rolling deadline carries no date. `date` is required only when type is 'fixed'; a rolling programme is exactly the case that requirement must not catch.",
|
|
7
|
+
"status": "open",
|
|
8
|
+
"sponsoringOrganizations": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Example Foundation"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"source": {},
|
|
14
|
+
"deadlines": [
|
|
15
|
+
{
|
|
16
|
+
"type": "rolling",
|
|
17
|
+
"label": "application"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"grant": {
|
|
21
|
+
"recurring": true
|
|
22
|
+
}
|
|
23
|
+
}
|