@seedprotocol/rdf 0.0.0-stage → 0.8.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented in this file.
4
+
5
+ ## Unreleased
6
+
7
+ - New package. `exportSeeds(seedUids, { chainId })` reads seeds with `getSeedAttestations` from @seedprotocol/query and returns their state graph: each seed's values at its head Version, with IRIs minted from the on-chain schemas (`{base}c/{model}`, `{base}p/{easType}/{easName}`, `{base}eas/{chainId}/{uid}`). `toQuads` gives RDF/JS quads; `toNQuads`, `toTurtle` and `toJsonLd` write RDF 1.1 N-Quads, Turtle and JSON-LD 1.1. `seedVocabulary` gives the Seed vocabulary in Turtle. The base IRI defaults to `PROVISIONAL_BASE_IRI`, which isn't registered yet.
8
+ - New `lens` option on `exportSeeds` and `stateGraph`: `{ schema, sidecar }` maps models and properties, by id, to terms such as `schema:headline`, and types Numbers (`xsd:integer`, `xsd:decimal`, canonical `xsd:double`) and Dates (`xsd:dateTime`). `StateGraph.warnings` lists terms it couldn't use.
9
+ - `toShacl(lens)` writes SHACL 2017 shapes in Turtle for exports made with that lens: a `sh:NodeShape` per model targeting its class, and per property its predicate, `sh:datatype`, `sh:maxCount 1`, `sh:minCount 1` when `required`, and `rdf:List` checks for Lists.
10
+ - `sidecarContext(lens)` generates a type-scoped JSON-LD `@context` from the sidecar's `terms` and the schema's current names. `checkSidecar` lists stale terms, unreadable terms and unmapped models and properties; `syncSidecar` regenerates `@context` and sets the new optional `schemaVersion`; `suggestTerms` drafts schema.org and dcterms terms for common names.
11
+ - An export with a lens includes a `prov:Activity` at `{base}export/{sha256}` with `prov:used <ni:///sha-256;…>`, the hash of the sidecar's `schemaId`, `terms` and `@context` as canonical JSON. The same sidecar always gives the same node; it has no time or agent.
12
+ - `exportSeeds` takes `asOf` (unix seconds) and exports what EAS held then, the same attestations as a reproducible archive bag with that `Seed-Bag-As-Of`. It reads EAS and refuses `source: 'local'`.
13
+ - The state graph has the seed's Version history: each live Version up to the head is a `seed:Version` with `prov:generatedAtTime`, `prov:wasAttributedTo` and `prov:wasRevisionOf` the Version before. Revoked Versions are left out.
14
+ - Properties are written in name order, so the same state gives the same bytes from every writer whatever order EAS returns attestations in.
15
+ - New `seed-rdf` bin: `seed-rdf check` (exits 1 on stale or unreadable terms, for CI), `seed-rdf sync [--write]` and `seed-rdf suggest [--write]`.
package/README.md CHANGED
@@ -1,3 +1,192 @@
1
- # Temporary Holding Version
1
+ # @seedprotocol/rdf
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ RDF export for [Seed Protocol](https://seedprotocol.io) items. It reads seeds through
4
+ [`@seedprotocol/query`](https://www.npmjs.com/package/@seedprotocol/query) and gives their current
5
+ values as a graph: RDF/JS quads, N-Quads, Turtle or JSON-LD. It doesn't depend on the SDK.
6
+
7
+ Every IRI comes from what is on chain, so an app gets RDF for its own models without registering
8
+ anything. **The base IRI is provisional:** `https://w3id.org/seedprotocol/` isn't registered yet,
9
+ so don't write exports into anything you can't replace later, such as an archive on Arweave.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @seedprotocol/rdf
15
+ ```
16
+
17
+ ## Export
18
+
19
+ ```ts
20
+ import { exportSeeds, toTurtle, toJsonLd, toNQuads, toQuads } from '@seedprotocol/rdf'
21
+
22
+ const graph = await exportSeeds([seedUid], { chainId: 11155420 })
23
+
24
+ toTurtle(graph) // Turtle (RDF 1.1)
25
+ toNQuads(graph) // N-Quads
26
+ toJsonLd(graph) // JSON-LD 1.1 object with an inline @context
27
+ toQuads(graph) // RDF/JS quads for N3.js, Comunica, Oxigraph or rdf-ext
28
+ ```
29
+
30
+ `exportSeeds` takes the `source` and `pins` options of `getSeeds`, plus:
31
+
32
+ | Option | |
33
+ |---|---|
34
+ | `asOf` | Unix seconds. The graph EAS held then: attestations made later are left out, and one revoked later counts. Reads EAS, so `source: 'local'` is refused. See [A graph beside an archive bag](#a-graph-beside-an-archive-bag). |
35
+ | `chainId` | The chain the seeds were attested on. Required: EAS uids don't include it. |
36
+ | `base` | Base of the minted IRIs, ending in `/`. Default `PROVISIONAL_BASE_IRI`. |
37
+ | `contentUrl` | `(transactionId) => string`, an `https://` URL for stored content, written as `schema:contentUrl`. |
38
+ | `lens` | `{ schema, sidecar }`: maps your models and properties to terms and types Numbers and Dates. See [Lens](#lens). |
39
+
40
+ To export seeds you have already read, pass the entries of `getSeedAttestations` to `stateGraph`.
41
+
42
+ ## What the graph contains
43
+
44
+ For each seed, at its head Version (or the pinned one):
45
+
46
+ | Seed | RDF |
47
+ |---|---|
48
+ | The seed | `{base}eas/{chainId}/{seedUid}`, typed `seed:Seed` and `{base}c/{model}` |
49
+ | Its attester and creation time | `prov:wasAttributedTo <did:pkh:eip155:{chainId}:{address}>`, `prov:generatedAtTime` |
50
+ | Its head Version | `seed:currentVersion` |
51
+ | Its Versions up to the head | Each a `seed:Version` with `seed:versionOf` the seed, `prov:generatedAtTime`, `prov:wasAttributedTo` its attester and `prov:wasRevisionOf` the Version before. Revoked Versions and Versions from other attesters are left out. |
52
+ | A property `string title` | `{base}p/string/title` with the newest value the seed's attester attested |
53
+ | A relation | The related seed's IRI |
54
+ | A list of relations | An RDF list of seed IRIs, in order |
55
+ | An Image, File, Html or Json seed | `seed:content <ar://{transactionId}>` beside its `storage_transaction_id` |
56
+
57
+ Literals are typed from the EAS type: `bool` is `xsd:boolean`, integer types are `xsd:integer`,
58
+ `bytes` values are `xsd:hexBinary`, and everything else is `xsd:string`. A Number is an EAS
59
+ `string` (ADR 0010) and a Date a `uint256` of seconds, so without a [lens](#lens) they come out
60
+ as `xsd:string` and `xsd:integer`. A removed value (ADR 0021) is left out. The graph has no blank
61
+ nodes except the cells of lists.
62
+
63
+ `seedVocabulary()` returns the `seed:` terms in Turtle.
64
+
65
+ The same seeds, `asOf`, options and sidecar give the same bytes from `toJsonLd`, `toTurtle` and
66
+ `toNQuads`, whatever order EAS returns attestations in. Seeds are written in the order you pass
67
+ them.
68
+
69
+ ## A graph beside an archive bag
70
+
71
+ A reproducible archive bag from `@seedprotocol/archive` records the seed, the time it covers and
72
+ the chain in bag-info.txt. Export with that time and the graph holds the same attestations as the
73
+ bag, plus the Version history. Anyone can rebuild it from EAS and compare bytes.
74
+
75
+ ```ts
76
+ import { readBagInfo } from '@seedprotocol/archive'
77
+ import { exportSeeds, toJsonLd } from '@seedprotocol/rdf'
78
+
79
+ const info = readBagInfo(bag)
80
+ const graph = await exportSeeds([info.get('Seed-Uid')!], {
81
+ chainId: Number(info.get('EAS-Chain-Id')),
82
+ asOf: Date.parse(info.get('Seed-Bag-As-Of')!) / 1000,
83
+ lens,
84
+ })
85
+ const jsonld = JSON.stringify(toJsonLd(graph))
86
+ ```
87
+
88
+ The graph goes beside the bag, not in it: the bag's `tagmanifest-sha256.txt` hash and its anchor
89
+ don't cover it, and its IRIs use the provisional base. Neither package imports the other; your
90
+ app reads bag-info with archive and passes the three values to rdf.
91
+
92
+ rdf doesn't read the Html's h-entry. To give a title as `dcterms:title`, map the property in the
93
+ sidecar:
94
+
95
+ ```json
96
+ {
97
+ "schemaId": "blog-schema-id",
98
+ "@context": { "dcterms": "http://purl.org/dc/terms/" },
99
+ "terms": {
100
+ "<title property id>": "dcterms:title",
101
+ "<author property id>": "dcterms:creator",
102
+ "<published at property id>": "dcterms:issued"
103
+ }
104
+ }
105
+ ```
106
+
107
+ ## Lens
108
+
109
+ A protocol predicate such as `{base}p/string/title` says what is on chain, not what it means. A
110
+ lens adds that: pass your Schema and a sidecar, a JSON file that lives beside it.
111
+
112
+ ```json
113
+ {
114
+ "schemaId": "my-schema-id",
115
+ "@context": { "schema": "https://schema.org/", "dcterms": "http://purl.org/dc/terms/" },
116
+ "terms": {
117
+ "<Post model id>": "schema:BlogPosting",
118
+ "<title property id>": "schema:headline",
119
+ "<publishedAt property id>": "schema:datePublished",
120
+ "<authors property id>": "dcterms:creator"
121
+ }
122
+ }
123
+ ```
124
+
125
+ ```ts
126
+ const graph = await exportSeeds([seedUid], { chainId, lens: { schema, sidecar } })
127
+ graph.warnings // terms it couldn't use, values that don't fit their type
128
+ ```
129
+
130
+ `terms` is keyed by the `id` of a model or property in the Schema file, so renaming either keeps its
131
+ term. Values are a prefixed name, using the top-level strings of `@context`, or an absolute IRI.
132
+
133
+ - A mapped model's class replaces `{base}c/{model}`, and a mapped property's term replaces its
134
+ protocol predicate. `seed:Seed` stays, and anything unmapped keeps its protocol IRI.
135
+ - A Number is typed per value: `xsd:integer`, `xsd:decimal`, or `xsd:double` in its canonical form
136
+ (`1e+21` becomes `1.0E21`). A value that isn't a number stays an `xsd:string`, with a warning.
137
+ - A Date is an `xsd:dateTime` in UTC.
138
+ - On-chain names are matched to the Schema the way sync matches them, including `nameMatch`. An
139
+ attestation made under a name the Schema no longer matches keeps its protocol predicate.
140
+
141
+ The graph also records which mapping it used: a `prov:Activity` at `{base}export/{sha256}`
142
+ with `prov:used <ni:///sha-256;…>` (RFC 6920), the SHA-256 of the sidecar's `schemaId`, `terms`
143
+ and `@context` as JSON with sorted keys. The node stands for the mapping, not for one export, so
144
+ two exports with the same sidecar name the same node, and it has no time or agent. Like every
145
+ minted IRI it uses the provisional base; keep it out of anything anchored.
146
+
147
+ A sidecar whose `schemaId` isn't the Schema's `id` throws. A term with no matching id or a prefix
148
+ `@context` doesn't define is a warning, and the export goes on without it.
149
+
150
+ ## SHACL
151
+
152
+ ```ts
153
+ import { toShacl } from '@seedprotocol/rdf'
154
+
155
+ const shapes = toShacl({ schema, sidecar }) // Turtle
156
+ ```
157
+
158
+ `toShacl` describes the graphs `exportSeeds` gives with the same lens, in SHACL 2017. Each model is a
159
+ `sh:NodeShape` targeting the class its seeds get, and each property a property shape on the
160
+ predicate they get:
161
+
162
+ | Schema | Shape |
163
+ |---|---|
164
+ | Text, Boolean, Date | `sh:datatype` `xsd:string`, `xsd:boolean`, `xsd:dateTime` |
165
+ | Number | `sh:or` of `xsd:integer`, `xsd:decimal` and `xsd:double` |
166
+ | Relation, Image, File, Html, Json | `sh:nodeKind sh:IRI` |
167
+ | List | every cell of the `rdf:List` has one `rdf:first` and one `rdf:rest`; a List of relations' members are IRIs |
168
+ | `required: true` | `sh:minCount 1` |
169
+
170
+ Every property has `sh:maxCount 1`. A relation's class isn't checked, because the related seed is
171
+ often not in the export. For shapes over the protocol IRIs, pass a sidecar with empty `terms`.
172
+
173
+ ## Keeping the sidecar current
174
+
175
+ The sidecar's own `@context` can carry a type-scoped context with your local names, for tools
176
+ that read the file as JSON-LD. It's generated from `terms`, so it goes stale when a model or
177
+ property is renamed; `terms` doesn't, because it's keyed by id.
178
+
179
+ ```bash
180
+ seed-rdf check # stale and unreadable terms, unmapped properties; exits 1 on the first two
181
+ seed-rdf sync --write # regenerate @context and schemaVersion from terms and the current names
182
+ seed-rdf suggest # draft schema.org / dcterms terms for unmapped names such as title or author
183
+ seed-rdf suggest --write # add them to the sidecar, creating it if needed
184
+ ```
185
+
186
+ Each command reads `seedSchema.json` and `seedSchema.context.jsonld` in the working directory;
187
+ `--schema` and `--sidecar` take other paths. `sync` and `suggest` print what they would write
188
+ unless given `--write`, and `sync` keeps stale terms: removing a mapping is your call. The same
189
+ functions are exported as `checkSidecar`, `syncSidecar`, `suggestTerms` and `sidecarContext`.
190
+
191
+ `seedVitePlugin` from `@seedprotocol/vite` runs the check on the dev server and warns when the
192
+ schema or sidecar changes.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ import { a as e, i as t, n, t as r } from "./sidecar-BjHSnqHK.js";
3
+ import i from "node:process";
4
+ import { readFile as a, writeFile as o } from "node:fs/promises";
5
+ import s from "node:path";
6
+ import { parseArgs as c } from "node:util";
7
+ //#region src/cli.ts
8
+ var l = "Usage:\n seed-rdf check [--schema <file>] [--sidecar <file>] [--base <iri>]\n List stale terms, terms that aren't IRIs, and unmapped models and properties.\n Exits 1 when there are stale or unreadable terms.\n\n seed-rdf sync [--schema <file>] [--sidecar <file>] [--base <iri>] [--write]\n Regenerate the sidecar's @context from its terms and the schema's current names, and set\n schemaVersion. Prints the result, or writes it to the sidecar with --write.\n\n seed-rdf suggest [--schema <file>] [--sidecar <file>] [--base <iri>] [--write]\n Draft schema.org and dcterms terms for unmapped names with a common meaning. Prints them,\n or adds them to the sidecar (creating it if needed) with --write. Review before committing.\n\nDefault --schema: seedSchema.json. Default --sidecar: the schema's path with .context.jsonld\nin place of .json.", u = class extends Error {
9
+ showUsage;
10
+ constructor(e, t = !1) {
11
+ super(e), this.showUsage = t;
12
+ }
13
+ };
14
+ async function d(e, t) {
15
+ let n;
16
+ try {
17
+ n = await a(e, "utf8");
18
+ } catch (n) {
19
+ throw n.code === "ENOENT" ? new u(`No ${t} at ${e}`) : n;
20
+ }
21
+ try {
22
+ return JSON.parse(n);
23
+ } catch (n) {
24
+ throw new u(`The ${t} at ${e} isn't JSON: ${n.message}`);
25
+ }
26
+ }
27
+ var f = (e) => `${JSON.stringify(e, null, 2)}\n`, p = (e) => e.property ? `${e.model}.${e.property}` : e.model;
28
+ async function m(i, a) {
29
+ try {
30
+ let { positionals: m, values: h } = c({
31
+ args: [...i],
32
+ allowPositionals: !0,
33
+ options: {
34
+ schema: { type: "string" },
35
+ sidecar: { type: "string" },
36
+ base: { type: "string" },
37
+ write: { type: "boolean" },
38
+ help: {
39
+ type: "boolean",
40
+ short: "h"
41
+ }
42
+ }
43
+ }), [g, ..._] = m;
44
+ if (h.help) return a.out(l), 0;
45
+ if (!g || _.length > 0 || ![
46
+ "check",
47
+ "sync",
48
+ "suggest"
49
+ ].includes(g)) throw new u(g ? `Unknown command: ${[g, ..._].join(" ")}` : "No command given", !0);
50
+ if (g === "check" && h.write) throw new u("check never writes; use sync --write");
51
+ let v = s.resolve(a.cwd, h.schema ?? "seedSchema.json"), y = h.sidecar ? s.resolve(a.cwd, h.sidecar) : v.replace(/(\.json)?$/, ".context.jsonld"), b = await d(v, "schema");
52
+ if (!b.id) throw new u(`The schema at ${v} has no id; the SDK assigns one when it writes the schema`);
53
+ let x = { base: h.base };
54
+ if (g === "suggest") {
55
+ let n = await d(y, "sidecar").catch((e) => {
56
+ if (!(e instanceof u && e.message.startsWith("No sidecar"))) throw e;
57
+ }) ?? {
58
+ schemaId: b.id,
59
+ terms: {}
60
+ }, i = t({
61
+ schema: b,
62
+ sidecar: n
63
+ });
64
+ if (!h.write) {
65
+ i.length === 0 && a.out("No suggestions.");
66
+ for (let e of i) a.out(`${p(e)} (${e.id}): ${e.term}`);
67
+ return 0;
68
+ }
69
+ let s = {
70
+ ...n.terms,
71
+ ...Object.fromEntries(i.map((e) => [e.id, e.term]))
72
+ }, c = {
73
+ ...r,
74
+ ...n["@context"]
75
+ };
76
+ return await o(y, f(e({
77
+ schema: b,
78
+ sidecar: {
79
+ ...n,
80
+ terms: s,
81
+ "@context": c
82
+ }
83
+ }, x))), a.out(`Added ${i.length} term${i.length === 1 ? "" : "s"} to ${y}. Review them before committing.`), 0;
84
+ }
85
+ let S = await d(y, "sidecar");
86
+ if (g === "sync") {
87
+ let t = e({
88
+ schema: b,
89
+ sidecar: S
90
+ }, x);
91
+ return h.write ? (await o(y, f(t)), a.out(`Wrote ${y}`), 0) : (a.out(f(t).trimEnd()), 0);
92
+ }
93
+ let C = n({
94
+ schema: b,
95
+ sidecar: S
96
+ }, x);
97
+ if (C.stale.length > 0) {
98
+ a.err("Stale terms (no model or property in the schema has this id):");
99
+ for (let { id: e, term: t } of C.stale) a.err(` ${e}: ${t}`);
100
+ }
101
+ if (C.invalid.length > 0) {
102
+ a.err("Terms that aren't an IRI or a name with a prefix in @context:");
103
+ for (let { id: e, term: t } of C.invalid) a.err(` ${e}: ${t}`);
104
+ }
105
+ if (C.unmapped.length > 0) {
106
+ a.out("Unmapped (exported under protocol IRIs):");
107
+ for (let e of C.unmapped) a.out(` ${p(e)}${e.id ? ` (${e.id})` : " (no id yet)"}`);
108
+ }
109
+ return (C.contextOutdated || C.versionOutdated) && a.out("The sidecar is behind the schema; run `seed-rdf sync --write` to update @context and schemaVersion."), +(C.stale.length > 0 || C.invalid.length > 0);
110
+ } catch (e) {
111
+ if (!(e instanceof u) && !(e instanceof TypeError)) throw e;
112
+ return a.err(`[Seed Protocol] ${e.message}`), e instanceof u && e.showUsage && a.err(l), 2;
113
+ }
114
+ }
115
+ //#endregion
116
+ //#region src/bin.ts
117
+ i.exitCode = await m(i.argv.slice(2), {
118
+ cwd: i.cwd(),
119
+ out: (e) => console.log(e),
120
+ err: (e) => console.error(e)
121
+ });
122
+ //#endregion
123
+
124
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","names":["syncSidecar","suggestTerms","SUGGESTED_PREFIXES","process","path","USAGE","UsageError","message","showUsage","readJson","text","readFile","file","error","what","json","value","target","t","runRdfCli","parseArgs","args","command","extra","positionals","values","io","schemaFile","sidecarFile","schema","options","sidecar","suggestions","s","terms","context","writeFile","synced","report","checkSidecar","id","term","line"],"sources":["../src/cli.ts","../src/bin.ts"],"sourcesContent":["import { readFile, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport { parseArgs } from 'node:util'\nimport type { RdfSidecar } from './lens.js'\nimport { SUGGESTED_PREFIXES, checkSidecar, suggestTerms, syncSidecar, type SidecarSchema } from './sidecar.js'\n\nconst USAGE = `Usage:\n seed-rdf check [--schema <file>] [--sidecar <file>] [--base <iri>]\n List stale terms, terms that aren't IRIs, and unmapped models and properties.\n Exits 1 when there are stale or unreadable terms.\n\n seed-rdf sync [--schema <file>] [--sidecar <file>] [--base <iri>] [--write]\n Regenerate the sidecar's @context from its terms and the schema's current names, and set\n schemaVersion. Prints the result, or writes it to the sidecar with --write.\n\n seed-rdf suggest [--schema <file>] [--sidecar <file>] [--base <iri>] [--write]\n Draft schema.org and dcterms terms for unmapped names with a common meaning. Prints them,\n or adds them to the sidecar (creating it if needed) with --write. Review before committing.\n\nDefault --schema: seedSchema.json. Default --sidecar: the schema's path with .context.jsonld\nin place of .json.`\n\nexport type CliIo = {\n readonly cwd: string\n readonly out: (line: string) => void\n readonly err: (line: string) => void\n}\n\nclass UsageError extends Error {\n constructor(\n message: string,\n readonly showUsage = false,\n ) {\n super(message)\n }\n}\n\nasync function readJson<T>(file: string, what: string): Promise<T> {\n let text: string\n try {\n text = await readFile(file, 'utf8')\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') throw new UsageError(`No ${what} at ${file}`)\n throw error\n }\n try {\n return JSON.parse(text) as T\n } catch (error) {\n throw new UsageError(`The ${what} at ${file} isn't JSON: ${(error as Error).message}`)\n }\n}\n\nconst json = (value: unknown) => `${JSON.stringify(value, null, 2)}\\n`\nconst target = (t: { model: string; property?: string }) => (t.property ? `${t.model}.${t.property}` : t.model)\n\n/**\n * Runs `seed-rdf` with `args` (without the program name) and returns its exit code: 0 when done,\n * 1 when `check` finds stale or unreadable terms, 2 on a usage or input error.\n */\nexport async function runRdfCli(args: readonly string[], io: CliIo): Promise<number> {\n try {\n const { positionals, values } = parseArgs({\n args: [...args],\n allowPositionals: true,\n options: {\n schema: { type: 'string' },\n sidecar: { type: 'string' },\n base: { type: 'string' },\n write: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n },\n })\n const [command, ...extra] = positionals\n if (values.help) {\n io.out(USAGE)\n return 0\n }\n if (!command || extra.length > 0 || !['check', 'sync', 'suggest'].includes(command)) {\n throw new UsageError(command ? `Unknown command: ${[command, ...extra].join(' ')}` : 'No command given', true)\n }\n if (command === 'check' && values.write) throw new UsageError('check never writes; use sync --write')\n\n const schemaFile = path.resolve(io.cwd, values.schema ?? 'seedSchema.json')\n const sidecarFile = values.sidecar\n ? path.resolve(io.cwd, values.sidecar)\n : schemaFile.replace(/(\\.json)?$/, '.context.jsonld')\n const schema = await readJson<SidecarSchema>(schemaFile, 'schema')\n if (!schema.id) throw new UsageError(`The schema at ${schemaFile} has no id; the SDK assigns one when it writes the schema`)\n const options = { base: values.base }\n\n if (command === 'suggest') {\n const existing = await readJson<RdfSidecar>(sidecarFile, 'sidecar').catch((error: unknown) => {\n if (error instanceof UsageError && error.message.startsWith('No sidecar')) return undefined\n throw error\n })\n const sidecar = existing ?? { schemaId: schema.id, terms: {} }\n const suggestions = suggestTerms({ schema, sidecar })\n if (!values.write) {\n if (suggestions.length === 0) io.out('No suggestions.')\n for (const s of suggestions) io.out(`${target(s)} (${s.id}): ${s.term}`)\n return 0\n }\n const terms = { ...sidecar.terms, ...Object.fromEntries(suggestions.map((s) => [s.id, s.term])) }\n const context = { ...SUGGESTED_PREFIXES, ...sidecar['@context'] }\n await writeFile(sidecarFile, json(syncSidecar({ schema, sidecar: { ...sidecar, terms, '@context': context } }, options)))\n io.out(`Added ${suggestions.length} term${suggestions.length === 1 ? '' : 's'} to ${sidecarFile}. Review them before committing.`)\n return 0\n }\n\n const sidecar = await readJson<RdfSidecar>(sidecarFile, 'sidecar')\n if (command === 'sync') {\n const synced = syncSidecar({ schema, sidecar }, options)\n if (!values.write) {\n io.out(json(synced).trimEnd())\n return 0\n }\n await writeFile(sidecarFile, json(synced))\n io.out(`Wrote ${sidecarFile}`)\n return 0\n }\n\n const report = checkSidecar({ schema, sidecar }, options)\n if (report.stale.length > 0) {\n io.err('Stale terms (no model or property in the schema has this id):')\n for (const { id, term } of report.stale) io.err(` ${id}: ${term}`)\n }\n if (report.invalid.length > 0) {\n io.err(`Terms that aren't an IRI or a name with a prefix in @context:`)\n for (const { id, term } of report.invalid) io.err(` ${id}: ${term}`)\n }\n if (report.unmapped.length > 0) {\n io.out('Unmapped (exported under protocol IRIs):')\n for (const t of report.unmapped) io.out(` ${target(t)}${t.id ? ` (${t.id})` : ' (no id yet)'}`)\n }\n if (report.contextOutdated || report.versionOutdated) {\n io.out('The sidecar is behind the schema; run `seed-rdf sync --write` to update @context and schemaVersion.')\n }\n return report.stale.length > 0 || report.invalid.length > 0 ? 1 : 0\n } catch (error) {\n if (!(error instanceof UsageError) && !(error instanceof TypeError)) throw error\n io.err(`[Seed Protocol] ${error.message}`)\n if (error instanceof UsageError && error.showUsage) io.err(USAGE)\n return 2\n }\n}\n","/** `seed-rdf check|sync|suggest`: keeps an app's RDF sidecar in step with its schema. */\nimport process from 'node:process'\nimport { runRdfCli } from './cli.js'\n\nprocess.exitCode = await runRdfCli(process.argv.slice(2), {\n cwd: process.cwd(),\n out: (line) => console.log(line),\n err: (line) => console.error(line),\n})\n"],"mappings":";;;;;;;AAMA,IAAMK,IAAQ,m3BAsBRC,IAAN,cAAyB,MAAM;CAGlB;CAFX,YACE,GACA,IAAqB,IACrB;EADS,AAET,MAAMC,CAAO,GAFJ,KAAA,YAAAC;CAGX;AACF;AAEA,eAAeC,EAAY,GAAc,GAA0B;CACjE,IAAIC;CACJ,IAAI;EACF,IAAO,MAAMC,EAASC,GAAM,MAAM;CACpC,SAASC,GAAO;EAEd,MADKA,EAAgC,SAAS,WAAgB,IAAIP,EAAW,MAAMQ,EAAK,MAAMF,GAAM,IAC9FC;CACR;CACA,IAAI;EACF,OAAO,KAAK,MAAMH,CAAI;CACxB,SAASG,GAAO;EACd,MAAM,IAAIP,EAAW,OAAOQ,EAAK,MAAMF,EAAK,eAAgBC,EAAgB,SAAS;CACvF;AACF;AAEA,IAAME,KAAQ,MAAmB,GAAG,KAAK,UAAUC,GAAO,MAAM,CAAC,EAAE,KAC7DC,KAAU,MAA6CC,EAAE,WAAW,GAAGA,EAAE,MAAM,GAAGA,EAAE,aAAaA,EAAE;AAMzG,eAAsBC,EAAU,GAAyB,GAA4B;CACnF,IAAI;EACF,IAAM,EAAE,gBAAa,cAAWC,EAAU;GACxC,MAAM,CAAC,GAAGC,CAAI;GACd,kBAAkB;GAClB,SAAS;IACP,QAAQ,EAAE,MAAM,SAAS;IACzB,SAAS,EAAE,MAAM,SAAS;IAC1B,MAAM,EAAE,MAAM,SAAS;IACvB,OAAO,EAAE,MAAM,UAAU;IACzB,MAAM;KAAE,MAAM;KAAW,OAAO;IAAI;GACtC;EACF,CAAC,GACK,CAACC,GAAS,GAAGC,KAASC;EAC5B,IAAIC,EAAO,MAET,OADAC,EAAG,IAAIrB,CAAK,GACL;EAET,IAAI,CAACiB,KAAWC,EAAM,SAAS,KAAK,CAAC;GAAC;GAAS;GAAQ;EAAS,CAAC,CAAC,SAASD,CAAO,GAChF,MAAM,IAAIhB,EAAWgB,IAAU,oBAAoB,CAACA,GAAS,GAAGC,CAAK,CAAC,CAAC,KAAK,GAAG,MAAM,oBAAoB,EAAI;EAE/G,IAAID,MAAY,WAAWG,EAAO,OAAO,MAAM,IAAInB,EAAW,sCAAsC;EAEpG,IAAMqB,IAAavB,EAAK,QAAQsB,EAAG,KAAKD,EAAO,UAAU,iBAAiB,GACpEG,IAAcH,EAAO,UACvBrB,EAAK,QAAQsB,EAAG,KAAKD,EAAO,OAAO,IACnCE,EAAW,QAAQ,cAAc,iBAAiB,GAChDE,IAAS,MAAMpB,EAAwBkB,GAAY,QAAQ;EACjE,IAAI,CAACE,EAAO,IAAI,MAAM,IAAIvB,EAAW,iBAAiBqB,EAAW,0DAA0D;EAC3H,IAAMG,IAAU,EAAE,MAAML,EAAO,KAAK;EAEpC,IAAIH,MAAY,WAAW;GAKzB,IAAMS,IAAU,MAJOtB,EAAqBmB,GAAa,SAAS,CAAC,CAAC,OAAO,MAAmB;IAC5F,IAAI,eAAiBtB,KAAcO,EAAM,QAAQ,WAAW,YAAY,IACxE,MAAMA;GACR,CAAC,KAC2B;IAAE,UAAUgB,EAAO;IAAI,OAAO,CAAC;GAAE,GACvDG,IAAc/B,EAAa;IAAE;IAAQ;GAAQ,CAAC;GACpD,IAAI,CAACwB,EAAO,OAAO;IACjB,AAAIO,EAAY,WAAW,KAAGN,EAAG,IAAI,iBAAiB;IACtD,KAAK,IAAMO,KAAKD,GAAa,EAAG,IAAI,GAAGf,EAAOgB,CAAC,EAAE,IAAIA,EAAE,GAAG,KAAKA,EAAE,MAAM;IACvE,OAAO;GACT;GACA,IAAMC,IAAQ;IAAE,GAAGH,EAAQ;IAAO,GAAG,OAAO,YAAYC,EAAY,KAAK,MAAM,CAACC,EAAE,IAAIA,EAAE,IAAI,CAAC,CAAC;GAAE,GAC1FE,IAAU;IAAE,GAAGjC;IAAoB,GAAG6B,EAAQ;GAAY;GAGhE,OAFA,MAAMK,EAAUR,GAAab,EAAKf,EAAY;IAAE;IAAQ,SAAS;KAAE,GAAG+B;KAAS;KAAO,YAAYI;IAAQ;GAAE,GAAGL,CAAO,CAAC,CAAC,GACxHJ,EAAG,IAAI,SAASM,EAAY,OAAO,OAAOA,EAAY,WAAW,IAAI,KAAK,IAAI,MAAMJ,EAAY,iCAAiC,GAC1H;EACT;EAEA,IAAMG,IAAU,MAAMtB,EAAqBmB,GAAa,SAAS;EACjE,IAAIN,MAAY,QAAQ;GACtB,IAAMe,IAASrC,EAAY;IAAE;IAAQ;GAAQ,GAAG8B,CAAO;GAOvD,OANKL,EAAO,SAIZ,MAAMW,EAAUR,GAAab,EAAKsB,CAAM,CAAC,GACzCX,EAAG,IAAI,SAASE,GAAa,GACtB,MALLF,EAAG,IAAIX,EAAKsB,CAAM,CAAC,CAAC,QAAQ,CAAC,GACtB;EAKX;EAEA,IAAMC,IAASC,EAAa;GAAE;GAAQ;EAAQ,GAAGT,CAAO;EACxD,IAAIQ,EAAO,MAAM,SAAS,GAAG;GAC3B,EAAG,IAAI,+DAA+D;GACtE,KAAK,IAAM,EAAE,OAAI,aAAUA,EAAO,OAAO,EAAG,IAAI,KAAKE,EAAG,IAAIC,GAAM;EACpE;EACA,IAAIH,EAAO,QAAQ,SAAS,GAAG;GAC7B,EAAG,IAAI,+DAA+D;GACtE,KAAK,IAAM,EAAE,OAAI,aAAUA,EAAO,SAAS,EAAG,IAAI,KAAKE,EAAG,IAAIC,GAAM;EACtE;EACA,IAAIH,EAAO,SAAS,SAAS,GAAG;GAC9B,EAAG,IAAI,0CAA0C;GACjD,KAAK,IAAMpB,KAAKoB,EAAO,UAAU,EAAG,IAAI,KAAKrB,EAAOC,CAAC,IAAIA,EAAE,KAAK,KAAKA,EAAE,GAAG,KAAK,gBAAgB;EACjG;EAIA,QAHIoB,EAAO,mBAAmBA,EAAO,oBACnCZ,EAAG,IAAI,qGAAqG,GAEvG,IAAO,MAAM,SAAS,KAAKY,EAAO,QAAQ,SAAS;CAC5D,SAASzB,GAAO;EACd,IAAI,EAAEA,aAAiBP,MAAe,EAAEO,aAAiB,YAAY,MAAMA;EAG3E,OAFAa,EAAG,IAAI,mBAAmBb,EAAM,SAAS,GACrCA,aAAiBP,KAAcO,EAAM,aAAWa,EAAG,IAAIrB,CAAK,GACzD;CACT;AACF;;;AC5IA,EAAQ,WAAW,MAAMc,EAAUhB,EAAQ,KAAK,MAAM,CAAC,GAAG;CACxD,KAAKA,EAAQ,IAAI;CACjB,MAAM,MAAS,QAAQ,IAAIuC,CAAI;CAC/B,MAAM,MAAS,QAAQ,MAAMA,CAAI;AACnC,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type CliIo = {
2
+ readonly cwd: string;
3
+ readonly out: (line: string) => void;
4
+ readonly err: (line: string) => void;
5
+ };
6
+ /**
7
+ * Runs `seed-rdf` with `args` (without the program name) and returns its exit code: 0 when done,
8
+ * 1 when `check` finds stale or unreadable terms, 2 on a usage or input error.
9
+ */
10
+ export declare function runRdfCli(args: readonly string[], io: CliIo): Promise<number>;
11
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC,CAAA;AA6BD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAqFnF"}
@@ -0,0 +1,65 @@
1
+ import type { SeedAttestations } from '@seedprotocol/query';
2
+ import { type IriScheme } from './iri.js';
3
+ import { type RdfLens } from './lens.js';
4
+ /** An IRI, a typed literal, or an RDF list of either. */
5
+ export type RdfObject = {
6
+ readonly kind: 'iri';
7
+ readonly iri: string;
8
+ } | {
9
+ readonly kind: 'literal';
10
+ readonly value: string;
11
+ readonly datatype: string;
12
+ } | {
13
+ readonly kind: 'list';
14
+ readonly items: readonly RdfObject[];
15
+ };
16
+ /** One subject and its statements, predicates in the order they were added. */
17
+ export type RdfNode = {
18
+ readonly id: string;
19
+ readonly properties: ReadonlyMap<string, readonly RdfObject[]>;
20
+ };
21
+ /**
22
+ * A state graph: each seed's current values at its head Version, in the default graph. Every
23
+ * subject is an IRI; the only blank nodes are the cells of RDF lists, which writers create.
24
+ */
25
+ export type StateGraph = {
26
+ readonly iris: IriScheme;
27
+ readonly nodes: readonly RdfNode[];
28
+ /** Prefixes from the lens, for the writers. */
29
+ readonly prefixes: ReadonlyMap<string, string>;
30
+ /** Lens terms that couldn't be used, and values that don't fit their data type. Empty without a lens. */
31
+ readonly warnings: readonly string[];
32
+ };
33
+ export type StateGraphOptions = {
34
+ /** The chain the seeds were attested on. EAS uids don't include it, so every IRI does. */
35
+ chainId: number;
36
+ /** Base of the minted IRIs. Default PROVISIONAL_BASE_IRI. */
37
+ base?: string;
38
+ /**
39
+ * An `https://` URL for an Arweave transaction, given as `schema:contentUrl` beside the
40
+ * `ar://` content IRI, since most RDF tools can't fetch `ar://`. None by default.
41
+ */
42
+ contentUrl?: (transactionId: string) => string;
43
+ /**
44
+ * The app's Schema and sidecar. Mapped classes and predicates replace the protocol IRIs in the
45
+ * graph, and a Number or Date gets its literal type. None by default.
46
+ */
47
+ lens?: RdfLens;
48
+ };
49
+ /**
50
+ * The state graph of `seeds` (from `getSeedAttestations` in @seedprotocol/query). Each seed is
51
+ * typed `seed:Seed` and its model class (`{base}c/{model}`), attributed to its attester as a
52
+ * `did:pkh`, and linked to its head Version (`seed:currentVersion`). Its live Versions up to the head
53
+ * are `seed:Version`s with `prov:generatedAtTime`, `prov:wasAttributedTo`, and `prov:wasRevisionOf`
54
+ * the Version before; revoked Versions are left out, as in an archive bag. Each property is one statement with the protocol
55
+ * predicate `{base}p/{easType}/{easName}`. Relations are Seed IRIs, lists are RDF lists, and an
56
+ * Image, File, Html or Json seed's `storage_transaction_id` also gives `seed:content <ar://…>`.
57
+ * Without a lens, values are typed from the EAS type alone: a Number (an EAS `string`, ADR 0010)
58
+ * is an `xsd:string`, and a Date (`uint256` seconds) an `xsd:integer`. With one, a Number is typed
59
+ * per value and a Date is an `xsd:dateTime`, and mapped terms replace the protocol IRIs, and the
60
+ * graph gets a `prov:Activity` `{base}export/{sha256}` whose `prov:used` is the sidecar's hash as
61
+ * an RFC 6920 `ni:///sha-256;…` IRI. That IRI is under the provisional base: keep it out of
62
+ * anything anchored, such as an archive bag.
63
+ */
64
+ export declare function stateGraph(seeds: readonly SeedAttestations[], options: StateGraphOptions): StateGraph;
65
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAmD,MAAM,qBAAqB,CAAA;AAC5G,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,EAAiD,KAAK,OAAO,EAAmB,MAAM,WAAW,CAAA;AAExG,yDAAyD;AACzD,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC/E;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAA;CAAE,CAAA;AAEnE,+EAA+E;AAC/E,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,CAAA;CAC/D,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAA;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9C,yGAAyG;IACzG,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9C;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AA8GD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAAE,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAgErG"}
@@ -0,0 +1,26 @@
1
+ import { type GetSeedAttestationsOptions } from '@seedprotocol/query';
2
+ import { type StateGraph, type StateGraphOptions } from './graph.js';
3
+ export { PROVISIONAL_BASE_IRI, iriScheme } from './iri.js';
4
+ export type { IriScheme } from './iri.js';
5
+ export { stateGraph } from './graph.js';
6
+ export type { RdfNode, RdfObject, StateGraph, StateGraphOptions } from './graph.js';
7
+ export type { LensSchema, RdfLens, RdfSidecar } from './lens.js';
8
+ export { toQuads, toNQuads } from './quads.js';
9
+ export { toTurtle } from './turtle.js';
10
+ export { toJsonLd } from './jsonld.js';
11
+ export type { JsonLdDocument } from './jsonld.js';
12
+ export { seedVocabulary } from './vocabulary.js';
13
+ export { toShacl } from './shacl.js';
14
+ export type { ShaclOptions } from './shacl.js';
15
+ export { SUGGESTED_PREFIXES, checkSidecar, sidecarContext, suggestTerms, syncSidecar } from './sidecar.js';
16
+ export type { SchemaTarget, SidecarContext, SidecarContextOptions, SidecarReport, SidecarSchema, TermSuggestion, } from './sidecar.js';
17
+ export type ExportSeedsOptions = StateGraphOptions & Pick<GetSeedAttestationsOptions, 'source' | 'pins' | 'asOf'>;
18
+ /**
19
+ * The state graph of the seeds, read with `getSeedAttestations` from @seedprotocol/query (same
20
+ * sources and pins as `getSeeds`). Seeds that aren't found are left out. With `asOf` (unix
21
+ * seconds), the graph is what EAS held then, the same attestations a reproducible archive bag with
22
+ * that `Seed-Bag-As-Of` holds; it reads EAS and refuses `source: 'local'`. Two exports with the
23
+ * same seeds, `asOf`, options and sidecar give the same bytes from every writer.
24
+ */
25
+ export declare function exportSeeds(seedUids: readonly string[], options: ExportSeedsOptions): Promise<StateGraph>;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,0BAA0B,EAAE,MAAM,qBAAqB,CAAA;AAC1F,OAAO,EAAc,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEhF,OAAO,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAC1D,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnF,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1G,YAAY,EACV,YAAY,EACZ,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,cAAc,CAAA;AAErB,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG,IAAI,CAAC,0BAA0B,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC,CAAA;AAEjH;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAIrB"}