@mailwoman/activity-lexicon 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @mailwoman/activity-lexicon
2
+
3
+ The reviewed activity-phrase lexicon for the [mailwoman](https://www.npmjs.com/package/mailwoman) geocoder: the surface forms a person types for an activity (`pick up a prescription` → `obtain_medication`), each entry carrying the committed record that attests it and the locales it applies to.
4
+
5
+ Recognition only. This package states nothing about the world — which establishments afford an activity, in which country, on whose authority — that knowledge lives in `@mailwoman/geographic-model`, and a phrase here is valid only when it names an activity concept that model carries. Zero runtime dependencies.
6
+
7
+ ## Provenance discipline
8
+
9
+ Every entry belongs to one of four attestation classes — a committed query, a derived form of one, a clause quoted from the compiled concept's description, or a regional register copied from a committed synonym — and the test suite re-checks each attestation against its source on every run. [`data/PROVENANCE.md`](./data/PROVENANCE.md) records the rules. An entry without a checkable attestation does not load.
10
+
11
+ ## Release posture
12
+
13
+ **Do not depend on this package yet.** Its npm name has not been published; it joins the coordinated mailwoman release after the one-time first publish establishes the name (`scripts/bless-package.ts` in the repository — an operator step). Until then it is a sanctioned release absence, recorded in `scripts/release-stage.ts`. When blessed, the change is the same three edits `@mailwoman/geographic-model` made: the `.release-it.json` entry, the sanctioned-absence removal, and the arithmetic in `AGENTS.md` and `scripts/release-stage.test.ts`.
14
+
15
+ ## License
16
+
17
+ AGPL-3.0-only OR LicenseRef-Commercial. See the repository root for details.
@@ -0,0 +1,121 @@
1
+ {
2
+ "lexiconID": "mailwoman-activity-phrases",
3
+ "version": "1.0.0",
4
+ "provenance": {
5
+ "source": "mailwoman-curated",
6
+ "authoredAt": "2026-08-27",
7
+ "sourceRecord": "packages/activity-lexicon/data/PROVENANCE.md",
8
+ "sourceURL": "https://github.com/sister-software/mailwoman/issues/1962",
9
+ "notes": "Reviewed surface forms, curated the way @mailwoman/poi-taxonomy's synonyms are curated: an entry is admitted only when something already committed attests it, and the attestation names that record and quotes the text it rests on. Four classes are admissible and each is checkable — a committed query row, a regular transformation of another entry, a register split a committed vocabulary already records, and a clause of the activity concept's own description. No entry rests on observed traffic; this vocabulary carries no measurement and nothing downstream may read it as one. Every phrase names an activity concept some other artifact owns, and this file states nothing about the world: which entity kinds afford the activity, in which country, with what modality and on whose authority all come from that artifact."
10
+ },
11
+ "phrases": [
12
+ {
13
+ "phrase": "prescription",
14
+ "activity": "obtain_medication",
15
+ "source": "curated",
16
+ "attestation": {
17
+ "kind": "committed-query",
18
+ "reference": "packages/mailwoman/eval-harness/fixtures/poi-board.jsonl#sem-act-us-02",
19
+ "detail": "prescription near Denver CO"
20
+ },
21
+ "note": "The bare activity noun. Unscoped because the row that attests it carries no locale, and scoping a committed form past its own attestation would make the entry say more than the record does."
22
+ },
23
+ {
24
+ "phrase": "prescriptions",
25
+ "activity": "obtain_medication",
26
+ "source": "curated",
27
+ "attestation": { "kind": "derived-form", "base": "prescription", "derivation": "plural" },
28
+ "note": "The plural of the bare noun. A person asking for the thing in general writes it as often as the singular."
29
+ },
30
+ {
31
+ "phrase": "pick up a prescription",
32
+ "activity": "obtain_medication",
33
+ "source": "curated",
34
+ "attestation": {
35
+ "kind": "committed-query",
36
+ "reference": "packages/mailwoman/eval-harness/fixtures/poi-board.jsonl#sem-act-us-01",
37
+ "detail": "where can i pick up a prescription near Denver CO"
38
+ },
39
+ "note": "The verb phrase for collecting a dispensed prescription."
40
+ },
41
+ {
42
+ "phrase": "pick up my medication",
43
+ "activity": "obtain_medication",
44
+ "source": "curated",
45
+ "attestation": {
46
+ "kind": "concept-description",
47
+ "reference": "obtain_medication",
48
+ "detail": "obtaining medication"
49
+ },
50
+ "note": "The same request naming the medication rather than the paperwork. The head noun is the concept's own; the verb phrase is the one `pick up a prescription` already carries."
51
+ },
52
+ {
53
+ "phrase": "fill a prescription",
54
+ "activity": "obtain_medication",
55
+ "source": "curated",
56
+ "attestation": {
57
+ "kind": "committed-query",
58
+ "reference": "packages/mailwoman/eval-harness/fixtures/poi-board.jsonl#sem-act-fr-01",
59
+ "detail": "somewhere to fill a prescription near Toulouse"
60
+ },
61
+ "note": "The verb phrase for having a prescription dispensed."
62
+ },
63
+ {
64
+ "phrase": "prescription refilled",
65
+ "activity": "obtain_medication",
66
+ "source": "curated",
67
+ "attestation": {
68
+ "kind": "committed-query",
69
+ "reference": "packages/mailwoman/eval-harness/fixtures/poi-board.jsonl#sem-act-mx-01",
70
+ "detail": "i need my prescription refilled near Tijuana"
71
+ },
72
+ "note": "The inflected form, as it appears inside a first-person sentence."
73
+ },
74
+ {
75
+ "phrase": "refill a prescription",
76
+ "activity": "obtain_medication",
77
+ "source": "curated",
78
+ "attestation": { "kind": "derived-form", "base": "prescription refilled", "derivation": "verb-phrase" },
79
+ "note": "The uninflected phrasing of the same request."
80
+ },
81
+ {
82
+ "phrase": "prescription refill",
83
+ "activity": "obtain_medication",
84
+ "source": "curated",
85
+ "attestation": { "kind": "derived-form", "base": "prescription refilled", "derivation": "nominalization" },
86
+ "note": "The noun phrase for the same request, which is how a person writes it when the query has no verb."
87
+ },
88
+ {
89
+ "phrase": "collect a prescription",
90
+ "activity": "obtain_medication",
91
+ "locales": ["en-GB", "en-AU", "en-NZ"],
92
+ "source": "curated",
93
+ "attestation": {
94
+ "kind": "regional-register",
95
+ "reference": "packages/poi-taxonomy/data/curated-overlay.json#chemist",
96
+ "base": "pick up a prescription",
97
+ "detail": "chemist → pharmacy, locales en-GB, en-AU, en-NZ"
98
+ },
99
+ "note": "The British-register verb for collecting a dispensed prescription. Its locales are copied from the committed `chemist` synonym rather than decided here, so the two records cannot drift into disagreeing about where the register applies."
100
+ },
101
+ {
102
+ "phrase": "collect my prescription",
103
+ "activity": "obtain_medication",
104
+ "locales": ["en-GB", "en-AU", "en-NZ"],
105
+ "source": "curated",
106
+ "attestation": { "kind": "derived-form", "base": "collect a prescription", "derivation": "possessive" },
107
+ "note": "The first-person phrasing of the British-register form, carrying its base's locale scope."
108
+ },
109
+ {
110
+ "phrase": "buy medicine",
111
+ "activity": "obtain_medication",
112
+ "source": "curated",
113
+ "attestation": {
114
+ "kind": "concept-description",
115
+ "reference": "obtain_medication",
116
+ "detail": "bought over the counter"
117
+ },
118
+ "note": "Obtaining medication without a prescription, which the concept's own description admits as part of the activity."
119
+ }
120
+ ]
121
+ }
package/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/activity-lexicon` — the reviewed surface forms for activity concepts.
7
+ *
8
+ * The vocabulary answers one question: which strings does a person type when they mean a given activity, and where
9
+ * is each of those strings used. It answers no question about the world. Which entity kinds afford the activity, in
10
+ * which country and on whose authority live in `@mailwoman/geographic-model`; which venue nouns name a POI category
11
+ * live in `@mailwoman/poi-taxonomy`. A consumer joins them; none of the three restates another.
12
+ */
13
+
14
+ export {
15
+ ACTIVITY_LEXICON_PATH,
16
+ auditActivityLexicon,
17
+ normalizeActivityPhrase,
18
+ readActivityLexicon,
19
+ resolveActivityPhraseLocale,
20
+ } from "./lexicon.ts"
21
+
22
+ export type * from "./types.ts"
package/lexicon.ts ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Reader and audit for the reviewed activity-phrase lexicon.
7
+ *
8
+ * WHY THIS IS ITS OWN VOCABULARY. `@mailwoman/poi-taxonomy`'s phrases are venue nouns and each names ONE category;
9
+ * an activity is afforded by a SET of entity kinds, and the set is country-conditional. A phrase naming an activity
10
+ * therefore cannot be a synonym in that table without saying something the table has no field for. It is equally not
11
+ * part of the compiled world model: that artifact carries concepts, relations, mappings and provenance, and a phrase
12
+ * is none of those — it is how a person says the thing, which is recognition rather than knowledge.
13
+ *
14
+ * THE AUDIT REFUSES RATHER THAN DEGRADES. Every problem {@linkcode auditActivityLexicon} reports is a record that
15
+ * would answer nothing while reading as though it answered: a phrase declared twice, a phrase scoped to no locale, a
16
+ * derived form whose base is absent or is itself derived so the chain never reaches a committed record. Each of those
17
+ * produces a lexicon that looks complete and is short, and a consumer measuring recognition breadth would read the
18
+ * shortfall as the world rather than as the file. So {@linkcode readActivityLexicon} throws.
19
+ *
20
+ * ZERO DEPENDENCIES, deliberately: a vocabulary any package may read must not drag a graph behind it.
21
+ */
22
+
23
+ import { existsSync, readFileSync } from "node:fs"
24
+ import { resolve } from "node:path"
25
+
26
+ import type {
27
+ ActivityPhraseEntry,
28
+ ActivityPhraseLexicon,
29
+ ActivityPhraseLocaleMatch,
30
+ ActivityPhraseDerivation,
31
+ } from "./types.ts"
32
+
33
+ const moduleDir = import.meta.dirname
34
+
35
+ /**
36
+ * Every derivation the closed list admits, for the audit.
37
+ */
38
+ const DERIVATIONS: ReadonlyArray<ActivityPhraseDerivation> = ["plural", "nominalization", "verb-phrase", "possessive"]
39
+
40
+ /**
41
+ * The committed lexicon.
42
+ *
43
+ * `data/` sits at the package root (it is a `files` entry), and this module sits either at that root — running from
44
+ * source — or under `out/` when compiled, so there are exactly two places to look. Probing for the FILE rather than
45
+ * attempting a parse keeps a corrupt lexicon from reading as an absent one.
46
+ */
47
+ export const ACTIVITY_LEXICON_PATH: string = (() => {
48
+ const candidates = [
49
+ resolve(moduleDir, "data", "activity-lexicon.json"),
50
+ resolve(moduleDir, "..", "data", "activity-lexicon.json"),
51
+ ]
52
+
53
+ const found = candidates.find((candidate) => existsSync(candidate))
54
+
55
+ if (!found) {
56
+ throw new Error(`activity-lexicon: could not find data/activity-lexicon.json — looked in ${candidates.join(", ")}`)
57
+ }
58
+
59
+ return found
60
+ })()
61
+
62
+ /**
63
+ * Normalize a phrase for comparison: NFKC, trimmed, whitespace collapsed, lowercased.
64
+ *
65
+ * `toLowerCase` rather than `toLocaleLowerCase`, deliberately: the locale-sensitive form folds a dotted capital `I` to
66
+ * `i̇` under a Turkish host locale, which would make the same query answer differently on two machines. Locale SCOPING
67
+ * is a property of the entry and is decided by {@linkcode resolveActivityPhraseLocale}; it never reaches the text.
68
+ */
69
+ export function normalizeActivityPhrase(phrase: string): string {
70
+ return phrase.normalize("NFKC").trim().replaceAll(/\s+/g, " ").toLowerCase()
71
+ }
72
+
73
+ /**
74
+ * Decide whether an entry answers under a locale, following the `@mailwoman/variant-aliases` semantics.
75
+ *
76
+ * A scoped entry does not match when the locale is unknown. That is the containment: a phrasing declared regional
77
+ * cannot be reached without knowing the region, or the record means something different from what it says.
78
+ */
79
+ export function resolveActivityPhraseLocale(
80
+ entry: ActivityPhraseEntry,
81
+ locale: string | undefined
82
+ ): ActivityPhraseLocaleMatch | null {
83
+ if (!entry.locales) return { scope: "unscoped", confidence: 1 }
84
+
85
+ if (!locale) return null
86
+
87
+ if (entry.locales.includes(locale)) return { scope: "exact", confidence: 1 }
88
+
89
+ const language = locale.split(/[-_]/)[0]
90
+
91
+ if (entry.locales.some((tag) => tag.split(/[-_]/)[0] === language)) return { scope: "language", confidence: 0.5 }
92
+
93
+ return null
94
+ }
95
+
96
+ /**
97
+ * Everything wrong with a lexicon that can be established without leaving this package, one message per problem.
98
+ *
99
+ * The checks an entry's attestation invites but this package cannot make — that a committed query row exists and ends
100
+ * in the phrase, that a referenced synonym carries the locales the entry copied, that a cited description clause is
101
+ * really in the compiled concept — belong to a consumer that holds those artifacts, and are made there.
102
+ */
103
+ export function auditActivityLexicon(lexicon: ActivityPhraseLexicon): string[] {
104
+ const problems: string[] = []
105
+
106
+ if (!lexicon.phrases.length) {
107
+ problems.push("the lexicon is empty — a vocabulary with no surface form can never fire")
108
+ }
109
+
110
+ const byPhrase = new Map<string, ActivityPhraseEntry>()
111
+
112
+ for (const entry of lexicon.phrases) {
113
+ const normalized = normalizeActivityPhrase(entry.phrase)
114
+ const named = JSON.stringify(entry.phrase)
115
+
116
+ if (!normalized) {
117
+ problems.push(`phrase ${named} normalizes to nothing`)
118
+
119
+ continue
120
+ }
121
+
122
+ if (byPhrase.has(normalized)) {
123
+ problems.push(`phrase ${named} is declared twice`)
124
+
125
+ continue
126
+ }
127
+
128
+ byPhrase.set(normalized, entry)
129
+
130
+ if (entry.source !== "curated") {
131
+ problems.push(
132
+ `phrase ${named} declares source ${JSON.stringify(entry.source)} — the only reviewed source is \`curated\``
133
+ )
134
+ }
135
+
136
+ if (!entry.note.trim()) {
137
+ problems.push(`phrase ${named} carries no note — an entry nobody can review is an entry nobody can remove`)
138
+ }
139
+
140
+ if (entry.locales && !entry.locales.length) {
141
+ problems.push(`phrase ${named} declares an empty locale list — scoped to nowhere, it can never fire`)
142
+ }
143
+ }
144
+
145
+ for (const entry of lexicon.phrases) {
146
+ problems.push(...auditAttestation(entry, byPhrase))
147
+ }
148
+
149
+ return problems
150
+ }
151
+
152
+ /**
153
+ * The attestation half of the audit: whatever an entry's attestation points at inside this lexicon has to be there.
154
+ */
155
+ function auditAttestation(entry: ActivityPhraseEntry, byPhrase: ReadonlyMap<string, ActivityPhraseEntry>): string[] {
156
+ const named = JSON.stringify(entry.phrase)
157
+ const { attestation } = entry
158
+
159
+ switch (attestation.kind) {
160
+ case "committed-query": {
161
+ const query = normalizeActivityPhrase(attestation.detail)
162
+ const phrase = normalizeActivityPhrase(entry.phrase)
163
+
164
+ if (query !== phrase && !query.includes(` ${phrase} `) && !query.startsWith(`${phrase} `)) {
165
+ return [
166
+ `phrase ${named} cites committed query ${JSON.stringify(attestation.detail)}, which does not contain it as a subject`,
167
+ ]
168
+ }
169
+
170
+ return []
171
+ }
172
+
173
+ case "concept-description": {
174
+ if (attestation.reference !== entry.activity) {
175
+ return [
176
+ `phrase ${named} cites the description of ${JSON.stringify(attestation.reference)} while naming activity ${JSON.stringify(entry.activity)}`,
177
+ ]
178
+ }
179
+
180
+ return attestation.detail.trim() ? [] : [`phrase ${named} cites a description clause but quotes none of it`]
181
+ }
182
+
183
+ case "derived-form":
184
+ case "regional-register": {
185
+ const base = byPhrase.get(normalizeActivityPhrase(attestation.base))
186
+
187
+ if (!base) {
188
+ return [
189
+ `phrase ${named} is attested against base ${JSON.stringify(attestation.base)}, which the lexicon does not declare`,
190
+ ]
191
+ }
192
+
193
+ const problems: string[] = []
194
+
195
+ if (base.attestation.kind === "derived-form") {
196
+ problems.push(
197
+ `phrase ${named} is derived from ${JSON.stringify(attestation.base)}, which is itself derived — an attestation chain that never reaches a committed record attests nothing`
198
+ )
199
+ }
200
+
201
+ if (base.activity !== entry.activity) {
202
+ problems.push(
203
+ `phrase ${named} names activity ${JSON.stringify(entry.activity)} while its base names ${JSON.stringify(base.activity)}`
204
+ )
205
+ }
206
+
207
+ if (attestation.kind === "derived-form") {
208
+ if (!DERIVATIONS.includes(attestation.derivation)) {
209
+ problems.push(
210
+ `phrase ${named} declares derivation ${JSON.stringify(attestation.derivation)}, which is not a known one`
211
+ )
212
+ }
213
+
214
+ if (JSON.stringify(entry.locales ?? null) !== JSON.stringify(base.locales ?? null)) {
215
+ problems.push(
216
+ `phrase ${named} is a derived form whose locale scope differs from its base — a regular transformation does not change where a phrasing is used`
217
+ )
218
+ }
219
+ }
220
+
221
+ return problems
222
+ }
223
+ }
224
+ }
225
+
226
+ let committed: ActivityPhraseLexicon | undefined
227
+
228
+ /**
229
+ * Read the lexicon, refusing one the audit rejects.
230
+ *
231
+ * The committed read is memoized; an explicit path is read fresh, which is what a test asserting a refusal needs.
232
+ */
233
+ export function readActivityLexicon(path: string = ACTIVITY_LEXICON_PATH): ActivityPhraseLexicon {
234
+ if (path === ACTIVITY_LEXICON_PATH && committed) return committed
235
+
236
+ // A corrupt shipped vocabulary is a broken build, and the SyntaxError names the offset. Zero dependencies here, so
237
+ // `@mailwoman/core`'s parse wrappers are deliberately out of reach.
238
+ // oxlint-disable-next-line no-restricted-properties -- zero-dependency leaf; corrupt shipped data must throw with its offset
239
+ const lexicon = JSON.parse(readFileSync(path, "utf8")) as ActivityPhraseLexicon
240
+ const problems = auditActivityLexicon(lexicon)
241
+
242
+ if (problems.length) {
243
+ throw new Error(
244
+ [`activity-lexicon: ${path} does not audit:`].concat(problems.map((problem) => ` - ${problem}`)).join("\n")
245
+ )
246
+ }
247
+
248
+ if (path === ACTIVITY_LEXICON_PATH) {
249
+ committed = lexicon
250
+ }
251
+
252
+ return lexicon
253
+ }
package/out/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/activity-lexicon` — the reviewed surface forms for activity concepts.
7
+ *
8
+ * The vocabulary answers one question: which strings does a person type when they mean a given activity, and where
9
+ * is each of those strings used. It answers no question about the world. Which entity kinds afford the activity, in
10
+ * which country and on whose authority live in `@mailwoman/geographic-model`; which venue nouns name a POI category
11
+ * live in `@mailwoman/poi-taxonomy`. A consumer joins them; none of the three restates another.
12
+ */
13
+ export { ACTIVITY_LEXICON_PATH, auditActivityLexicon, normalizeActivityPhrase, readActivityLexicon, resolveActivityPhraseLocale, } from "./lexicon.ts";
14
+ export type * from "./types.ts";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACN,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,2BAA2B,GAC3B,MAAM,cAAc,CAAA;AAErB,mBAAmB,YAAY,CAAA"}
package/out/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/activity-lexicon` — the reviewed surface forms for activity concepts.
7
+ *
8
+ * The vocabulary answers one question: which strings does a person type when they mean a given activity, and where
9
+ * is each of those strings used. It answers no question about the world. Which entity kinds afford the activity, in
10
+ * which country and on whose authority live in `@mailwoman/geographic-model`; which venue nouns name a POI category
11
+ * live in `@mailwoman/poi-taxonomy`. A consumer joins them; none of the three restates another.
12
+ */
13
+ export { ACTIVITY_LEXICON_PATH, auditActivityLexicon, normalizeActivityPhrase, readActivityLexicon, resolveActivityPhraseLocale, } from "./lexicon.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACN,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,2BAA2B,GAC3B,MAAM,cAAc,CAAA"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Reader and audit for the reviewed activity-phrase lexicon.
7
+ *
8
+ * WHY THIS IS ITS OWN VOCABULARY. `@mailwoman/poi-taxonomy`'s phrases are venue nouns and each names ONE category;
9
+ * an activity is afforded by a SET of entity kinds, and the set is country-conditional. A phrase naming an activity
10
+ * therefore cannot be a synonym in that table without saying something the table has no field for. It is equally not
11
+ * part of the compiled world model: that artifact carries concepts, relations, mappings and provenance, and a phrase
12
+ * is none of those — it is how a person says the thing, which is recognition rather than knowledge.
13
+ *
14
+ * THE AUDIT REFUSES RATHER THAN DEGRADES. Every problem {@linkcode auditActivityLexicon} reports is a record that
15
+ * would answer nothing while reading as though it answered: a phrase declared twice, a phrase scoped to no locale, a
16
+ * derived form whose base is absent or is itself derived so the chain never reaches a committed record. Each of those
17
+ * produces a lexicon that looks complete and is short, and a consumer measuring recognition breadth would read the
18
+ * shortfall as the world rather than as the file. So {@linkcode readActivityLexicon} throws.
19
+ *
20
+ * ZERO DEPENDENCIES, deliberately: a vocabulary any package may read must not drag a graph behind it.
21
+ */
22
+ import type { ActivityPhraseEntry, ActivityPhraseLexicon, ActivityPhraseLocaleMatch } from "./types.ts";
23
+ /**
24
+ * The committed lexicon.
25
+ *
26
+ * `data/` sits at the package root (it is a `files` entry), and this module sits either at that root — running from
27
+ * source — or under `out/` when compiled, so there are exactly two places to look. Probing for the FILE rather than
28
+ * attempting a parse keeps a corrupt lexicon from reading as an absent one.
29
+ */
30
+ export declare const ACTIVITY_LEXICON_PATH: string;
31
+ /**
32
+ * Normalize a phrase for comparison: NFKC, trimmed, whitespace collapsed, lowercased.
33
+ *
34
+ * `toLowerCase` rather than `toLocaleLowerCase`, deliberately: the locale-sensitive form folds a dotted capital `I` to
35
+ * `i̇` under a Turkish host locale, which would make the same query answer differently on two machines. Locale SCOPING
36
+ * is a property of the entry and is decided by {@linkcode resolveActivityPhraseLocale}; it never reaches the text.
37
+ */
38
+ export declare function normalizeActivityPhrase(phrase: string): string;
39
+ /**
40
+ * Decide whether an entry answers under a locale, following the `@mailwoman/variant-aliases` semantics.
41
+ *
42
+ * A scoped entry does not match when the locale is unknown. That is the containment: a phrasing declared regional
43
+ * cannot be reached without knowing the region, or the record means something different from what it says.
44
+ */
45
+ export declare function resolveActivityPhraseLocale(entry: ActivityPhraseEntry, locale: string | undefined): ActivityPhraseLocaleMatch | null;
46
+ /**
47
+ * Everything wrong with a lexicon that can be established without leaving this package, one message per problem.
48
+ *
49
+ * The checks an entry's attestation invites but this package cannot make — that a committed query row exists and ends
50
+ * in the phrase, that a referenced synonym carries the locales the entry copied, that a cited description clause is
51
+ * really in the compiled concept — belong to a consumer that holds those artifacts, and are made there.
52
+ */
53
+ export declare function auditActivityLexicon(lexicon: ActivityPhraseLexicon): string[];
54
+ /**
55
+ * Read the lexicon, refusing one the audit rejects.
56
+ *
57
+ * The committed read is memoized; an explicit path is read fresh, which is what a test asserting a refusal needs.
58
+ */
59
+ export declare function readActivityLexicon(path?: string): ActivityPhraseLexicon;
60
+ //# sourceMappingURL=lexicon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lexicon.d.ts","sourceRoot":"","sources":["../lexicon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EACX,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EAEzB,MAAM,YAAY,CAAA;AASnB;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAahC,CAAA;AAEJ;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,mBAAmB,EAC1B,MAAM,EAAE,MAAM,GAAG,SAAS,GACxB,yBAAyB,GAAG,IAAI,CAYlC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,EAAE,CA+C7E;AA8ED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,MAA8B,GAAG,qBAAqB,CAoB/F"}
package/out/lexicon.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Reader and audit for the reviewed activity-phrase lexicon.
7
+ *
8
+ * WHY THIS IS ITS OWN VOCABULARY. `@mailwoman/poi-taxonomy`'s phrases are venue nouns and each names ONE category;
9
+ * an activity is afforded by a SET of entity kinds, and the set is country-conditional. A phrase naming an activity
10
+ * therefore cannot be a synonym in that table without saying something the table has no field for. It is equally not
11
+ * part of the compiled world model: that artifact carries concepts, relations, mappings and provenance, and a phrase
12
+ * is none of those — it is how a person says the thing, which is recognition rather than knowledge.
13
+ *
14
+ * THE AUDIT REFUSES RATHER THAN DEGRADES. Every problem {@linkcode auditActivityLexicon} reports is a record that
15
+ * would answer nothing while reading as though it answered: a phrase declared twice, a phrase scoped to no locale, a
16
+ * derived form whose base is absent or is itself derived so the chain never reaches a committed record. Each of those
17
+ * produces a lexicon that looks complete and is short, and a consumer measuring recognition breadth would read the
18
+ * shortfall as the world rather than as the file. So {@linkcode readActivityLexicon} throws.
19
+ *
20
+ * ZERO DEPENDENCIES, deliberately: a vocabulary any package may read must not drag a graph behind it.
21
+ */
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { resolve } from "node:path";
24
+ const moduleDir = import.meta.dirname;
25
+ /**
26
+ * Every derivation the closed list admits, for the audit.
27
+ */
28
+ const DERIVATIONS = ["plural", "nominalization", "verb-phrase", "possessive"];
29
+ /**
30
+ * The committed lexicon.
31
+ *
32
+ * `data/` sits at the package root (it is a `files` entry), and this module sits either at that root — running from
33
+ * source — or under `out/` when compiled, so there are exactly two places to look. Probing for the FILE rather than
34
+ * attempting a parse keeps a corrupt lexicon from reading as an absent one.
35
+ */
36
+ export const ACTIVITY_LEXICON_PATH = (() => {
37
+ const candidates = [
38
+ resolve(moduleDir, "data", "activity-lexicon.json"),
39
+ resolve(moduleDir, "..", "data", "activity-lexicon.json"),
40
+ ];
41
+ const found = candidates.find((candidate) => existsSync(candidate));
42
+ if (!found) {
43
+ throw new Error(`activity-lexicon: could not find data/activity-lexicon.json — looked in ${candidates.join(", ")}`);
44
+ }
45
+ return found;
46
+ })();
47
+ /**
48
+ * Normalize a phrase for comparison: NFKC, trimmed, whitespace collapsed, lowercased.
49
+ *
50
+ * `toLowerCase` rather than `toLocaleLowerCase`, deliberately: the locale-sensitive form folds a dotted capital `I` to
51
+ * `i̇` under a Turkish host locale, which would make the same query answer differently on two machines. Locale SCOPING
52
+ * is a property of the entry and is decided by {@linkcode resolveActivityPhraseLocale}; it never reaches the text.
53
+ */
54
+ export function normalizeActivityPhrase(phrase) {
55
+ return phrase.normalize("NFKC").trim().replaceAll(/\s+/g, " ").toLowerCase();
56
+ }
57
+ /**
58
+ * Decide whether an entry answers under a locale, following the `@mailwoman/variant-aliases` semantics.
59
+ *
60
+ * A scoped entry does not match when the locale is unknown. That is the containment: a phrasing declared regional
61
+ * cannot be reached without knowing the region, or the record means something different from what it says.
62
+ */
63
+ export function resolveActivityPhraseLocale(entry, locale) {
64
+ if (!entry.locales)
65
+ return { scope: "unscoped", confidence: 1 };
66
+ if (!locale)
67
+ return null;
68
+ if (entry.locales.includes(locale))
69
+ return { scope: "exact", confidence: 1 };
70
+ const language = locale.split(/[-_]/)[0];
71
+ if (entry.locales.some((tag) => tag.split(/[-_]/)[0] === language))
72
+ return { scope: "language", confidence: 0.5 };
73
+ return null;
74
+ }
75
+ /**
76
+ * Everything wrong with a lexicon that can be established without leaving this package, one message per problem.
77
+ *
78
+ * The checks an entry's attestation invites but this package cannot make — that a committed query row exists and ends
79
+ * in the phrase, that a referenced synonym carries the locales the entry copied, that a cited description clause is
80
+ * really in the compiled concept — belong to a consumer that holds those artifacts, and are made there.
81
+ */
82
+ export function auditActivityLexicon(lexicon) {
83
+ const problems = [];
84
+ if (!lexicon.phrases.length) {
85
+ problems.push("the lexicon is empty — a vocabulary with no surface form can never fire");
86
+ }
87
+ const byPhrase = new Map();
88
+ for (const entry of lexicon.phrases) {
89
+ const normalized = normalizeActivityPhrase(entry.phrase);
90
+ const named = JSON.stringify(entry.phrase);
91
+ if (!normalized) {
92
+ problems.push(`phrase ${named} normalizes to nothing`);
93
+ continue;
94
+ }
95
+ if (byPhrase.has(normalized)) {
96
+ problems.push(`phrase ${named} is declared twice`);
97
+ continue;
98
+ }
99
+ byPhrase.set(normalized, entry);
100
+ if (entry.source !== "curated") {
101
+ problems.push(`phrase ${named} declares source ${JSON.stringify(entry.source)} — the only reviewed source is \`curated\``);
102
+ }
103
+ if (!entry.note.trim()) {
104
+ problems.push(`phrase ${named} carries no note — an entry nobody can review is an entry nobody can remove`);
105
+ }
106
+ if (entry.locales && !entry.locales.length) {
107
+ problems.push(`phrase ${named} declares an empty locale list — scoped to nowhere, it can never fire`);
108
+ }
109
+ }
110
+ for (const entry of lexicon.phrases) {
111
+ problems.push(...auditAttestation(entry, byPhrase));
112
+ }
113
+ return problems;
114
+ }
115
+ /**
116
+ * The attestation half of the audit: whatever an entry's attestation points at inside this lexicon has to be there.
117
+ */
118
+ function auditAttestation(entry, byPhrase) {
119
+ const named = JSON.stringify(entry.phrase);
120
+ const { attestation } = entry;
121
+ switch (attestation.kind) {
122
+ case "committed-query": {
123
+ const query = normalizeActivityPhrase(attestation.detail);
124
+ const phrase = normalizeActivityPhrase(entry.phrase);
125
+ if (query !== phrase && !query.includes(` ${phrase} `) && !query.startsWith(`${phrase} `)) {
126
+ return [
127
+ `phrase ${named} cites committed query ${JSON.stringify(attestation.detail)}, which does not contain it as a subject`,
128
+ ];
129
+ }
130
+ return [];
131
+ }
132
+ case "concept-description": {
133
+ if (attestation.reference !== entry.activity) {
134
+ return [
135
+ `phrase ${named} cites the description of ${JSON.stringify(attestation.reference)} while naming activity ${JSON.stringify(entry.activity)}`,
136
+ ];
137
+ }
138
+ return attestation.detail.trim() ? [] : [`phrase ${named} cites a description clause but quotes none of it`];
139
+ }
140
+ case "derived-form":
141
+ case "regional-register": {
142
+ const base = byPhrase.get(normalizeActivityPhrase(attestation.base));
143
+ if (!base) {
144
+ return [
145
+ `phrase ${named} is attested against base ${JSON.stringify(attestation.base)}, which the lexicon does not declare`,
146
+ ];
147
+ }
148
+ const problems = [];
149
+ if (base.attestation.kind === "derived-form") {
150
+ problems.push(`phrase ${named} is derived from ${JSON.stringify(attestation.base)}, which is itself derived — an attestation chain that never reaches a committed record attests nothing`);
151
+ }
152
+ if (base.activity !== entry.activity) {
153
+ problems.push(`phrase ${named} names activity ${JSON.stringify(entry.activity)} while its base names ${JSON.stringify(base.activity)}`);
154
+ }
155
+ if (attestation.kind === "derived-form") {
156
+ if (!DERIVATIONS.includes(attestation.derivation)) {
157
+ problems.push(`phrase ${named} declares derivation ${JSON.stringify(attestation.derivation)}, which is not a known one`);
158
+ }
159
+ if (JSON.stringify(entry.locales ?? null) !== JSON.stringify(base.locales ?? null)) {
160
+ problems.push(`phrase ${named} is a derived form whose locale scope differs from its base — a regular transformation does not change where a phrasing is used`);
161
+ }
162
+ }
163
+ return problems;
164
+ }
165
+ }
166
+ }
167
+ let committed;
168
+ /**
169
+ * Read the lexicon, refusing one the audit rejects.
170
+ *
171
+ * The committed read is memoized; an explicit path is read fresh, which is what a test asserting a refusal needs.
172
+ */
173
+ export function readActivityLexicon(path = ACTIVITY_LEXICON_PATH) {
174
+ if (path === ACTIVITY_LEXICON_PATH && committed)
175
+ return committed;
176
+ // A corrupt shipped vocabulary is a broken build, and the SyntaxError names the offset. Zero dependencies here, so
177
+ // `@mailwoman/core`'s parse wrappers are deliberately out of reach.
178
+ // oxlint-disable-next-line no-restricted-properties -- zero-dependency leaf; corrupt shipped data must throw with its offset
179
+ const lexicon = JSON.parse(readFileSync(path, "utf8"));
180
+ const problems = auditActivityLexicon(lexicon);
181
+ if (problems.length) {
182
+ throw new Error([`activity-lexicon: ${path} does not audit:`].concat(problems.map((problem) => ` - ${problem}`)).join("\n"));
183
+ }
184
+ if (path === ACTIVITY_LEXICON_PATH) {
185
+ committed = lexicon;
186
+ }
187
+ return lexicon;
188
+ }
189
+ //# sourceMappingURL=lexicon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lexicon.js","sourceRoot":"","sources":["../lexicon.ts"],"names":[],"mappings":"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"}
package/out/types.d.ts ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Types for the reviewed activity-phrase lexicon.
7
+ *
8
+ * AN ENTRY DECLARES A SURFACE FORM AND NOTHING ELSE. The phrase is what a person types; the activity is a concept
9
+ * identifier some other artifact owns. Which entity kinds afford that activity, in which country, with what modality
10
+ * and on whose authority are not stated here and cannot be — this vocabulary is recognition, and a consumer that
11
+ * wants the semantics reads the artifact that carries them.
12
+ *
13
+ * EVERY ENTRY NAMES WHAT ATTESTS IT. {@linkcode ActivityPhraseAttestation} is a discriminated union rather than a
14
+ * free-text field because an attestation nobody can check is indistinguishable from an invented one: each member
15
+ * points at something committed — a query row, another entry in this lexicon, a synonym in a committed vocabulary,
16
+ * or a clause of the activity concept's own description — and names the exact text it rests on.
17
+ */
18
+ /**
19
+ * Where a record came from. Field-for-field the shape `@mailwoman/geographic-model` uses for its own provenance, so a
20
+ * consumer carrying both never has to translate between two spellings of the same idea. It is restated rather than
21
+ * imported because this package declares zero dependencies.
22
+ */
23
+ export interface ActivityLexiconProvenance {
24
+ /**
25
+ * The naming authority, dataset, publication, or curator.
26
+ */
27
+ source: string;
28
+ sourceVersion?: string;
29
+ /**
30
+ * The identifier of the specific record within the source.
31
+ */
32
+ sourceRecord?: string;
33
+ sourceURL?: string;
34
+ /**
35
+ * ISO 8601 calendar date the record was authored, `YYYY-MM-DD`.
36
+ */
37
+ authoredAt?: string;
38
+ notes?: string;
39
+ }
40
+ /**
41
+ * The phrase is the subject of a query committed to this repository. The strongest attestation available: the form was
42
+ * written down for grading before it was written down for recognition.
43
+ */
44
+ export interface CommittedQueryAttestation {
45
+ kind: "committed-query";
46
+ /**
47
+ * `<repository-relative file>#<row id>`.
48
+ */
49
+ reference: string;
50
+ /**
51
+ * The committed query verbatim. A reader can grep for it; a test can require the phrase to end it.
52
+ */
53
+ detail: string;
54
+ }
55
+ /**
56
+ * Which regular transformation produced a derived form. Deliberately closed: an open list would let any pair of strings
57
+ * be declared related.
58
+ */
59
+ export type ActivityPhraseDerivation = "plural" | "nominalization" | "verb-phrase" | "possessive";
60
+ /**
61
+ * The phrase is a regular transformation of another entry in this lexicon. The base carries the authority; the
62
+ * derivation names the transformation.
63
+ */
64
+ export interface DerivedFormAttestation {
65
+ kind: "derived-form";
66
+ /**
67
+ * Another entry's `phrase`. The base may not itself be derived — an attestation chain that never reaches a committed
68
+ * record attests nothing.
69
+ */
70
+ base: string;
71
+ derivation: ActivityPhraseDerivation;
72
+ }
73
+ /**
74
+ * The phrase is the regional-register counterpart of another entry, and the register split it follows is recorded in a
75
+ * committed vocabulary rather than asserted here.
76
+ */
77
+ export interface RegionalRegisterAttestation {
78
+ kind: "regional-register";
79
+ /**
80
+ * The committed record that carries the register split, e.g. a `@mailwoman/poi-taxonomy` synonym phrase.
81
+ */
82
+ reference: string;
83
+ /**
84
+ * The entry whose register this one mirrors.
85
+ */
86
+ base: string;
87
+ /**
88
+ * The locales the referenced record carries, written out so a reader sees what the entry's own `locales` are copied
89
+ * from.
90
+ */
91
+ detail: string;
92
+ }
93
+ /**
94
+ * The phrase paraphrases a clause of the activity concept's own description. The clause is quoted so the citation can
95
+ * be checked against the compiled artifact rather than believed.
96
+ */
97
+ export interface ConceptDescriptionAttestation {
98
+ kind: "concept-description";
99
+ /**
100
+ * The concept identifier whose description is cited — the entry's own `activity`.
101
+ */
102
+ reference: string;
103
+ /**
104
+ * The exact substring of that concept's description the phrase rests on.
105
+ */
106
+ detail: string;
107
+ }
108
+ export type ActivityPhraseAttestation = CommittedQueryAttestation | DerivedFormAttestation | RegionalRegisterAttestation | ConceptDescriptionAttestation;
109
+ /**
110
+ * One reviewed surface form.
111
+ */
112
+ export interface ActivityPhraseEntry {
113
+ /**
114
+ * The text a person types, as a reader would write it. Comparison is done over the normalized form.
115
+ */
116
+ phrase: string;
117
+ /**
118
+ * A concept identifier of kind `activity`, owned by whichever artifact the consumer resolves against. This lexicon
119
+ * neither defines the concept nor claims anything about it.
120
+ */
121
+ activity: string;
122
+ /**
123
+ * BCP-47 tags where the phrasing is in active use, following the `@mailwoman/variant-aliases` semantics: an exact tag
124
+ * match is a full match, a language-only match is a weaker one, and nothing else matches.
125
+ *
126
+ * ABSENT MEANS UNSCOPED, and is not the same as an empty list: a phrase used everywhere carries no tags, while a
127
+ * phrase scoped to nowhere is a record that can never fire, which the audit refuses.
128
+ */
129
+ locales?: ReadonlyArray<string>;
130
+ /**
131
+ * How the entry was produced. `curated` is the only admissible value: a phrase mined from traffic would be a
132
+ * measurement, and this vocabulary carries none.
133
+ */
134
+ source: "curated";
135
+ attestation: ActivityPhraseAttestation;
136
+ /**
137
+ * Why the entry is in the lexicon, for a reader deciding whether it still belongs.
138
+ */
139
+ note: string;
140
+ }
141
+ /**
142
+ * The committed lexicon.
143
+ */
144
+ export interface ActivityPhraseLexicon {
145
+ lexiconID: string;
146
+ version: string;
147
+ provenance: ActivityLexiconProvenance;
148
+ phrases: ActivityPhraseEntry[];
149
+ }
150
+ /**
151
+ * How an entry's locale scope met the locale a query was read under.
152
+ *
153
+ * - `unscoped` — the entry declares no locales and answers under any.
154
+ * - `exact` — the query's locale tag is one the entry declares.
155
+ * - `language` — only the language subtag agrees. Weaker on purpose: a regional phrasing reached through its language
156
+ * alone is a guess about the region.
157
+ */
158
+ export type ActivityPhraseLocaleScope = "unscoped" | "exact" | "language";
159
+ /**
160
+ * One entry matched under one locale.
161
+ */
162
+ export interface ActivityPhraseLocaleMatch {
163
+ scope: ActivityPhraseLocaleScope;
164
+ /**
165
+ * `1` for `unscoped` and `exact`, `0.5` for `language` — the numbers `@mailwoman/variant-aliases` reports for the
166
+ * same three cases, carried over rather than chosen here.
167
+ */
168
+ confidence: number;
169
+ }
170
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACzC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACzC,IAAI,EAAE,iBAAiB,CAAA;IACvB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,gBAAgB,GAAG,aAAa,GAAG,YAAY,CAAA;AAEjG;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,cAAc,CAAA;IACpB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,wBAAwB,CAAA;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC3C,IAAI,EAAE,mBAAmB,CAAA;IACzB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC7C,IAAI,EAAE,qBAAqB,CAAA;IAC3B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,yBAAyB,GAClC,yBAAyB,GACzB,sBAAsB,GACtB,2BAA2B,GAC3B,6BAA6B,CAAA;AAEhC;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC/B;;;OAGG;IACH,MAAM,EAAE,SAAS,CAAA;IACjB,WAAW,EAAE,yBAAyB,CAAA;IACtC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,yBAAyB,CAAA;IACrC,OAAO,EAAE,mBAAmB,EAAE,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzE;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACzC,KAAK,EAAE,yBAAyB,CAAA;IAChC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;CAClB"}
package/out/types.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Types for the reviewed activity-phrase lexicon.
7
+ *
8
+ * AN ENTRY DECLARES A SURFACE FORM AND NOTHING ELSE. The phrase is what a person types; the activity is a concept
9
+ * identifier some other artifact owns. Which entity kinds afford that activity, in which country, with what modality
10
+ * and on whose authority are not stated here and cannot be — this vocabulary is recognition, and a consumer that
11
+ * wants the semantics reads the artifact that carries them.
12
+ *
13
+ * EVERY ENTRY NAMES WHAT ATTESTS IT. {@linkcode ActivityPhraseAttestation} is a discriminated union rather than a
14
+ * free-text field because an attestation nobody can check is indistinguishable from an invented one: each member
15
+ * points at something committed — a query row, another entry in this lexicon, a synonym in a committed vocabulary,
16
+ * or a clause of the activity concept's own description — and names the exact text it rests on.
17
+ */
18
+ export {};
19
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@mailwoman/activity-lexicon",
3
+ "version": "0.0.0",
4
+ "description": "Reviewed surface forms for activity concepts — the phrases a person types for what they want to do, each carrying the record that attests it and the locales it is used in.",
5
+ "keywords": [
6
+ "activity",
7
+ "lexicon",
8
+ "phrases",
9
+ "provenance"
10
+ ],
11
+ "license": "AGPL-3.0-only OR LicenseRef-Commercial",
12
+ "contributors": [
13
+ {
14
+ "name": "Teffen Ellis",
15
+ "email": "teffen@sister.software"
16
+ }
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/sister-software/mailwoman.git",
21
+ "directory": "packages/activity-lexicon"
22
+ },
23
+ "files": [
24
+ "out/**/*.js",
25
+ "out/**/*.js.map",
26
+ "out/**/*.d.ts",
27
+ "out/**/*.d.ts.map",
28
+ "data/**/*.json",
29
+ "*.ts",
30
+ "**/*.ts",
31
+ "!*.test.ts",
32
+ "!**/*.test.ts",
33
+ "!test/**"
34
+ ],
35
+ "type": "module",
36
+ "exports": {
37
+ "./package.json": "./package.json",
38
+ ".": {
39
+ "types": "./out/index.d.ts",
40
+ "default": "./out/index.js"
41
+ },
42
+ "./lexicon": {
43
+ "types": "./out/lexicon.d.ts",
44
+ "default": "./out/lexicon.js"
45
+ },
46
+ "./types": {
47
+ "types": "./out/types.d.ts",
48
+ "default": "./out/types.js"
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "exports": {
54
+ "./package.json": "./package.json",
55
+ ".": {
56
+ "types": "./out/index.d.ts",
57
+ "default": "./out/index.js"
58
+ },
59
+ "./lexicon": {
60
+ "types": "./out/lexicon.d.ts",
61
+ "default": "./out/lexicon.js"
62
+ },
63
+ "./types": {
64
+ "types": "./out/types.d.ts",
65
+ "default": "./out/types.js"
66
+ }
67
+ }
68
+ },
69
+ "engines": {
70
+ "node": ">=24.18.0"
71
+ }
72
+ }
package/types.ts ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Types for the reviewed activity-phrase lexicon.
7
+ *
8
+ * AN ENTRY DECLARES A SURFACE FORM AND NOTHING ELSE. The phrase is what a person types; the activity is a concept
9
+ * identifier some other artifact owns. Which entity kinds afford that activity, in which country, with what modality
10
+ * and on whose authority are not stated here and cannot be — this vocabulary is recognition, and a consumer that
11
+ * wants the semantics reads the artifact that carries them.
12
+ *
13
+ * EVERY ENTRY NAMES WHAT ATTESTS IT. {@linkcode ActivityPhraseAttestation} is a discriminated union rather than a
14
+ * free-text field because an attestation nobody can check is indistinguishable from an invented one: each member
15
+ * points at something committed — a query row, another entry in this lexicon, a synonym in a committed vocabulary,
16
+ * or a clause of the activity concept's own description — and names the exact text it rests on.
17
+ */
18
+
19
+ /**
20
+ * Where a record came from. Field-for-field the shape `@mailwoman/geographic-model` uses for its own provenance, so a
21
+ * consumer carrying both never has to translate between two spellings of the same idea. It is restated rather than
22
+ * imported because this package declares zero dependencies.
23
+ */
24
+ export interface ActivityLexiconProvenance {
25
+ /**
26
+ * The naming authority, dataset, publication, or curator.
27
+ */
28
+ source: string
29
+ sourceVersion?: string
30
+ /**
31
+ * The identifier of the specific record within the source.
32
+ */
33
+ sourceRecord?: string
34
+ sourceURL?: string
35
+ /**
36
+ * ISO 8601 calendar date the record was authored, `YYYY-MM-DD`.
37
+ */
38
+ authoredAt?: string
39
+ notes?: string
40
+ }
41
+
42
+ /**
43
+ * The phrase is the subject of a query committed to this repository. The strongest attestation available: the form was
44
+ * written down for grading before it was written down for recognition.
45
+ */
46
+ export interface CommittedQueryAttestation {
47
+ kind: "committed-query"
48
+ /**
49
+ * `<repository-relative file>#<row id>`.
50
+ */
51
+ reference: string
52
+ /**
53
+ * The committed query verbatim. A reader can grep for it; a test can require the phrase to end it.
54
+ */
55
+ detail: string
56
+ }
57
+
58
+ /**
59
+ * Which regular transformation produced a derived form. Deliberately closed: an open list would let any pair of strings
60
+ * be declared related.
61
+ */
62
+ export type ActivityPhraseDerivation = "plural" | "nominalization" | "verb-phrase" | "possessive"
63
+
64
+ /**
65
+ * The phrase is a regular transformation of another entry in this lexicon. The base carries the authority; the
66
+ * derivation names the transformation.
67
+ */
68
+ export interface DerivedFormAttestation {
69
+ kind: "derived-form"
70
+ /**
71
+ * Another entry's `phrase`. The base may not itself be derived — an attestation chain that never reaches a committed
72
+ * record attests nothing.
73
+ */
74
+ base: string
75
+ derivation: ActivityPhraseDerivation
76
+ }
77
+
78
+ /**
79
+ * The phrase is the regional-register counterpart of another entry, and the register split it follows is recorded in a
80
+ * committed vocabulary rather than asserted here.
81
+ */
82
+ export interface RegionalRegisterAttestation {
83
+ kind: "regional-register"
84
+ /**
85
+ * The committed record that carries the register split, e.g. a `@mailwoman/poi-taxonomy` synonym phrase.
86
+ */
87
+ reference: string
88
+ /**
89
+ * The entry whose register this one mirrors.
90
+ */
91
+ base: string
92
+ /**
93
+ * The locales the referenced record carries, written out so a reader sees what the entry's own `locales` are copied
94
+ * from.
95
+ */
96
+ detail: string
97
+ }
98
+
99
+ /**
100
+ * The phrase paraphrases a clause of the activity concept's own description. The clause is quoted so the citation can
101
+ * be checked against the compiled artifact rather than believed.
102
+ */
103
+ export interface ConceptDescriptionAttestation {
104
+ kind: "concept-description"
105
+ /**
106
+ * The concept identifier whose description is cited — the entry's own `activity`.
107
+ */
108
+ reference: string
109
+ /**
110
+ * The exact substring of that concept's description the phrase rests on.
111
+ */
112
+ detail: string
113
+ }
114
+
115
+ export type ActivityPhraseAttestation =
116
+ | CommittedQueryAttestation
117
+ | DerivedFormAttestation
118
+ | RegionalRegisterAttestation
119
+ | ConceptDescriptionAttestation
120
+
121
+ /**
122
+ * One reviewed surface form.
123
+ */
124
+ export interface ActivityPhraseEntry {
125
+ /**
126
+ * The text a person types, as a reader would write it. Comparison is done over the normalized form.
127
+ */
128
+ phrase: string
129
+ /**
130
+ * A concept identifier of kind `activity`, owned by whichever artifact the consumer resolves against. This lexicon
131
+ * neither defines the concept nor claims anything about it.
132
+ */
133
+ activity: string
134
+ /**
135
+ * BCP-47 tags where the phrasing is in active use, following the `@mailwoman/variant-aliases` semantics: an exact tag
136
+ * match is a full match, a language-only match is a weaker one, and nothing else matches.
137
+ *
138
+ * ABSENT MEANS UNSCOPED, and is not the same as an empty list: a phrase used everywhere carries no tags, while a
139
+ * phrase scoped to nowhere is a record that can never fire, which the audit refuses.
140
+ */
141
+ locales?: ReadonlyArray<string>
142
+ /**
143
+ * How the entry was produced. `curated` is the only admissible value: a phrase mined from traffic would be a
144
+ * measurement, and this vocabulary carries none.
145
+ */
146
+ source: "curated"
147
+ attestation: ActivityPhraseAttestation
148
+ /**
149
+ * Why the entry is in the lexicon, for a reader deciding whether it still belongs.
150
+ */
151
+ note: string
152
+ }
153
+
154
+ /**
155
+ * The committed lexicon.
156
+ */
157
+ export interface ActivityPhraseLexicon {
158
+ lexiconID: string
159
+ version: string
160
+ provenance: ActivityLexiconProvenance
161
+ phrases: ActivityPhraseEntry[]
162
+ }
163
+
164
+ /**
165
+ * How an entry's locale scope met the locale a query was read under.
166
+ *
167
+ * - `unscoped` — the entry declares no locales and answers under any.
168
+ * - `exact` — the query's locale tag is one the entry declares.
169
+ * - `language` — only the language subtag agrees. Weaker on purpose: a regional phrasing reached through its language
170
+ * alone is a guess about the region.
171
+ */
172
+ export type ActivityPhraseLocaleScope = "unscoped" | "exact" | "language"
173
+
174
+ /**
175
+ * One entry matched under one locale.
176
+ */
177
+ export interface ActivityPhraseLocaleMatch {
178
+ scope: ActivityPhraseLocaleScope
179
+ /**
180
+ * `1` for `unscoped` and `exact`, `0.5` for `language` — the numbers `@mailwoman/variant-aliases` reports for the
181
+ * same three cases, carried over rather than chosen here.
182
+ */
183
+ confidence: number
184
+ }