@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/ARTIFACTS.md ADDED
@@ -0,0 +1,85 @@
1
+ # Artifacts — what this standard ships, what it will ship, and what it will not
2
+
3
+ A standard is a set of files. This is the list, with a status and a generator for each, so that
4
+ "can I get this as X?" has a written answer instead of an implied roadmap.
5
+
6
+ Three statuses: **shipped** (in the package today), **planned** (agreed, with a stated trigger —
7
+ not a promise of a date), **declined** (decided against, with the reason).
8
+
9
+ The reason declines are written down at all: an unwritten decline reads as an oversight, gets
10
+ re-proposed every few months, and each time costs the same conversation.
11
+
12
+ ---
13
+
14
+ ## Shipped
15
+
16
+ | Artifact | What it is | Generated by |
17
+ |---|---|---|
18
+ | [`schemas/v1.0.0/opportunity.schema.json`](./schemas/v1.0.0/opportunity.schema.json) | **The normative artifact.** JSON Schema draft 2020-12, one self-contained file, local `#/$defs` pointers only. | Hand-authored. Identity (`$id`, the `specVersion` constant, description) stamped by `scripts/codegen.mjs` from `spec.config.json`. |
19
+ | [`src/generated/opportunity.ts`](./src/generated/opportunity.ts) | TypeScript types for every object in the schema. | `scripts/codegen.mjs` via `json-schema-to-typescript`. CI-gated by `codegen:check`. |
20
+ | [`schemas/v1.0.0/context.jsonld`](./schemas/v1.0.0/context.jsonld) | JSON-LD context mapping every top-level term to schema.org / DAOIP-5 / the RFP Hub vocabulary. Term IRIs are versionless; the *document* is versioned. | Hand-authored; `@vocab` stamped by codegen. Drift against the schema fails CI (`scripts/check-spec.mjs`). |
21
+ | [`conformance/v1.0.0/{pass,fail}/`](./conformance/v1.0.0) | Whole documents, one file per rule, named after the rule. Shipped in the package `files` array so external implementers can run them. | Hand-authored. |
22
+ | [`registries/*.json`](./registries) | The three open vocabularies whose values must mean the same thing across publishers: eligibility keys, deadline labels, program models. Plus `entry.schema.json` constraining their shape. `ecosystems` and `networks` are open lists with **no** registry — see the decline below. | Hand-authored entries; `registries/index.json` generated by codegen. |
23
+ | [`schemas/v1.0.0/FIELDS.md`](./schemas/v1.0.0/FIELDS.md) — the field tables | The field reference: type, requiredness, description and governing registry for every property. | `scripts/codegen.mjs`, spliced into the `generated:fields` marker block. Narrative around it is hand-written. |
24
+ | [`schemas/index.json`](./schemas/index.json) | Machine-readable index of published versions, with `latest`. | `scripts/codegen.mjs` from `spec.config.json`. |
25
+ | [`meta/rfphub-schema.meta.json`](./meta/rfphub-schema.meta.json) | Metaschema constraining our own schema file and legalising the three `x-` annotations (`x-stability`, `x-since`, `x-deprecated`) on top of the native 2020-12 vocabulary — including the rule that `x-deprecated` may only accompany a native `deprecated: true`, never replace it. | Hand-authored, validated in CI. |
26
+ | [`spec.config.json`](./spec.config.json) | The spec's identity. The only place a version string or namespace IRI is hand-written. | Hand-authored. |
27
+ | [`schemas/v1.0.0/examples/`](./schemas/v1.0.0/examples) | 28 curated real-world documents. Illustrative, not normative. | Mapped from a public aggregator API; see `BENCHMARK.md`. |
28
+ | Policy docs — [`PROCESS.md`](./PROCESS.md), [`NORMATIVE.md`](./NORMATIVE.md), [`CHANGELOG.md`](./CHANGELOG.md), [`STATUS.md`](./schemas/v1.0.0/STATUS.md), [`adr/`](../../adr) | Four Markdown files and a decision log. No tooling, highest credibility per hour of anything in this table. | Hand-written. |
29
+
30
+ ---
31
+
32
+ ## Planned
33
+
34
+ Each carries a **trigger** — the condition that makes it worth building. None is scheduled;
35
+ building any of them before its trigger is carrying cost for no reader.
36
+
37
+ | Artifact | What it would be | Trigger |
38
+ |---|---|---|
39
+ | **Zod schemas** | Runtime-parsing schemas for TypeScript consumers, generated from the JSON Schema — never hand-written. | A consumer needs runtime parsing that the ajv path does not already cover. (The root README has promised these; this row is that promise, scheduled honestly.) |
40
+ | **Pydantic models** | The same for Python consumers. | The first Python consumer. Adding a Python toolchain to a pnpm monorepo before then is pure carrying cost. |
41
+ | **`vocabulary.ttl`** | RDFS/OWL in Turtle — the only format that can state "this field **is** that schema.org property" via `owl:equivalentProperty`, rather than the informal assertion `context.jsonld` makes today. ~200 lines, and the source any other RDF serialization would be generated from. | A consumer asks for it. Not before — see the RDF decline below. |
42
+ | **DAOIP-5 `grantPools` export adapter** | A one-way output adapter emitting grant-type opportunities as DAOIP-5 `grantPools` JSON, applying the three cardinality reductions documented in [`CROSSWALK.md`](./schemas/v1.0.0/CROSSWALK.md). Emits the object shape directly — DAOIP-5's own `@context` does not resolve. | A named consumer wants DAOIP-5 output. It is the only serialization in this niche with a real audience. |
43
+ | **`opportunity-package.schema.json`** (envelope) | `{specVersion, publisher, license, generatedAt, opportunities: [...]}` — because "this object is valid" and "this file is valid RFP Hub data" are different claims, and the envelope is the natural home for publisher, licence and generation time. Both closest domain analogues ship exactly two schemas. | The first bulk export or bulk publication path. Cheaper now than later, but it is an addition to a settled field plan, so it lands as its own decision. |
44
+ | **HTTP `Link` header context advertisement** | On `application/json` opportunity responses from the API: `Link: <…/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"`. Upgrades every response to linked data for zero payload bytes and no `@context` pollution. | **The canonical domain decision.** A conformant processor MUST fetch an advertised context, so advertising one is only worth doing once the URL is stable — the raw-repository URL used today would pin every consumer to a branch path. |
45
+
46
+ Two more, noted rather than tabled: **CSV codelists** of the registries (worth it when
47
+ non-developer curators or translators appear — today it adds a parser and a second
48
+ source-of-truth question), and a **separate publication tree** mirroring `$id` byte-for-byte
49
+ (the right end state, blocked on the same domain decision).
50
+
51
+ ### On identifiers, while the domain is undecided
52
+
53
+ **The canonical domain is undecided, and no identifier pretends otherwise.** Nothing is minted on
54
+ a domain the project does not own. `$id` — for the schema, the metaschema and the registry entry
55
+ schema — is a `raw.githubusercontent.com` URL that dereferences to exactly the bytes shipped here
56
+ (served as `text/plain`, the one known limitation). `@vocab` carries a `draft` segment so it reads
57
+ as provisional on sight.
58
+
59
+ **Swap `spec.config.json` + run `pnpm codegen` when the domain lands.** `baseUrl` and `vocabIri`
60
+ in [`spec.config.json`](./spec.config.json) are the only hand-written identifiers in the package;
61
+ every `$id`, the `@vocab`, and the self-identification examples in the schema are stamped from
62
+ them. `pnpm check` fails if any identifier is hand-written or points at the retired placeholder
63
+ domain, so the swap cannot be done halfway.
64
+
65
+ ---
66
+
67
+ ## Declined
68
+
69
+ | Not shipping | Why |
70
+ |---|---|
71
+ | **RDF serializations beyond the JSON-LD context** — RDF/XML, N-Triples, N-Quads, a standalone OWL file, SHACL, ShEx | Schema.org itself generates SHACL/ShEx and does not even serve them; both closest domain analogues ship no RDF at all. Each additional serialization is another file to keep in sync with no reader. If RDF is ever needed, `vocabulary.ttl` is the single source and the rest are one-line conversions from it. |
72
+ | **YAML-authored schema compiled to JSON** | The extra build step and the "which file is the source?" ambiguity start paying off somewhere past ~1500 lines. This schema is a few hundred. |
73
+ | **Content negotiation** — 303 redirects, `Accept` sniffing, PURL registration, VoID/DCAT descriptors | Verified that schema.org does none of these. A stable URL serving the correct `Content-Type` is the entire requirement. |
74
+ | **IANA media type registration** (`application/vnd.rfphub…`) | This is what made GeoJSON an interop primitive, but it only pays once something on the wire must distinguish standard payloads from arbitrary JSON. `application/schema+json` and `application/ld+json` cover today's needs. |
75
+ | **Dual `http://` and `https://` namespace IRIs** | Actively harmful. Schema.org's four-variant matrix exists only because it cannot escape URIs minted in 2011 — it is a scar, not a template. Pick `https` once. |
76
+ | **JSON Schema `$vocabulary` / a custom dialect** | Dialects exist for projects that add keywords to the schema *language*. This is an application schema; a ~25-line metaschema is the right weight. |
77
+ | **`x-crosswalk` / `x-daoip5` annotations inside the schema** | JSON Schema's direction of travel forbids undeclared unknown keywords. Crosswalk data stays in a sidecar document. |
78
+ | **A bundled / dereferenced schema copy** | One self-contained file using only local `#/$defs` pointers. There is nothing to dereference. |
79
+ | **One Markdown file per registry entry** (the layout larger bodies use) | That shape only pays off because a static site renders it into index pages, detail pages and feeds. Without the site it is N files nobody can read as a list. One JSON file per vocabulary is readable, diffable and machine-consumable today. |
80
+ | **A separate conformance-suite repository** | Repo overhead at this size. A versioned directory shipped in the package `files` array gets an external implementer exactly the same thing. |
81
+ | **Branch-per-release-line** with forward-porting scripts | Exists to serve several concurrent release lines and a large technical committee. Frozen sibling directories on `main` is the correct shape here. Borrow the discipline it protects, not the machinery. |
82
+ | **Sphinx / Read the Docs + gettext i18n** | The most expensive component of the largest comparable stack, justified there by a real multi-language mandate. Generated field tables do the useful half. |
83
+ | **A Zenodo DOI** | Cheap to mint and plausible if benchmark results ever get cited in ecosystem research. There is no citer today. Revisit when there is one. |
84
+ | **A machine-readable migration sidecar** (`migration/v1.0.0-recut.json` + a generated `MIGRATION.md`) | Built during the re-cut and removed the same day. It is a converter input with **zero external consumers** — the whole basis for re-cutting v1.0.0 in place was that nobody was holding pre-re-cut data. The [field mapping table in `CHANGELOG.md`](./CHANGELOG.md#field-mapping-old--new) carries the same information for the only audience that exists, a human reading the release entry, at a fraction of the machinery. Revisit if a consumer with pre-re-cut data appears. |
85
+ | **Registries for `ecosystems` and `networks`** | Both fields stay open with no registry. A registry over a list of chain names reads as an allowed-values list however carefully `NORMATIVE.md` words the "registered ≠ allowed" distinction, and it would put a review step in front of a newly launched chain for no interoperability gain. No decision in the re-cut required one. The three registries that remain govern vocabularies where two publishers genuinely need to mean the same thing by a value. |
package/CHANGELOG.md ADDED
@@ -0,0 +1,264 @@
1
+ # Changelog — RFP Hub Standard
2
+
3
+ This file records changes to the **standard** (the schema, the context, and the artifacts
4
+ governed by them). It is not the npm package's release log: package version and spec version
5
+ are separate axes, and only the spec version is tracked here.
6
+
7
+ Entries are grouped **Schema / Context / Tooling / Docs**.
8
+
9
+ ---
10
+
11
+ ## Spec v1.0.0 re-cut in place (2026-07-27)
12
+
13
+ **v1.0.0 was re-cut in place: the contents published under this version string differ from
14
+ those published before this date.** This is the one thing semantic versioning exists to
15
+ prevent, and it was done deliberately and once. The standard's initial milestone was never
16
+ declared complete and no external consumer had adopted the earlier shape, so a version bump
17
+ would have invented a migration story
18
+ nobody needed. **In-place re-cuts end here** — a second one would not be defensible.
19
+
20
+ - **Pre-re-cut bytes:** git tag `standard/spec-v1.0.0-precut-2026-07`, created before the first
21
+ re-cut commit. That tag is the only way to recover the earlier shape.
22
+ - **Rationale and alternatives considered:** [`adr/0001-recut-v1.0.0-in-place.md`](../../adr/0001-recut-v1.0.0-in-place.md)
23
+ - **The field decisions themselves:** [`adr/0002-v-next-field-recut.md`](../../adr/0002-v-next-field-recut.md)
24
+ - **Field-by-field mapping:** the [Field mapping (old → new)](#field-mapping-old--new) table
25
+ below. Rows are never removed from it — a field that disappeared from the schema keeps its
26
+ row here permanently, because that row is what tells a reader holding pre-re-cut data what
27
+ happened to it.
28
+
29
+ A document valid against the pre-re-cut schema is **not** valid against this one.
30
+
31
+ ### Schema
32
+
33
+ **Renamed**
34
+
35
+ - `type` → `fundingType`. Same six-value structural discriminator; the invariant is now
36
+ `opportunity[opportunity.fundingType]`.
37
+ - `funding.totalBudget` → `funding.budget`.
38
+ - `funding.amountDistributed` → `funding.allocated`. **Semantics change**, not just a rename:
39
+ *distributed* → *committed to date*. `remaining` is derived as `budget − allocated` and never
40
+ stored.
41
+ - `grant.fundingMechanism` (scalar enum) → `grant.fundingMechanisms` (array, `uniqueItems`),
42
+ with `matching` added to the value set — mechanisms co-occur, and a single-valued enum
43
+ recorded the fixed-grant-plus-matching-grant case wrongly.
44
+
45
+ **Removed**
46
+
47
+ - `organization` (superseded by `sponsoringOrganizations[]`).
48
+ - `source.url`, and the provenance `$def`'s `required: ["url"]`, not replaced. Link-back now
49
+ runs through `applicationUrl` alone. Consequence, stated plainly: "every entry is traceable
50
+ to an original posting" is now asserted by ingestion policy, not by validation.
51
+ - `closesAt` (superseded by `deadlines[]`).
52
+ - `funding.awardsToDate`.
53
+ - `rfp.issuingOrganization`, `rfp.budget`, `rfp.proposalDeadline`.
54
+ - `hackathon.registrationDeadline`, `hackathon.submissionDeadline`, `hackathon.startDate`,
55
+ `hackathon.endDate`.
56
+ - `accelerator.applicationDeadline`.
57
+
58
+ **Added**
59
+
60
+ - `sponsoringOrganizations[]` (required, `minItems: 1`; `[0]` is the primary/display org) and
61
+ `operatingOrganizations[]` (who runs intake/process).
62
+ - `organization.contacts[]` — `{name?, role?, telegram?, email?}`, every field optional, `{}`
63
+ validates deliberately.
64
+ - `deadlines[]` — `{type: "fixed"|"rolling", date?, label?}`. **Every** per-type-block date
65
+ folds in, event boundaries included, distinguished by `label`. `date` is required and
66
+ non-null when `type` is `"fixed"`, enforced by `if`/`then`.
67
+ - `milestones[]` and `$defs/milestone` — `{title?, amount?, criteria?}`, valid on any
68
+ `fundingType`, array order is the sequence, no date field. Marked `x-stability: provisional`.
69
+ - `eligibility` — an open key→value map of plain strings.
70
+ - `prerequisites`, `resourceLinks`, `serviceAgreement` — three optional top-level free-text
71
+ strings. `serviceAgreement` is marked `x-stability: provisional`.
72
+ - `grant.programModel` — an open string, marked `x-stability: provisional`.
73
+ - Optional top-level `$schema`, `@context` and `@type`, so an instance can identify itself
74
+ against `additionalProperties: false` and be consumed as linked data.
75
+ - `x-stability` / `x-since` / `x-deprecated` annotations, legalised by a new metaschema. They
76
+ extend the 2020-12 vocabulary rather than duplicating it: deprecation is signalled by the
77
+ **native** `deprecated` keyword, and `x-deprecated` may only accompany it to carry the
78
+ since/replacedBy/note metadata JSON Schema has no keyword for.
79
+
80
+ **Newly enforced**
81
+
82
+ - **One block per funding type.** Each of the six `if`/`then` branches now also forbids every
83
+ non-matching type block. A `grant` record carrying an `rfp` object used to validate; it now
84
+ fails. `opportunity[opportunity.fundingType]` is a guarantee rather than an expectation.
85
+ - `specVersion` stays `const "1.0.0"`. It was briefly loosened to a `^1\.0\.\d+$` patch-line
86
+ pattern during this release and **reverted the same day**: a loosening is a breaking change
87
+ under this project's own bidirectional definition, and there is no patch process in existence
88
+ for it to serve. Revisit at the first real editorial patch. See
89
+ [`adr/0003`](../../adr/0003-instance-self-identification-and-version-pattern.md).
90
+
91
+ **Schema-file hygiene**, from an audit of the file itself against JSON Schema, OpenAPI and
92
+ OCDS/360Giving practice rather than against the field plan:
93
+
94
+ - **Descriptions are informative prose only.** Every all-caps BCP 14 keyword was removed from a
95
+ description and either keyword-enforced or reworded. `NORMATIVE.md` says the schema's
96
+ *keywords* are the constraints; a MUST in a description that no keyword backs is a prose
97
+ constraint hiding in the schema. The one genuinely unenforceable rule — `milestones[].amount`
98
+ following `funding.currency`, a dependency that crosses two objects — now says so at the point
99
+ it is stated and points at FIELDS.md.
100
+ - **Editorial notes moved to `$comment`.** Design rationale, deliberate omissions, and traps a
101
+ future editor might "fix" (why `hackathon.prizes` is not `uniqueItems`, why there is no
102
+ `rfp.outOfScope`, why `organization` has no `extensions`) are maintainer notes, not consumer
103
+ documentation, and they no longer leak into the generated field reference.
104
+ - **`examples` added** to the fields whose shape is not obvious from their type: `eligibility`,
105
+ `deadlines`, `milestones`, `serviceAgreement`, `grant.fundingMechanisms`, `grant.programModel`,
106
+ and a dozen others.
107
+ - **Descriptions completed** — every declared property and every `$def` now carries one, checked
108
+ in CI. Twenty-eight were previously bare (`socialLinks.*`, `teamSize.*`, most of `accelerator`
109
+ and `vcFund`, and the roots of `monetaryAmount`, `amountRange`, `teamSize`, `socialLinks`,
110
+ `prize`, `bounty`, `vcFund`).
111
+ - **Constraints tightened where they were merely absent:** `format: "uri"` on all seven
112
+ `socialLinks` values (they are links, not handles — `contact.telegram` remains a handle and is
113
+ documented as the exception); `uniqueItems` on `deadlines` and `rfp.requirements`;
114
+ `minLength: 1` on the items of `hackathon.tracks`, `bounty.skills`, `vcFund.portfolio` and
115
+ `rfp.requirements`, matching the other free-text arrays. No committed example changed.
116
+ - **`$defs` reordered** to match the order the generated field reference renders them in, and a
117
+ root `$comment` now records the property-ordering convention, the `x-stability` default, and
118
+ why `additionalProperties: false` is safe alongside `allOf` here.
119
+
120
+ **Also cleaned:** an internal issue-tracker ID that had leaked into the normative `ecosystems`
121
+ description (and from there into every generated type file). A CI lint now fails on any
122
+ recurrence — the standard is CC0 and designed to be embedded and forked, so a leak travels.
123
+
124
+ #### Field mapping (old → new)
125
+
126
+ Every field that changed name, shape or existence in the re-cut. **No row is ever removed from
127
+ this table.** Two rows carry a semantics change rather than a mechanical move — those are the
128
+ ones a converter cannot get right by renaming keys, and they are called out in the notes.
129
+
130
+ | Old | New | Kind | Note |
131
+ |---|---|---|---|
132
+ | `type` | `fundingType` | renamed | Same six values; the invariant becomes `opportunity[opportunity.fundingType]`. |
133
+ | `organization` | `sponsoringOrganizations[0]` | reshaped | Single required object → required array, `minItems: 1`. Wrap to migrate; `[0]` is primary/display. |
134
+ | — | `operatingOrganizations` | added | Who runs intake/process, as distinct from who backs it. |
135
+ | — | `sponsoringOrganizations[].contacts` | added | `{name?, role?, telegram?, email?}`, all optional. |
136
+ | `source.url` | `applicationUrl` | removed | Partial carry-over only. `applicationUrl` is an intake channel, not necessarily the original posting; the `sameAs` sense has no successor. Put a load-bearing original posting in `resourceLinks`. |
137
+ | `source` `required: ["url"]` | — | removed | The provenance `$def` now has no required property. `"source": {}` validates. |
138
+ | `closesAt` | `deadlines[]` | reshaped | **Semantics change.** A single scalar becomes an array of `{type, date?, label?}`. Migrate a non-null value to `[{type:"fixed", date, label:"application"}]`; null migrates to no entry. Sorting/filtering must derive a next-deadline value at the consumer layer, and **selection is by `label`, never by array position** — the head of the array may be a hackathon's start date. |
139
+ | `funding.totalBudget` | `funding.budget` | renamed | Pure rename. |
140
+ | `funding.amountDistributed` | `funding.allocated` | reshaped | **Semantics change.** *Distributed* → *committed to date*. Copying the old value across asserts something different from what the source said. `remaining` is derived as `budget − allocated` and never stored; `disbursed`/`delivered` are not modelled. |
141
+ | `funding.awardsToDate` | — | removed | A per-award count in an otherwise program-level envelope. No successor. |
142
+ | `grant.fundingMechanism` | `grant.fundingMechanisms` | reshaped | Scalar enum → array (`uniqueItems`), plus a new `matching` value. Wrap to migrate. DAOIP-5's field stays singular, so that mapping becomes first-element-or-join. |
143
+ | — | `grant.programModel` | added | Open string; conventional values in `registries/program-models.json`. Provisional. |
144
+ | `rfp.issuingOrganization` | `sponsoringOrganizations[0].name` | reshaped | Free-text issuer superseded by the structured organisation. |
145
+ | `rfp.budget` | `funding` | reshaped | RFP-local `{amount, currency}` → the top-level envelope. |
146
+ | `rfp.proposalDeadline` | `deadlines[]` | reshaped | Label `application`. |
147
+ | `hackathon.registrationDeadline` | `deadlines[]` | reshaped | Label `registration`. |
148
+ | `hackathon.submissionDeadline` | `deadlines[]` | reshaped | Label `submission`. |
149
+ | `hackathon.startDate` | `deadlines[]` | reshaped | Label `event start`. Event boundaries fold in literally — one date model, not two. |
150
+ | `hackathon.endDate` | `deadlines[]` | reshaped | Label `event end`. |
151
+ | `accelerator.applicationDeadline` | `deadlines[]` | reshaped | Label `application`. |
152
+ | — | `eligibility` | added | Open key→value map of plain strings. |
153
+ | — | `prerequisites` | added | Free text: what a *proposal* must contain. Distinct from `rfp.requirements`, which is what the *work* must deliver. |
154
+ | — | `resourceLinks` | added | One free-form string, deliberately not an array of URIs. |
155
+ | — | `serviceAgreement` | added | Free text; presence is the signal. Valid on any `fundingType`. Provisional. |
156
+ | — | `milestones[]` | added | `{title?, amount?, criteria?}`; array order is the sequence; no date field. Provisional. |
157
+ | — | `$schema` / `@context` / `@type` | added | Optional self-identification, permitted against `additionalProperties: false` and ignored by validation. |
158
+ | `allOf` (matching block required) | `allOf` (matching required, others forbidden) | reshaped | One block per funding type is now enforced. A `grant` carrying an `rfp` object used to validate and now fails. |
159
+
160
+ #### Identifiers
161
+
162
+ **No identifier is minted on a domain the project does not own.** The earlier `$id` and `@vocab`
163
+ pointed at a domain nobody controls — a URL that looks final, never dereferences, and is inherited
164
+ by every downstream fork of a CC0 artifact.
165
+
166
+ - **`$id`** — for the schema, the metaschema and the registry entry schema — is now a
167
+ `raw.githubusercontent.com` URL that **dereferences to exactly the bytes shipped here**. Known
168
+ limitation: GitHub serves it as `text/plain`, not `application/schema+json`. A URL resolving to
169
+ the real document with the wrong Content-Type beats one resolving to a parking page.
170
+ - **`@vocab`** carries a `draft` path segment mirroring this version's maturity, so it reads as
171
+ provisional on sight. It does not dereference, and `STATUS.md` says so rather than hiding it.
172
+ - **`spec.config.json` is the only place a base URL is written.** All three `$id`s, the `@vocab`
173
+ and the schema's own self-identification examples are stamped from it, so adopting a canonical
174
+ domain is a one-line edit plus `pnpm codegen`. `pnpm check` fails if an identifier is
175
+ hand-written or points at the retired placeholder domain, so the swap cannot be done halfway.
176
+
177
+ ### Context
178
+
179
+ - `@vocab` moved off the version-scoped IRI to a versionless one. Under
180
+ an in-place re-cut, `…/v1.0.0#organization` would have denoted two different things at two
181
+ different times. Version the context *document*, never the term IRIs.
182
+ - `@protected: true` added to the stable core terms.
183
+ - Coverage extended from 13 terms to every top-level property of the new shape, with
184
+ schema.org and DAOIP-5 mappings where they exist — `sponsoringOrganizations → schema:funder`,
185
+ `operatingOrganizations → schema:sponsor`, `budget → daoip5:totalGrantPoolSize`,
186
+ `fundingMechanisms → daoip5:grantFundingMechanism`. Terms for deleted fields removed.
187
+ - A CI check now fails on context↔schema drift in either direction, which was previously
188
+ invisible.
189
+
190
+ ### Tooling
191
+
192
+ - **`spec.config.json`** — one metadata block holding the spec's identity (`specVersion`,
193
+ `schemaDir`, `id`, `vocabIri`, `status`, `recutDate`). The version string used to be
194
+ hand-written in seven places; it is now hand-written in one.
195
+ - **`scripts/codegen.mjs`** extended to stamp that identity into the schema `$id`, the
196
+ `specVersion` constant, the schema description, the context's `@vocab` and `SPEC_VERSION` —
197
+ and to generate `registries/index.json`, `schemas/index.json` and the `FIELDS.md` field
198
+ tables alongside the TypeScript types. `codegen:check` covers all seven artifacts.
199
+ - **`scripts/check-spec.mjs`** (`pnpm check`) — publication rules: context↔schema drift,
200
+ version-string agreement against `spec.config.json`, and the source-neutrality lint.
201
+ - **`registries/`** — `eligibility-keys`, `deadline-labels` and `program-models`, plus
202
+ `entry.schema.json` and a generated `index.json`. Entries are never deleted; they are
203
+ deprecated with a `replacedBy` pointer. `ecosystems` and `networks` deliberately have **no**
204
+ registry: they stay plain open lists, because a registry there reads as an allowed-values list
205
+ despite what `NORMATIVE.md` says, and no decision required one.
206
+ - **`conformance/v1.0.0/{pass,fail}/`** — whole documents, one file per rule, named after the
207
+ rule, shipped in the package `files` array so external implementers can run them. Replaces
208
+ the 2 valid + 4 invalid fixtures that lived inside `packages/validate/test/` where nobody
209
+ outside the repo could reach them.
210
+ - **`meta/rfphub-schema.meta.json`** — metaschema constraining our schema file's shape and
211
+ legalising the three `x-` annotation keywords, validated in CI.
212
+ - **`rfphub-validate` gains an advisory tier** (`src/checks/`): warnings reported separately
213
+ from schema errors, count-phrased in text output, promoted to a failing exit code by
214
+ `--strict`. Seed checks cover unregistered eligibility keys, deadline labels and program
215
+ models, plus the milestone-amount-without-envelope-currency rule that JSON Schema cannot
216
+ express because it crosses two objects. The registries and this tier are a pair: either one
217
+ alone is worth less than neither.
218
+
219
+ ### Docs
220
+
221
+ - **`FIELDS.md` rewritten for the new shape**, and half of it is now generated: the field
222
+ reference tables — type, requiredness, description and governing registry for every property —
223
+ are rendered from the schema into a marker block by `pnpm codegen` and gated by
224
+ `codegen:check`. A hand-maintained field table drifts from the schema inside one release. The
225
+ narrative around them carries the BCP 14 keyword boilerplate, the documentation conventions the
226
+ schema cannot enforce (sponsor ≠ source of funds, `applicationUrl` = whatever the submission
227
+ channel is, `prerequisites` vs `rfp.requirements`, the three free-text siblings,
228
+ select-deadlines-by-label, milestone currency and due dates, single-currency-at-the-envelope),
229
+ and a conformance section.
230
+ - **Design principle #2 reworded.** It used to read "every entry MUST carry a `source.url`" — a
231
+ MUST on a field this release deletes. It now says what is actually true: provenance
232
+ completeness is asserted by ingestion policy, not by schema validation.
233
+ - **`CROSSWALK.md` remapped** for every renamed, removed and added field, with "no equivalent"
234
+ stated honestly rather than left blank, and the three cardinality divergences from DAOIP-5
235
+ (single vs. multi-currency, one close date vs. labelled deadline array, scalar vs. array
236
+ funding mechanism) written out. The claim that "no v1.0.0 field had to change to align" is
237
+ retired. **Its JSON-LD usage example now validates** — it previously carried `@context` and
238
+ `@type` against `additionalProperties: false`, so the project's own published example failed
239
+ against the project's own schema.
240
+ - **`BENCHMARK.md` corrected** to distinguish the pre-re-cut corpus measurement (311 pulled,
241
+ 289 validated — not re-run) from the current claim (28 committed examples, converted and
242
+ re-validated against the re-cut schema).
243
+ - **New governance and process set:** `PROCESS.md` (feature stages; the operational, bidirectional
244
+ definition of breaking with JSON Schema's indeterminate-state carve-out; deprecation;
245
+ registry registration criteria; errata triage labels; release checklist), `NORMATIVE.md`
246
+ (normative vs. informative, and the rule that informative content may be corrected outside the
247
+ release cycle), `ARTIFACTS.md` (every artifact shipped/planned/declined, with its generator and
248
+ its trigger), `schemas/v1.0.0/STATUS.md` (maturity, the honest re-cut paragraph, feedback
249
+ channel), and `GOVERNANCE.md` at the repo root (editors, decision rule, review windows, appeal
250
+ path, and an explicit list of what this project deliberately does not have).
251
+ - **`adr/`** added at the repo root with a MADR template, an index, and three records: the
252
+ in-place re-cut, the field re-cut with its accepted costs, and the two tooling-driven additions
253
+ (instance self-identification accepted; the `specVersion` patch-line pattern proposed and
254
+ reverted the same day).
255
+ - **`schemas/index.json`** (generated) — a machine-readable index of published spec versions with
256
+ a `latest` pointer. Free while there is one version and impossible to retrofit cleanly.
257
+ - Root `README.md` link-back language corrected to describe the application link, and
258
+ `CONTRIBUTING.md`'s versioning section rewritten — it previously promised "additive → minor,
259
+ breaking → new major at a new URL", the exact policy this release breaks, and forward-referenced
260
+ governance documents that did not exist. They exist now and it links them.
261
+ - All 28 curated examples converted to the new shape. Where the conversion would have dropped
262
+ a real value, it was rehomed rather than lost: `source.url` became `applicationUrl` on the
263
+ seven records that had none and `resourceLinks` on the six where `applicationUrl` already
264
+ pointed elsewhere; the one non-zero `funding.awardsToDate` was preserved under `extensions`.
package/LICENSE ADDED
@@ -0,0 +1,121 @@
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
package/NORMATIVE.md ADDED
@@ -0,0 +1,77 @@
1
+ # What is normative in the RFP Hub Standard
2
+
3
+ Nobody infers this. It has to be written down, so this is the document that says which artifacts
4
+ carry authority and which ones merely describe it.
5
+
6
+ ## Normative
7
+
8
+ These define the standard. A claim of conformance is a claim about these files.
9
+
10
+ | Artifact | What it governs |
11
+ |---|---|
12
+ | [`schemas/v1.0.0/opportunity.schema.json`](./schemas/v1.0.0/opportunity.schema.json) | **The definition.** A document conforms to v1.0.0 when it validates against this file. |
13
+ | [`registries/`](./registries) — the **registered values** | The conventional values of the three open vocabularies the standard governs by registry: `eligibility` keys, `deadlines[].label`, `grant.programModel`. See the important qualification below. |
14
+ | [`conformance/v1.0.0/`](./conformance/v1.0.0) | The published pass/fail cases. An implementation is conformant with respect to the suite when it accepts everything in `pass/` and rejects everything in `fail/`. |
15
+ | [`spec.config.json`](./spec.config.json) | The spec's identity — version, `$id`, vocabulary IRI, maturity. |
16
+ | [`schemas/v1.0.0/context.jsonld`](./schemas/v1.0.0/context.jsonld) | The term IRIs a document expands to when read as linked data. |
17
+
18
+ **The qualification on registries.** A registry is normative about **what a value means**, not
19
+ about **which values are allowed**. The schema deliberately keeps these fields open: an
20
+ unregistered deadline label, eligibility key or program model is **valid data** and produces a
21
+ *warning*, never a validation error. Registering a value fixes its meaning so two publishers
22
+ using it mean the same thing. Entries are never deleted — see [`PROCESS.md`](./PROCESS.md).
23
+
24
+ **Why only three.** `ecosystems` and `networks` are open lists too, and deliberately have **no**
25
+ registry. That distinction above is a fine one, and a registry over a list of chain names would
26
+ be read as an allowed-values list no matter how this document words it — while also putting a
27
+ review step in front of a newly launched chain for no interoperability gain. The three
28
+ vocabularies that *are* registry-governed are the ones where two publishers writing different
29
+ strings for the same concept produces genuinely uncomparable data.
30
+
31
+ **The qualification on the conformance suite.** Passing the suite is **evidence of conformance,
32
+ not the definition of it**. The schema is the definition. The suite asserts nothing about which
33
+ error an implementation reports, how many, or in what order.
34
+
35
+ ## Informative
36
+
37
+ These explain the standard. They are corrigible, and correcting them is not a spec change.
38
+
39
+ | Artifact | What it is |
40
+ |---|---|
41
+ | [`schemas/v1.0.0/FIELDS.md`](./schemas/v1.0.0/FIELDS.md) — the **prose** | Field explanations, documentation conventions, status semantics, consumer guidance. Its generated field tables are derived from the schema and cannot disagree with it; the narrative around them is human-written and can. |
42
+ | [`schemas/v1.0.0/CROSSWALK.md`](./schemas/v1.0.0/CROSSWALK.md) | Mappings to DAOIP-5 and schema.org/Grant. |
43
+ | [`schemas/v1.0.0/BENCHMARK.md`](./schemas/v1.0.0/BENCHMARK.md) | A measurement against real data. |
44
+ | [`schemas/v1.0.0/examples/`](./schemas/v1.0.0/examples) | Curated real-world documents. Illustrations, **not** conformance cases. |
45
+ | [`CHANGELOG.md`](./CHANGELOG.md) (including its field-mapping table), [`ARTIFACTS.md`](./ARTIFACTS.md), [`schemas/v1.0.0/STATUS.md`](./schemas/v1.0.0/STATUS.md) | The record of what changed, what ships, and where this version stands. |
46
+ | READMEs, [`adr/`](../../adr) | Orientation and decision history. |
47
+ | [`PROCESS.md`](./PROCESS.md), [`GOVERNANCE.md`](../../GOVERNANCE.md) | Normative about **the project's process**, not about the data format. A document does not conform or fail to conform to them. |
48
+
49
+ ## The two rules that follow
50
+
51
+ 1. **The schema takes precedence over conflicting prose.** If [`FIELDS.md`](./schemas/v1.0.0/FIELDS.md)
52
+ describes a constraint the schema does not enforce, or describes it differently, **the schema
53
+ is right and the prose is a defect**. Report it as an erratum; do not implement the prose.
54
+
55
+ One deliberate exception, stated in both places: a handful of rules in FIELDS.md are
56
+ **normative in intent but not schema-expressible** — the clearest is that
57
+ `milestones[].amount` must be denominated in `funding.currency`, a dependency that crosses two
58
+ objects. Those are enforced by the validator's advisory tier and by ingestion policy, and each
59
+ one is labelled as such at the point it is stated. They are requirements on *publishers*, not
60
+ conditions of schema validity.
61
+
62
+ 2. **Informative content may be corrected outside the release cycle.** A wrong crosswalk row, a
63
+ stale benchmark number, an unclear sentence in FIELDS.md, a broken link — all of these can be
64
+ fixed by an ordinary PR at any time, without a spec version, without a comment window, and
65
+ without a changelog entry beyond the commit. This is the whole point of drawing the line: it
66
+ keeps documentation improvable at the speed documentation needs to be improvable, while the
67
+ schema stays under change control.
68
+
69
+ The inverse also holds. A change to any artifact in the normative table — **including adding,
70
+ deprecating or re-describing a registry value** — is a spec change and follows
71
+ [`PROCESS.md`](./PROCESS.md).
72
+
73
+ ## What this does not decide
74
+
75
+ Whether a *field* is stable is a separate axis from whether a *document* is normative. Per-field
76
+ maturity is carried by the `x-stability` annotation inside the schema and governed by the feature
77
+ stages in [`PROCESS.md`](./PROCESS.md).