@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.
Files changed (77) hide show
  1. package/ARTIFACTS.md +85 -0
  2. package/CHANGELOG.md +264 -0
  3. package/LICENSE +121 -0
  4. package/NORMATIVE.md +77 -0
  5. package/PROCESS.md +239 -0
  6. package/README.md +83 -0
  7. package/conformance/README.md +52 -0
  8. package/conformance/v1.0.0/fail/deadline-fixed-date-null.json +22 -0
  9. package/conformance/v1.0.0/fail/deadline-fixed-without-date.json +21 -0
  10. package/conformance/v1.0.0/fail/eligibility-value-not-string.json +21 -0
  11. package/conformance/v1.0.0/fail/empty-sponsoring-organizations.json +11 -0
  12. package/conformance/v1.0.0/fail/missing-matching-type-block.json +14 -0
  13. package/conformance/v1.0.0/fail/missing-required-properties.json +5 -0
  14. package/conformance/v1.0.0/fail/missing-sponsoring-organizations.json +10 -0
  15. package/conformance/v1.0.0/fail/one-block-per-fundingtype.json +18 -0
  16. package/conformance/v1.0.0/fail/source-url-removed.json +17 -0
  17. package/conformance/v1.0.0/fail/status-not-in-enum.json +15 -0
  18. package/conformance/v1.0.0/fail/unknown-top-level-property.json +16 -0
  19. package/conformance/v1.0.0/fail/wrong-fundingtype-value.json +14 -0
  20. package/conformance/v1.0.0/fail/wrong-specversion.json +15 -0
  21. package/conformance/v1.0.0/pass/deadline-rolling-without-date.json +23 -0
  22. package/conformance/v1.0.0/pass/full-featured.json +137 -0
  23. package/conformance/v1.0.0/pass/grant-funding-mechanisms-array.json +27 -0
  24. package/conformance/v1.0.0/pass/grant.json +45 -0
  25. package/conformance/v1.0.0/pass/hackathon.json +67 -0
  26. package/conformance/v1.0.0/pass/minimal-required-only.json +15 -0
  27. package/conformance/v1.0.0/pass/self-identification.json +18 -0
  28. package/conformance/v1.0.0/pass/source-empty-object.json +30 -0
  29. package/dist/index.cjs +902 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +661 -0
  32. package/dist/index.d.ts +661 -0
  33. package/dist/index.js +896 -0
  34. package/dist/index.js.map +1 -0
  35. package/meta/rfphub-schema.meta.json +131 -0
  36. package/package.json +56 -0
  37. package/registries/deadline-labels.json +38 -0
  38. package/registries/eligibility-keys.json +46 -0
  39. package/registries/entry.schema.json +48 -0
  40. package/registries/index.json +44 -0
  41. package/registries/program-models.json +26 -0
  42. package/schemas/index.json +12 -0
  43. package/schemas/v1.0.0/BENCHMARK.md +160 -0
  44. package/schemas/v1.0.0/CROSSWALK.md +200 -0
  45. package/schemas/v1.0.0/FIELDS.md +530 -0
  46. package/schemas/v1.0.0/STATUS.md +81 -0
  47. package/schemas/v1.0.0/context.jsonld +86 -0
  48. package/schemas/v1.0.0/examples/01-hackathon-fundingmap_1200.json +181 -0
  49. package/schemas/v1.0.0/examples/02-hackathon-fundingmap_1197.json +122 -0
  50. package/schemas/v1.0.0/examples/03-hackathon-fundingmap_1095.json +114 -0
  51. package/schemas/v1.0.0/examples/04-grant-fundingmap_1459.json +67 -0
  52. package/schemas/v1.0.0/examples/05-hackathon-fundingmap_1210.json +97 -0
  53. package/schemas/v1.0.0/examples/06-hackathon-fundingmap_1340.json +91 -0
  54. package/schemas/v1.0.0/examples/07-hackathon-fundingmap_1093.json +89 -0
  55. package/schemas/v1.0.0/examples/08-hackathon-fundingmap_1091.json +89 -0
  56. package/schemas/v1.0.0/examples/09-hackathon-fundingmap_1094.json +89 -0
  57. package/schemas/v1.0.0/examples/10-grant-fundingmap_600.json +65 -0
  58. package/schemas/v1.0.0/examples/11-hackathon-fundingmap_1389.json +83 -0
  59. package/schemas/v1.0.0/examples/12-hackathon-fundingmap_1356.json +83 -0
  60. package/schemas/v1.0.0/examples/13-grant-fundingmap_1458.json +59 -0
  61. package/schemas/v1.0.0/examples/14-grant-fundingmap_1059.json +57 -0
  62. package/schemas/v1.0.0/examples/15-hackathon-fundingmap_1430.json +82 -0
  63. package/schemas/v1.0.0/examples/16-hackathon-fundingmap_1092.json +79 -0
  64. package/schemas/v1.0.0/examples/17-hackathon-fundingmap_1277.json +75 -0
  65. package/schemas/v1.0.0/examples/18-hackathon-fundingmap_1302.json +72 -0
  66. package/schemas/v1.0.0/examples/19-hackathon-fundingmap_1231.json +76 -0
  67. package/schemas/v1.0.0/examples/20-hackathon-fundingmap_1235.json +74 -0
  68. package/schemas/v1.0.0/examples/21-hackathon-fundingmap_1219.json +73 -0
  69. package/schemas/v1.0.0/examples/22-hackathon-fundingmap_1254.json +72 -0
  70. package/schemas/v1.0.0/examples/23-grant-fundingmap_491.json +57 -0
  71. package/schemas/v1.0.0/examples/24-hackathon-fundingmap_1358.json +73 -0
  72. package/schemas/v1.0.0/examples/25-grant-fundingmap_1021.json +57 -0
  73. package/schemas/v1.0.0/examples/26-bounty-fundingmap_1453.json +57 -0
  74. package/schemas/v1.0.0/examples/27-accelerator-fundingmap_1291.json +56 -0
  75. package/schemas/v1.0.0/examples/28-rfp-fundingmap_1242.json +43 -0
  76. package/schemas/v1.0.0/opportunity.schema.json +758 -0
  77. 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,5 @@
1
+ {
2
+ "specVersion": "1.0.0",
3
+ "id": "conformance:missing-required",
4
+ "fundingType": "grant"
5
+ }
@@ -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
+ }