linguistic-enricher 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # linguistic-enricher
2
+
3
+ **linguistic-enricher** is a deterministic linguistic processing pipeline for Node.js that incrementally enriches plain text with structured linguistic information.
4
+
5
+ It takes raw text as input and produces a single, fully structured document that contains:
6
+
7
+ - a canonical text surface,
8
+ - sentence segmentation and tokenization,
9
+ - part-of-speech information,
10
+ - multi-word expressions (MWEs),
11
+ - shallow phrase structure (chunks),
12
+ - syntactic heads,
13
+ - and deterministic, token-level linguistic relations.
14
+
15
+ The pipeline is **library-first**, **schema-driven**, and **additive by design**.
16
+ It focuses strictly on *linguistic structure*, not domain logic, business rules, or normative interpretation.
17
+
18
+ ---
19
+
20
+ ## What this project is
21
+
22
+ `linguistic-enricher` is best described as a **linguistic enricher**:
23
+
24
+ - It **adds structure** to text.
25
+ - It does **not rewrite or reinterpret** the original text.
26
+ - It does **not apply domain semantics, rules, or policies**.
27
+ - It produces **reproducible, explainable results**.
28
+
29
+ The output is a single, incrementally enriched document that represents the linguistic state of the input text up to the level of accepted linguistic relations.
30
+
31
+ This makes the package suitable as:
32
+
33
+ - a preprocessing layer for downstream NLP systems,
34
+ - a compiler-like front end for controlled or structured language processing,
35
+ - or a general-purpose linguistic analysis engine embedded directly into Node.js applications.
36
+
37
+ ---
38
+
39
+ ## What this project is not
40
+
41
+ `linguistic-enricher` deliberately does **not**:
42
+
43
+ - perform business or domain reasoning,
44
+ - assert norms, obligations, or policies,
45
+ - infer facts beyond what is linguistically explicit in the text,
46
+ - or depend on any specific downstream framework or ontology.
47
+
48
+ The pipeline’s authoritative output ends at **linguistic relations**.
49
+ Anything beyond that (assertions, governance rules, domain models) belongs in a separate, downstream layer.
50
+
51
+ ---
52
+
53
+ ## Core principles
54
+
55
+ ### Deterministic
56
+
57
+ Given the same input text and configuration, the pipeline produces the same output.
58
+ Probabilistic model output is treated as observational input only and is never accepted as authoritative truth.
59
+
60
+ ### Additive
61
+
62
+ The text is enriched step by step.
63
+ Earlier structures are never removed or rewritten.
64
+ Later stages only add structure or precision.
65
+
66
+ ### Anchored
67
+
68
+ All annotations are explicitly anchored to the canonical text using character spans, tokens, or segments.
69
+ There is no implicit or floating interpretation.
70
+
71
+ ### Schema-driven
72
+
73
+ The output conforms to a single, evolving document schema that represents the complete linguistic enrichment state.
74
+
75
+ ### Library-first
76
+
77
+ All functionality is available through a JavaScript API and can be embedded directly into any Node.js project.
78
+ A CLI is provided only as a thin wrapper around the same API.
79
+
80
+ ---
81
+
82
+ ## High-level pipeline overview
83
+
84
+ Conceptually, the pipeline performs the following transformations:
85
+
86
+ 1. **Canonicalization**
87
+ A single authoritative text surface is established. All later offsets and annotations refer to this text.
88
+
89
+ 2. **Segmentation and tokenization**
90
+ The text is segmented (typically into sentences) and tokenized into a stable token stream.
91
+
92
+ 3. **Part-of-speech tagging**
93
+ Each token is enriched with grammatical category information.
94
+
95
+ 4. **Multi-word expression detection and materialization**
96
+ Lexical units spanning multiple tokens are detected and deterministically materialized as authoritative MWEs.
97
+
98
+ 5. **Shallow parsing (chunking)**
99
+ Tokens are grouped into flat syntactic phrases (e.g. noun phrases, verb phrases).
100
+
101
+ 6. **Head identification**
102
+ Each phrase receives exactly one deterministic syntactic head token.
103
+
104
+ 7. **Relation extraction**
105
+ Token-level linguistic relations are derived deterministically and stored as accepted relations.
106
+
107
+ Relations represent **linguistic predicate–argument structure**, not conceptual, ontological, or domain semantics.
108
+
109
+ The result is a fully enriched linguistic document with stable structure and traceable provenance.
110
+
111
+ ---
112
+
113
+ ## Input and output
114
+
115
+ ### Input
116
+
117
+ The minimal input is plain text:
118
+
119
+ ```js
120
+ const text = `
121
+ A webshop is an online store where customers can select products,
122
+ place them in a cart, and complete a purchase.
123
+ `;
124
+ ```
125
+
126
+ Optionally, a partially enriched document that already conforms to the schema may be provided to resume processing.
127
+
128
+ ---
129
+
130
+ ### Output
131
+
132
+ The output is a single JavaScript object representing the enriched document.
133
+
134
+ It includes:
135
+
136
+ - the canonical text,
137
+ - segments and tokens with stable spans,
138
+ - annotations for MWEs, chunks, and heads,
139
+ - and accepted token-level relations.
140
+
141
+ The output is designed to be:
142
+
143
+ - machine-readable,
144
+ - human-inspectable,
145
+ - and suitable for downstream processing.
146
+
147
+ ---
148
+
149
+ ## Usage as a library
150
+
151
+ ```js
152
+ const { runPipeline } = require("linguistic-enricher");
153
+
154
+ const result = await runPipeline(text, {
155
+ target: "relations_extracted"
156
+ });
157
+
158
+ console.log(result.stage);
159
+ ```
160
+
161
+ The library API is the primary interface.
162
+ File I/O, serialization, and CLI concerns are intentionally kept outside the core logic.
163
+
164
+ ---
165
+
166
+ ## Current maturity and semantic parity
167
+
168
+ This package currently provides a stable baseline implementation of the full 00..11 pipeline surface.
169
+
170
+ - Baseline orchestration, validation hooks, CLI/API integration, and deterministic utilities are implemented and tested.
171
+ - Stage-by-stage linguistic parity hardening against the intended semantic baseline is still in progress.
172
+ - The legacy semantic corpus is treated as a semantic reference only, not as a technical implementation template.
173
+
174
+ ---
175
+
176
+ ## Optional external services
177
+
178
+ ### Lexical signals (Wikipedia title index)
179
+
180
+ Some enrichment stages optionally use **lexical signals provided by a Wikipedia title index service**.
181
+
182
+ This service is expected to expose the HTTP API of
183
+ [`wikipedia-title-index`](https://www.npmjs.com/package/wikipedia-title-index) and provides deterministic
184
+ title-based lookup signals (exact matches, prefix counts, and variant matches).
185
+
186
+ `linguistic-enricher` does **not** embed or bundle this data.
187
+ Instead, an external service endpoint can be configured:
188
+
189
+ ```js
190
+ await runPipeline(text, {
191
+ services: {
192
+ "wikipedia-title-index": {
193
+ endpoint: "http://localhost:3000"
194
+ }
195
+ }
196
+ });
197
+ ```
198
+
199
+ If no endpoint is configured, all enrichment stages that depend on lexical title signals run deterministically without those signals.
200
+
201
+ ---
202
+
203
+ ## Python runtime integration
204
+
205
+ Some enrichment stages rely on established Python-based NLP tooling (for example for part-of-speech tagging and dependency analysis).
206
+
207
+ This tooling is handled internally:
208
+
209
+ - Python is invoked as a subprocess.
210
+ - Communication uses JSON over stdin/stdout only.
211
+ - Consumers of the Node.js API do not interact with Python directly.
212
+
213
+ A built-in runtime check (`doctor`) can be used to verify Python availability and required dependencies.
214
+
215
+ ---
216
+
217
+ ## CLI (optional)
218
+
219
+ A command-line interface is provided for convenience:
220
+
221
+ ```bash
222
+ npx linguistic-enricher run --in input.txt --out result.json
223
+ npx linguistic-enricher run --text "A webshop is an online store." --target canonical --pretty
224
+ npx linguistic-enricher validate --in result.json
225
+ npx linguistic-enricher doctor
226
+ ```
227
+
228
+ The CLI is a thin wrapper around the same library API and is fully cross-platform.
229
+
230
+ ---
231
+
232
+ ## Design boundary
233
+
234
+ `linguistic-enricher` intentionally produces authoritative output only up to **linguistic relations**.
235
+
236
+ The underlying document schema is forward-compatible and may include later enrichment stages, but this package itself does **not** generate normative assertions, obligations, or domain-level interpretations.
237
+
238
+ This boundary keeps the project:
239
+
240
+ - reusable,
241
+ - framework-agnostic,
242
+ - and stable as a foundational linguistic layer.
243
+
244
+ ---
245
+
246
+ ## License
247
+
248
+ MIT
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const api = require("../src/index");
6
+
7
+ function usageError(message) {
8
+ const error = new Error(message);
9
+ error.code = "E_CLI_USAGE";
10
+ return error;
11
+ }
12
+
13
+ function printGlobalUsage() {
14
+ console.log("Usage: linguistic-enricher <command> [options]");
15
+ console.log("");
16
+ console.log("Commands:");
17
+ console.log(" run Run pipeline and print JSON");
18
+ console.log(" validate Validate JSON document");
19
+ console.log(" doctor Check runtime prerequisites");
20
+ console.log("");
21
+ console.log("Use \"linguistic-enricher <command> --help\" for command options.");
22
+ }
23
+
24
+ function printRunUsage() {
25
+ console.log("Usage: linguistic-enricher run (--text <text> | --in <file>) [options]");
26
+ console.log("Options:");
27
+ console.log(" --out <file> Write JSON output to file");
28
+ console.log(" --target <pipeline-target> Pipeline cutoff target");
29
+ console.log(" --pretty Pretty-print JSON");
30
+ console.log(" --service-wti-endpoint <url> wikipedia-title-index endpoint");
31
+ console.log(" --timeout-ms <ms> Service/runtime timeout");
32
+ console.log(" --strict Strict mode for optional dependencies");
33
+ }
34
+
35
+ function printValidateUsage() {
36
+ console.log("Usage: linguistic-enricher validate --in <file> [--pretty]");
37
+ }
38
+
39
+ function printDoctorUsage() {
40
+ console.log("Usage: linguistic-enricher doctor [--python-executable <path>] [--timeout-ms <ms>]");
41
+ }
42
+
43
+ function parseArgs(argv, command) {
44
+ const args = argv.slice(3);
45
+ const out = {};
46
+ const noValueFlags = new Set(["--pretty", "--strict", "--help", "-h"]);
47
+ const needsValue = new Set(["--in", "--text", "--out", "--target", "--service-wti-endpoint", "--timeout-ms", "--python-executable"]);
48
+ const allowedByCommand = {
49
+ run: new Set(["--in", "--text", "--out", "--target", "--pretty", "--service-wti-endpoint", "--timeout-ms", "--strict", "--help", "-h"]),
50
+ validate: new Set(["--in", "--pretty", "--help", "-h"]),
51
+ doctor: new Set(["--python-executable", "--timeout-ms", "--help", "-h"])
52
+ };
53
+ const allowed = allowedByCommand[command] || new Set(["--help", "-h"]);
54
+
55
+ for (let i = 0; i < args.length; i += 1) {
56
+ const arg = args[i];
57
+ if (!arg.startsWith("-")) {
58
+ throw usageError("Unexpected positional argument: " + arg);
59
+ }
60
+ if (!allowed.has(arg)) {
61
+ throw usageError("Unknown flag for " + command + ": " + arg);
62
+ }
63
+ if (noValueFlags.has(arg)) {
64
+ if (arg === "--help" || arg === "-h") {
65
+ out.help = true;
66
+ } else {
67
+ out[arg.slice(2)] = true;
68
+ }
69
+ continue;
70
+ }
71
+ if (needsValue.has(arg)) {
72
+ const next = args[i + 1];
73
+ if (!next || next.startsWith("-")) {
74
+ throw usageError("Missing value for flag: " + arg);
75
+ }
76
+ out[arg.slice(2)] = next;
77
+ i += 1;
78
+ continue;
79
+ }
80
+ throw usageError("Unknown flag: " + arg);
81
+ }
82
+
83
+ return out;
84
+ }
85
+
86
+ function readInput(options) {
87
+ if (options.text && options.in) {
88
+ throw new Error("Use either --text or --in, not both.");
89
+ }
90
+
91
+ if (options.text) {
92
+ return options.text;
93
+ }
94
+
95
+ if (options.in) {
96
+ return fs.readFileSync(options.in, "utf8");
97
+ }
98
+
99
+ throw new Error("Missing input. Use --text or --in.");
100
+ }
101
+
102
+ async function run(argv) {
103
+ const options = parseArgs(argv, "run");
104
+ if (options.help) {
105
+ printRunUsage();
106
+ return;
107
+ }
108
+ const input = readInput(options);
109
+
110
+ const pipelineOptions = {
111
+ target: options.target,
112
+ timeoutMs: options["timeout-ms"] ? Number(options["timeout-ms"]) : undefined,
113
+ services: options["service-wti-endpoint"]
114
+ ? { "wikipedia-title-index": { endpoint: options["service-wti-endpoint"] } }
115
+ : undefined,
116
+ strict: options.strict === true
117
+ };
118
+
119
+ const result = await api.runPipeline(input, pipelineOptions);
120
+ const serialized = JSON.stringify(result, null, options.pretty ? 2 : 0);
121
+
122
+ if (options.out) {
123
+ fs.writeFileSync(options.out, serialized + "\n", "utf8");
124
+ } else {
125
+ console.log(serialized);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * CLI command for `doctor`.
131
+ * @returns {Promise<void>}
132
+ */
133
+ async function doctor(argv) {
134
+ const options = parseArgs(argv, "doctor");
135
+ if (options.help) {
136
+ printDoctorUsage();
137
+ return;
138
+ }
139
+ const result = await api.runDoctor({
140
+ pythonExecutable: options["python-executable"] || process.env.PYTHON_EXECUTABLE,
141
+ timeoutMs: options["timeout-ms"] ? Number(options["timeout-ms"]) : undefined
142
+ });
143
+ console.log("Doctor checks passed.");
144
+ console.log(
145
+ "Python executable: " +
146
+ result.python.executable +
147
+ " (" +
148
+ result.python.version +
149
+ ")"
150
+ );
151
+ console.log("spaCy: ok");
152
+ console.log("spaCy model: " + result.model.name + " (installed)");
153
+ }
154
+
155
+ function validate(argv) {
156
+ const options = parseArgs(argv, "validate");
157
+ if (options.help) {
158
+ printValidateUsage();
159
+ return;
160
+ }
161
+ if (!options.in) {
162
+ throw usageError("validate requires --in <path>");
163
+ }
164
+
165
+ const raw = fs.readFileSync(options.in, "utf8");
166
+ const doc = JSON.parse(raw);
167
+ const result = api.validateDocument(doc);
168
+ console.log(JSON.stringify(result, null, options.pretty ? 2 : 0));
169
+ }
170
+
171
+ async function main(argv) {
172
+ const command = argv[2];
173
+
174
+ if (!command || command === "--help" || command === "-h") {
175
+ printGlobalUsage();
176
+ return;
177
+ }
178
+
179
+ if (command === "run") {
180
+ await run(argv);
181
+ return;
182
+ }
183
+
184
+ if (command === "doctor") {
185
+ await doctor(argv);
186
+ return;
187
+ }
188
+
189
+ if (command === "validate") {
190
+ validate(argv);
191
+ return;
192
+ }
193
+
194
+ throw usageError("Unknown command. Supported commands: run, doctor, validate");
195
+ }
196
+
197
+ if (require.main === module) {
198
+ main(process.argv).catch(function onMainError(error) {
199
+ const code = error && error.code ? error.code : "E_CLI_FAILED";
200
+ console.error("CLI failed [" + code + "]: " + error.message);
201
+ if (code === "E_CLI_USAGE") {
202
+ const cmd = process.argv[2];
203
+ if (cmd === "run") {
204
+ printRunUsage();
205
+ } else if (cmd === "validate") {
206
+ printValidateUsage();
207
+ } else if (cmd === "doctor") {
208
+ printDoctorUsage();
209
+ } else {
210
+ printGlobalUsage();
211
+ }
212
+ }
213
+ process.exit(1);
214
+ });
215
+ }
216
+
217
+ module.exports = {
218
+ run,
219
+ doctor,
220
+ validate,
221
+ main
222
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "linguistic-enricher",
3
+ "version": "1.0.0",
4
+ "description": "Deterministic linguistic enrichment pipeline for Node.js (CommonJS)",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/svenschaefer/linguistic-enricher"
8
+ },
9
+ "main": "src/index.js",
10
+ "bin": {
11
+ "linguistic-enricher": "bin/linguistic-enricher.js"
12
+ },
13
+ "files": [
14
+ "src/**",
15
+ "bin/**",
16
+ "README.md",
17
+ "schema.json",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "test": "npm run lint && npm run test:unit && npm run test:integration",
22
+ "test:unit": "node --test test/unit/*.test.js",
23
+ "test:integration": "node --test test/integration/*.test.js",
24
+ "lint": "eslint src bin test",
25
+ "doctor": "node bin/linguistic-enricher.js doctor",
26
+ "check:cache-live": "node scripts/check-live-cache.js"
27
+ },
28
+ "keywords": [
29
+ "nlp",
30
+ "linguistics",
31
+ "pipeline",
32
+ "commonjs"
33
+ ],
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=24.10.0"
37
+ },
38
+ "devDependencies": {
39
+ "eslint": "^8.57.0"
40
+ },
41
+ "dependencies": {
42
+ "ajv": "^8.17.1",
43
+ "sbd": "^1.0.19",
44
+ "wikipedia-title-index": "^1.2.6",
45
+ "wink-pos-tagger": "^2.2.2",
46
+ "wink-tokenizer": "^5.2.0"
47
+ }
48
+ }