@cosyte/script-utils 0.1.2 → 0.1.4
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 +30 -12
- package/LICENSE +1 -1
- package/README.md +224 -15
- package/agent-notes.d.ts +92 -0
- package/agent-notes.js +512 -0
- package/attw.d.ts +18 -0
- package/attw.js +188 -8
- package/bin/cosyte-agent-notes.js +23 -0
- package/bin/cosyte-attw.js +24 -0
- package/bin/cosyte-catalog.js +23 -0
- package/bin/cosyte-claude-card.js +23 -0
- package/bin/cosyte-internal-refs.js +23 -0
- package/bin/cosyte-no-emdash.js +18 -0
- package/bin/cosyte-phi-scan.js +26 -0
- package/bin/cosyte-readme.js +22 -0
- package/bin/script-utils.js +44 -0
- package/catalog.d.ts +110 -0
- package/catalog.js +251 -0
- package/catalog.json +210 -0
- package/check-no-emdash.sh +69 -22
- package/claude-card.d.ts +64 -0
- package/claude-card.js +296 -0
- package/internal-refs.d.ts +12 -2
- package/internal-refs.js +131 -6
- package/package.json +40 -1
- package/phi-scan.d.ts +22 -0
- package/phi-scan.js +152 -4
- package/readme.d.ts +81 -0
- package/readme.js +489 -0
package/catalog.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// @cosyte/script-utils/catalog
|
|
2
|
+
//
|
|
3
|
+
// THE CATALOG OF COSYTE'S LIBRARIES (ST13, ST14). One record per library, generated from that
|
|
4
|
+
// library's own package.json, with the tier it belongs to; and one for Pathways, our integration
|
|
5
|
+
// engine, which is in development and not on npm. The tiers and Pathways' status come from a small
|
|
6
|
+
// hand-kept file in cosyte/config (`catalog/tiers.json`); everything else is read out of each
|
|
7
|
+
// package.json by `scripts/catalog.mjs` there, and written beside this module as `catalog.json`.
|
|
8
|
+
// A site, the org profile, a deck or a knowledgebase page reads that one list instead of keeping
|
|
9
|
+
// its own roster.
|
|
10
|
+
//
|
|
11
|
+
// IT ALSO CARRIES THE CHECK EVERY LIBRARY RUNS. Each package.json description is that library's one
|
|
12
|
+
// canonical line (ST14), so the description in a library's package.json must be the catalog's.
|
|
13
|
+
// When a library changes its description, its check fails until the catalog is regenerated and a
|
|
14
|
+
// script-utils release carries it: the catalog and the package cannot quietly disagree.
|
|
15
|
+
//
|
|
16
|
+
// node node_modules/@cosyte/script-utils/catalog.js # the package in this directory
|
|
17
|
+
// node node_modules/@cosyte/script-utils/catalog.js --dir <path> # another package directory
|
|
18
|
+
//
|
|
19
|
+
// EXIT CODES, the contract every gate here keeps: 0 the description agrees, 1 it does not, 2 the
|
|
20
|
+
// check could not run (no package.json, or a package the catalog does not list).
|
|
21
|
+
//
|
|
22
|
+
// ZERO DEPENDENCIES AND NO BUILD STEP, like the rest of this package.
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { join, resolve } from "node:path";
|
|
26
|
+
|
|
27
|
+
import { isCliEntrypoint } from "./index.js";
|
|
28
|
+
|
|
29
|
+
/** The three tiers, in the order ST13 names them. */
|
|
30
|
+
export const TIERS = Object.freeze(["standard", "built-on", "engine"]);
|
|
31
|
+
|
|
32
|
+
/** Deep-freeze a parsed JSON value, so no consumer can edit the shared catalog in place. */
|
|
33
|
+
function freeze(value) {
|
|
34
|
+
if (value !== null && typeof value === "object") {
|
|
35
|
+
for (const child of Object.values(value)) freeze(child);
|
|
36
|
+
Object.freeze(value);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The catalog, as `catalog.json` beside this module carries it. */
|
|
42
|
+
export const CATALOG = freeze(
|
|
43
|
+
JSON.parse(readFileSync(new URL("./catalog.json", import.meta.url), "utf8")),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The release line a version is on: `0.<minor>` below 1.0.0, where a minor may break, and the
|
|
48
|
+
* major from 1.0.0 on. `0.1.2` is the 0.1 line (ST16).
|
|
49
|
+
*
|
|
50
|
+
* @param {string} version A `major.minor.patch` version.
|
|
51
|
+
* @returns {string} The line, for example `0.1`.
|
|
52
|
+
*/
|
|
53
|
+
export function releaseLine(version) {
|
|
54
|
+
const match = /^(\d+)\.(\d+)\.\d+(?:[-+].*)?$/.exec(String(version));
|
|
55
|
+
if (match === null) throw new TypeError(`${JSON.stringify(version)} is not a semver version.`);
|
|
56
|
+
return match[1] === "0" ? `0.${match[2]}` : match[1];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The `https://github.com/cosyte/<repo>` a package.json's repository names, or `null`. */
|
|
60
|
+
function repositoryUrl(manifest) {
|
|
61
|
+
const raw =
|
|
62
|
+
typeof manifest.repository === "string" ? manifest.repository : manifest.repository?.url;
|
|
63
|
+
if (typeof raw !== "string") return null;
|
|
64
|
+
const match = /github\.com[/:]cosyte\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/.exec(raw);
|
|
65
|
+
return match === null ? null : `https://github.com/cosyte/${match[1]}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The slug a `@cosyte/<slug>` name carries. */
|
|
69
|
+
function slugOf(name) {
|
|
70
|
+
const match = /^@cosyte\/([a-z0-9][a-z0-9.-]*)$/.exec(String(name));
|
|
71
|
+
if (match === null)
|
|
72
|
+
throw new TypeError(`${JSON.stringify(name)} is not a @cosyte/<name> package.`);
|
|
73
|
+
return match[1];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The fields every record takes from its package.json, refusing a manifest that lacks one. */
|
|
77
|
+
function fromManifest(name, manifest) {
|
|
78
|
+
if (manifest === undefined || manifest === null || typeof manifest !== "object") {
|
|
79
|
+
throw new TypeError(`no package.json was read for ${name}.`);
|
|
80
|
+
}
|
|
81
|
+
if (manifest.name !== name) {
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
`the package.json read for ${name} names ${JSON.stringify(manifest.name)}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const slug = slugOf(name);
|
|
87
|
+
const problems = [];
|
|
88
|
+
if (typeof manifest.description !== "string" || manifest.description.trim() === "") {
|
|
89
|
+
problems.push("no description");
|
|
90
|
+
}
|
|
91
|
+
if (typeof manifest.version !== "string") problems.push("no version");
|
|
92
|
+
if (typeof manifest.engines?.node !== "string") problems.push("no engines.node");
|
|
93
|
+
const repository = repositoryUrl(manifest);
|
|
94
|
+
if (repository === null) problems.push("no github.com/cosyte repository");
|
|
95
|
+
if (problems.length > 0)
|
|
96
|
+
throw new TypeError(`${name}'s package.json has ${problems.join(", ")}.`);
|
|
97
|
+
return {
|
|
98
|
+
name,
|
|
99
|
+
slug,
|
|
100
|
+
description: manifest.description,
|
|
101
|
+
line: releaseLine(manifest.version),
|
|
102
|
+
engines: { node: manifest.engines.node },
|
|
103
|
+
repository,
|
|
104
|
+
docs: `https://docs.cosyte.com/${slug}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build the catalog from the hand-kept tiers file and each package's package.json. Pure: it reads
|
|
110
|
+
* nothing, so the generator decides where the manifests come from.
|
|
111
|
+
*
|
|
112
|
+
* @param {{ standards: { package: string, standard: string }[], builtOn: string[],
|
|
113
|
+
* engine: { package: string, name: string, status: string, onNpm: boolean } }} tiers
|
|
114
|
+
* @param {Map<string, Record<string, any>> | Record<string, Record<string, any>>} manifests
|
|
115
|
+
* Each package's parsed package.json, keyed by package name.
|
|
116
|
+
* @returns {{ libraries: object[], engine: object }} The catalog.
|
|
117
|
+
* @throws {TypeError} When a tier names a package with no usable manifest, or names one twice.
|
|
118
|
+
*/
|
|
119
|
+
export function buildCatalog(tiers, manifests) {
|
|
120
|
+
const get = (name) => (manifests instanceof Map ? manifests.get(name) : manifests[name]);
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const claim = (name) => {
|
|
123
|
+
if (seen.has(name)) throw new TypeError(`${name} is listed twice in the tiers.`);
|
|
124
|
+
seen.add(name);
|
|
125
|
+
};
|
|
126
|
+
const libraries = [];
|
|
127
|
+
for (const { package: name, standard } of tiers.standards) {
|
|
128
|
+
claim(name);
|
|
129
|
+
libraries.push({
|
|
130
|
+
...fromManifest(name, get(name)),
|
|
131
|
+
npm: `https://www.npmjs.com/package/${name}`,
|
|
132
|
+
tier: "standard",
|
|
133
|
+
standard,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const name of tiers.builtOn) {
|
|
137
|
+
claim(name);
|
|
138
|
+
libraries.push({
|
|
139
|
+
...fromManifest(name, get(name)),
|
|
140
|
+
npm: `https://www.npmjs.com/package/${name}`,
|
|
141
|
+
tier: "built-on",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const { package: enginePackage, name: engineName, status, onNpm } = tiers.engine;
|
|
145
|
+
claim(enginePackage);
|
|
146
|
+
const engine = {
|
|
147
|
+
...fromManifest(enginePackage, get(enginePackage)),
|
|
148
|
+
tier: "engine",
|
|
149
|
+
displayName: engineName,
|
|
150
|
+
status,
|
|
151
|
+
onNpm,
|
|
152
|
+
};
|
|
153
|
+
return { libraries, engine };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The catalog record for a package name: a library or the engine, or `undefined`.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} name A package name, for example `@cosyte/hl7`.
|
|
160
|
+
* @param {{ libraries: object[], engine: object }} [catalog] Defaults to `CATALOG`.
|
|
161
|
+
*/
|
|
162
|
+
export function catalogEntry(name, catalog = CATALOG) {
|
|
163
|
+
if (catalog.engine?.name === name) return catalog.engine;
|
|
164
|
+
return catalog.libraries.find((entry) => entry.name === name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Compare a package.json with its catalog record. Only the description is compared: it is the line
|
|
169
|
+
* ST14 makes canonical, and the other generated fields move with a release (the version line) or
|
|
170
|
+
* cannot drift without the package moving (the name and repository).
|
|
171
|
+
*
|
|
172
|
+
* @param {Record<string, any>} manifest The package's parsed package.json.
|
|
173
|
+
* @param {{ libraries: object[], engine: object }} [catalog] Defaults to `CATALOG`.
|
|
174
|
+
* @returns {{ field: string, catalog: string, packageJson: string }[]} The disagreements.
|
|
175
|
+
* @throws {TypeError} When the catalog does not list the package.
|
|
176
|
+
*/
|
|
177
|
+
export function compareWithCatalog(manifest, catalog = CATALOG) {
|
|
178
|
+
const entry = catalogEntry(manifest?.name, catalog);
|
|
179
|
+
if (entry === undefined) {
|
|
180
|
+
throw new TypeError(`the catalog does not list ${JSON.stringify(manifest?.name ?? null)}.`);
|
|
181
|
+
}
|
|
182
|
+
if (manifest.description === entry.description) return [];
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
field: "description",
|
|
186
|
+
catalog: entry.description,
|
|
187
|
+
packageJson: String(manifest.description ?? ""),
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run the check over one package directory and return the exit code. It never exits the process.
|
|
194
|
+
*
|
|
195
|
+
* @param {{ dir?: string, argv?: string[] }} [options] `dir` is the package directory (default:
|
|
196
|
+
* the working directory). `argv` is the CLI arguments, read only for `--dir <path>`.
|
|
197
|
+
* @returns {0 | 1 | 2} 0 agree, 1 disagree, 2 the check could not run.
|
|
198
|
+
*/
|
|
199
|
+
export function runCatalogCheck(options = {}) {
|
|
200
|
+
let dir = options.dir ?? process.cwd();
|
|
201
|
+
const argv = options.argv ?? [];
|
|
202
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
203
|
+
if (argv[i] === "--dir" && typeof argv[i + 1] === "string") {
|
|
204
|
+
dir = argv[i + 1];
|
|
205
|
+
i += 1;
|
|
206
|
+
} else if (argv[i] !== "--") {
|
|
207
|
+
process.stderr.write(
|
|
208
|
+
`catalog check: ${JSON.stringify(argv[i])} is not an argument this check accepts (only ` +
|
|
209
|
+
`--dir <package directory>). Nothing was checked.\n`,
|
|
210
|
+
);
|
|
211
|
+
return 2;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
dir = resolve(dir);
|
|
215
|
+
let manifest;
|
|
216
|
+
try {
|
|
217
|
+
manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
218
|
+
} catch (err) {
|
|
219
|
+
process.stderr.write(`catalog check: could not read package.json in ${dir}: ${err.message}\n`);
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
let differences;
|
|
223
|
+
try {
|
|
224
|
+
differences = compareWithCatalog(manifest);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
process.stderr.write(`catalog check: ${err.message} Nothing was checked.\n`);
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
if (differences.length === 0) {
|
|
230
|
+
process.stdout.write(`catalog check: ${manifest.name}'s description is the catalog's.\n`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
const report = differences
|
|
234
|
+
.map(
|
|
235
|
+
(d) =>
|
|
236
|
+
` ${d.field}\n package.json: ${JSON.stringify(d.packageJson)}\n` +
|
|
237
|
+
` catalog: ${JSON.stringify(d.catalog)}`,
|
|
238
|
+
)
|
|
239
|
+
.join("\n");
|
|
240
|
+
process.stderr.write(
|
|
241
|
+
`catalog check: ${manifest.name}'s package.json and the catalog disagree.\n${report}\n` +
|
|
242
|
+
`The catalog is generated from package.json in cosyte/config (scripts/catalog.mjs) and ` +
|
|
243
|
+
`ships in @cosyte/script-utils. Change both together: regenerate the catalog, release ` +
|
|
244
|
+
`script-utils, then move this package to that release.\n`,
|
|
245
|
+
);
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (isCliEntrypoint(import.meta.url)) {
|
|
250
|
+
process.exitCode = runCatalogCheck({ argv: process.argv.slice(2) });
|
|
251
|
+
}
|
package/catalog.json
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "GENERATED by cosyte/config scripts/catalog.mjs from each package's package.json and catalog/tiers.json. Do not edit by hand: change the package.json or the tiers file, then run `node scripts/catalog.mjs write` from the umbrella.",
|
|
3
|
+
"libraries": [
|
|
4
|
+
{
|
|
5
|
+
"name": "@cosyte/hl7",
|
|
6
|
+
"slug": "hl7",
|
|
7
|
+
"description": "Developer-focused HL7 v2 toolkit for Node.js and TypeScript, message parsing and building, mutation and serialization, and one-line helpers.",
|
|
8
|
+
"line": "0.1",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22.0.0"
|
|
11
|
+
},
|
|
12
|
+
"repository": "https://github.com/cosyte/hl7",
|
|
13
|
+
"docs": "https://docs.cosyte.com/hl7",
|
|
14
|
+
"npm": "https://www.npmjs.com/package/@cosyte/hl7",
|
|
15
|
+
"tier": "standard",
|
|
16
|
+
"standard": "HL7 v2"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "@cosyte/mllp",
|
|
20
|
+
"slug": "mllp",
|
|
21
|
+
"description": "MLLP transport for HL7 v2 in Node.js and TypeScript: client and server, framing, ACK correlation, auto-reconnect, TLS, and an in-memory transport for tests.",
|
|
22
|
+
"line": "0.1",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"repository": "https://github.com/cosyte/mllp",
|
|
27
|
+
"docs": "https://docs.cosyte.com/mllp",
|
|
28
|
+
"npm": "https://www.npmjs.com/package/@cosyte/mllp",
|
|
29
|
+
"tier": "standard",
|
|
30
|
+
"standard": "MLLP"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "@cosyte/fhir",
|
|
34
|
+
"slug": "fhir",
|
|
35
|
+
"description": "FHIR R4 for Node.js and TypeScript: a resource model that keeps every value exactly as written, JSON and XML codecs, layered validation, and a safety readout for status and negation.",
|
|
36
|
+
"line": "0.1",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"repository": "https://github.com/cosyte/fhir",
|
|
41
|
+
"docs": "https://docs.cosyte.com/fhir",
|
|
42
|
+
"npm": "https://www.npmjs.com/package/@cosyte/fhir",
|
|
43
|
+
"tier": "standard",
|
|
44
|
+
"standard": "FHIR"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "@cosyte/ccda",
|
|
48
|
+
"slug": "ccda",
|
|
49
|
+
"description": "C-CDA parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.",
|
|
50
|
+
"line": "0.1",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.0.0"
|
|
53
|
+
},
|
|
54
|
+
"repository": "https://github.com/cosyte/ccda",
|
|
55
|
+
"docs": "https://docs.cosyte.com/ccda",
|
|
56
|
+
"npm": "https://www.npmjs.com/package/@cosyte/ccda",
|
|
57
|
+
"tier": "standard",
|
|
58
|
+
"standard": "C-CDA"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "@cosyte/x12",
|
|
62
|
+
"slug": "x12",
|
|
63
|
+
"description": "Developer-focused ASC X12 healthcare EDI parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.",
|
|
64
|
+
"line": "0.1",
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22.0.0"
|
|
67
|
+
},
|
|
68
|
+
"repository": "https://github.com/cosyte/x12",
|
|
69
|
+
"docs": "https://docs.cosyte.com/x12",
|
|
70
|
+
"npm": "https://www.npmjs.com/package/@cosyte/x12",
|
|
71
|
+
"tier": "standard",
|
|
72
|
+
"standard": "X12"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "@cosyte/ncpdp",
|
|
76
|
+
"slug": "ncpdp",
|
|
77
|
+
"description": "NCPDP parser, serializer, and builder for Node.js and TypeScript: SCRIPT ePrescribing and Telecom pharmacy claims, lenient on parse, spec-clean on emit.",
|
|
78
|
+
"line": "0.1",
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": ">=22.0.0"
|
|
81
|
+
},
|
|
82
|
+
"repository": "https://github.com/cosyte/ncpdp",
|
|
83
|
+
"docs": "https://docs.cosyte.com/ncpdp",
|
|
84
|
+
"npm": "https://www.npmjs.com/package/@cosyte/ncpdp",
|
|
85
|
+
"tier": "standard",
|
|
86
|
+
"standard": "NCPDP"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "@cosyte/astm",
|
|
90
|
+
"slug": "astm",
|
|
91
|
+
"description": "ASTM parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.",
|
|
92
|
+
"line": "0.1",
|
|
93
|
+
"engines": {
|
|
94
|
+
"node": ">=22.0.0"
|
|
95
|
+
},
|
|
96
|
+
"repository": "https://github.com/cosyte/astm",
|
|
97
|
+
"docs": "https://docs.cosyte.com/astm",
|
|
98
|
+
"npm": "https://www.npmjs.com/package/@cosyte/astm",
|
|
99
|
+
"tier": "standard",
|
|
100
|
+
"standard": "ASTM"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "@cosyte/dicom",
|
|
104
|
+
"slug": "dicom",
|
|
105
|
+
"description": "DICOM Part 10 parser + utility library for Node.js and TypeScript: metadata-first, vendor-quirky-tolerant, dual ESM/CJS.",
|
|
106
|
+
"line": "0.1",
|
|
107
|
+
"engines": {
|
|
108
|
+
"node": ">=22.0.0"
|
|
109
|
+
},
|
|
110
|
+
"repository": "https://github.com/cosyte/dicom",
|
|
111
|
+
"docs": "https://docs.cosyte.com/dicom",
|
|
112
|
+
"npm": "https://www.npmjs.com/package/@cosyte/dicom",
|
|
113
|
+
"tier": "standard",
|
|
114
|
+
"standard": "DICOM"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "@cosyte/terminology",
|
|
118
|
+
"slug": "terminology",
|
|
119
|
+
"description": "Terminology engine for US healthcare code systems in Node.js and TypeScript: FHIR $translate, $lookup, $validate-code and $expand over code systems, value sets and maps you supply.",
|
|
120
|
+
"line": "0.1",
|
|
121
|
+
"engines": {
|
|
122
|
+
"node": ">=22.0.0"
|
|
123
|
+
},
|
|
124
|
+
"repository": "https://github.com/cosyte/terminology",
|
|
125
|
+
"docs": "https://docs.cosyte.com/terminology",
|
|
126
|
+
"npm": "https://www.npmjs.com/package/@cosyte/terminology",
|
|
127
|
+
"tier": "built-on"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "@cosyte/transform",
|
|
131
|
+
"slug": "transform",
|
|
132
|
+
"description": "HL7 v2 → FHIR R4 transformation library for Node.js and TypeScript: IG-grounded, fail-safe, value-free diagnostics; never a confident wrong FHIR value.",
|
|
133
|
+
"line": "0.1",
|
|
134
|
+
"engines": {
|
|
135
|
+
"node": ">=22.0.0"
|
|
136
|
+
},
|
|
137
|
+
"repository": "https://github.com/cosyte/transform",
|
|
138
|
+
"docs": "https://docs.cosyte.com/transform",
|
|
139
|
+
"npm": "https://www.npmjs.com/package/@cosyte/transform",
|
|
140
|
+
"tier": "built-on"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "@cosyte/deid",
|
|
144
|
+
"slug": "deid",
|
|
145
|
+
"description": "Healthcare de-identification for Node.js and TypeScript: applies a HIPAA Safe Harbor policy to HL7 v2, C-CDA, FHIR R4, X12, NCPDP Telecom and DICOM, fails closed, and emits a value-free manifest.",
|
|
146
|
+
"line": "0.1",
|
|
147
|
+
"engines": {
|
|
148
|
+
"node": ">=22.0.0"
|
|
149
|
+
},
|
|
150
|
+
"repository": "https://github.com/cosyte/deid",
|
|
151
|
+
"docs": "https://docs.cosyte.com/deid",
|
|
152
|
+
"npm": "https://www.npmjs.com/package/@cosyte/deid",
|
|
153
|
+
"tier": "built-on"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "@cosyte/synth",
|
|
157
|
+
"slug": "synth",
|
|
158
|
+
"description": "Deterministic, seedable synthetic healthcare test fixtures for Node.js and TypeScript: HL7 v2, FHIR R4 and US Core, C-CDA, X12, NCPDP and ASTM, spec-clean by construction and never real PHI.",
|
|
159
|
+
"line": "0.1",
|
|
160
|
+
"engines": {
|
|
161
|
+
"node": ">=22.0.0"
|
|
162
|
+
},
|
|
163
|
+
"repository": "https://github.com/cosyte/synth",
|
|
164
|
+
"docs": "https://docs.cosyte.com/synth",
|
|
165
|
+
"npm": "https://www.npmjs.com/package/@cosyte/synth",
|
|
166
|
+
"tier": "built-on"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"name": "@cosyte/dates",
|
|
170
|
+
"slug": "dates",
|
|
171
|
+
"description": "Date and time utilities for the @cosyte/* healthcare parsers: precision-preserving conversion and validation that never guesses a timezone.",
|
|
172
|
+
"line": "0.1",
|
|
173
|
+
"engines": {
|
|
174
|
+
"node": ">=22.0.0"
|
|
175
|
+
},
|
|
176
|
+
"repository": "https://github.com/cosyte/dates",
|
|
177
|
+
"docs": "https://docs.cosyte.com/dates",
|
|
178
|
+
"npm": "https://www.npmjs.com/package/@cosyte/dates",
|
|
179
|
+
"tier": "built-on"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "@cosyte/cli",
|
|
183
|
+
"slug": "cli",
|
|
184
|
+
"description": "The cosyte CLI: a PHI-safe developer front door over the @cosyte/* healthcare parsers. Parse HL7 v2 / FHIR from a file or stdin to typed JSON, with a documented exit-code contract.",
|
|
185
|
+
"line": "0.1",
|
|
186
|
+
"engines": {
|
|
187
|
+
"node": ">=22.0.0 <26.0.0"
|
|
188
|
+
},
|
|
189
|
+
"repository": "https://github.com/cosyte/cli",
|
|
190
|
+
"docs": "https://docs.cosyte.com/cli",
|
|
191
|
+
"npm": "https://www.npmjs.com/package/@cosyte/cli",
|
|
192
|
+
"tier": "built-on"
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"engine": {
|
|
196
|
+
"name": "@cosyte/pathways",
|
|
197
|
+
"slug": "pathways",
|
|
198
|
+
"description": "A code-first healthcare integration engine for Node.js: channels in YAML, transforms in TypeScript. Early and not yet released.",
|
|
199
|
+
"line": "0.0",
|
|
200
|
+
"engines": {
|
|
201
|
+
"node": ">=24.0.0"
|
|
202
|
+
},
|
|
203
|
+
"repository": "https://github.com/cosyte/pathways",
|
|
204
|
+
"docs": "https://docs.cosyte.com/pathways",
|
|
205
|
+
"tier": "engine",
|
|
206
|
+
"displayName": "Pathways",
|
|
207
|
+
"status": "in development",
|
|
208
|
+
"onNpm": false
|
|
209
|
+
}
|
|
210
|
+
}
|
package/check-no-emdash.sh
CHANGED
|
@@ -228,19 +228,32 @@ unset -f grep xargs sed awk 2>/dev/null || true
|
|
|
228
228
|
# refuse_if_incomplete reads. That branch only refines wording, never the red.
|
|
229
229
|
export LC_ALL=C.UTF-8
|
|
230
230
|
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
# entities
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
231
|
+
# What counts as a dash, for U+2014 and U+2013 alike. Case never matters, so `&MDASH;`
|
|
232
|
+
# and `%e2%80%94` count too (D56: the forms the marketing and consulting scanner caught,
|
|
233
|
+
# which this gate now matches so those repos lose nothing by moving to it):
|
|
234
|
+
#
|
|
235
|
+
# the characters U+2014 U+2013
|
|
236
|
+
# HTML named entities, ; optional — –
|
|
237
|
+
# HTML decimal references — – leading zeros and a missing ; too
|
|
238
|
+
# HTML hex references — – leading zeros and a missing ; too
|
|
239
|
+
# URL encoding of the UTF-8 bytes %E2%80%94 %E2%80%93
|
|
240
|
+
# backslash-u escapes (JS, JSON, Java, \u2014 \u{2014} \U00002014
|
|
241
|
+
# C#, Python, Rust, Swift)
|
|
242
|
+
# backslash-x escapes (PCRE, Perl) \x{2014}
|
|
243
|
+
# CSS escapes \2014 \002014
|
|
244
|
+
# Unicode names (Python, Perl) \N{EM DASH} \N{EN DASH}
|
|
245
|
+
#
|
|
246
|
+
# The lookaheads keep the near misses out: 𔃈 and the CSS escape \20140 name other
|
|
247
|
+
# characters, and &mdashboard is not an entity. The escape forms are not theoretical: the
|
|
248
|
+
# parser-template manifest once carried a backslash-u em dash, and a literal-only sweep
|
|
249
|
+
# passed straight over it.
|
|
250
|
+
#
|
|
251
|
+
# A consumer may narrow WHAT IS SCANNED through the declaration above; nothing may narrow
|
|
252
|
+
# WHAT IS MATCHED, and widening it is a change every consuming repo inherits at once. The
|
|
253
|
+
# en dash's forms (operator decision, 2026-11-07) and these (D56) were such changes.
|
|
254
|
+
PATTERN='(?i)\x{2014}|\x{2013}|&[mn]dash(?![a-z])|�*821[12](?![0-9])|�*201[34](?![0-9a-f])'
|
|
255
|
+
PATTERN+='|%E2%80%9[34]|\\u(?:0000)?201[34]|\\u\{0*201[34]\}|\\x\{0*201[34]\}'
|
|
256
|
+
PATTERN+='|\\0{0,2}201[34](?![0-9a-f])|\\N\{E[MN] DASH\}'
|
|
244
257
|
|
|
245
258
|
# The declaration's path, relative to the top level of the repo under scan. Deliberately
|
|
246
259
|
# NOT relative to this file: in a consuming repo this file lives under `node_modules/`
|
|
@@ -281,10 +294,11 @@ SCAN_MODE=files
|
|
|
281
294
|
|
|
282
295
|
refuse_arguments() {
|
|
283
296
|
echo "ERROR: check-no-emdash - $1" >&2
|
|
284
|
-
echo " The modes are: (no argument) to scan every tracked file
|
|
285
|
-
echo " to scan the text arriving on stdin; --list-scanned to write the
|
|
286
|
-
echo " default scan reads; --self-id to write the sha256 of the
|
|
287
|
-
echo " that is executing. Refusing to run rather
|
|
297
|
+
echo " The modes are: (no argument) to scan every tracked file and its name;" >&2
|
|
298
|
+
echo " --stdin LABEL to scan the text arriving on stdin; --list-scanned to write the" >&2
|
|
299
|
+
echo " paths the default scan reads; --self-id to write the sha256 of the" >&2
|
|
300
|
+
echo " implementation that is executing; --help for this. Refusing to run rather" >&2
|
|
301
|
+
echo " than guess which was meant." >&2
|
|
288
302
|
exit 1
|
|
289
303
|
}
|
|
290
304
|
|
|
@@ -324,6 +338,22 @@ case "${1:-}" in
|
|
|
324
338
|
refuse_arguments "--self-id takes no further argument, and this was given: ${2}"
|
|
325
339
|
fi
|
|
326
340
|
;;
|
|
341
|
+
--help | -h)
|
|
342
|
+
cat << 'USAGE'
|
|
343
|
+
check-no-emdash: refuse the em dash (U+2014) and the en dash (U+2013), as the characters and
|
|
344
|
+
in every encoded form, in the tracked files of the repository it runs in and in their names.
|
|
345
|
+
|
|
346
|
+
check-no-emdash.sh scan every tracked file and every tracked file name
|
|
347
|
+
check-no-emdash.sh --stdin LABEL scan the text on stdin (a PR title, body, commit message)
|
|
348
|
+
check-no-emdash.sh --list-scanned write the NUL-separated paths the default scan reads
|
|
349
|
+
check-no-emdash.sh --self-id write the sha256 of the implementation that is executing
|
|
350
|
+
|
|
351
|
+
A repository leaves paths out in scripts/check-no-emdash.exclude: one per line, an entry ending
|
|
352
|
+
in / covers everything beneath it, # starts a comment. Exit 0 means the mode completed and found
|
|
353
|
+
nothing banned; exit 1 means anything else.
|
|
354
|
+
USAGE
|
|
355
|
+
exit 0
|
|
356
|
+
;;
|
|
327
357
|
*)
|
|
328
358
|
refuse_arguments "unrecognised argument: ${1}"
|
|
329
359
|
;;
|
|
@@ -341,6 +371,7 @@ GITERR=$(mktemp)
|
|
|
341
371
|
FILELIST=$(mktemp)
|
|
342
372
|
SCANLIST=$(mktemp)
|
|
343
373
|
PLAINLIST=$(mktemp)
|
|
374
|
+
NAMELIST=$(mktemp)
|
|
344
375
|
STDINBUF=$(mktemp)
|
|
345
376
|
BINPROBE=$(mktemp)
|
|
346
377
|
PROBEERR=$(mktemp)
|
|
@@ -353,8 +384,8 @@ PROBEERR=$(mktemp)
|
|
|
353
384
|
# not lost with it.
|
|
354
385
|
close_exit_vocabulary() {
|
|
355
386
|
local code=$?
|
|
356
|
-
rm -f "$ERRLOG" "$GITERR" "$FILELIST" "$SCANLIST" "$PLAINLIST" "$
|
|
357
|
-
"$PROBEERR"
|
|
387
|
+
rm -f "$ERRLOG" "$GITERR" "$FILELIST" "$SCANLIST" "$PLAINLIST" "$NAMELIST" "$STDINBUF" \
|
|
388
|
+
"$BINPROBE" "$PROBEERR"
|
|
358
389
|
if [ "$code" -ne 0 ] && [ "$code" -ne 1 ]; then
|
|
359
390
|
echo "ERROR: check-no-emdash - the run ended with status ${code}. This gate reports 0" >&2
|
|
360
391
|
echo " for a mode that completed clean and 1 for everything else, so it is" >&2
|
|
@@ -816,6 +847,7 @@ esac
|
|
|
816
847
|
|
|
817
848
|
: > "$SCANLIST"
|
|
818
849
|
: > "$PLAINLIST"
|
|
850
|
+
: > "$NAMELIST"
|
|
819
851
|
gitlinks=0
|
|
820
852
|
scanned=0
|
|
821
853
|
self_excluded=0
|
|
@@ -831,11 +863,16 @@ for i in "${!TRACKED[@]}"; do
|
|
|
831
863
|
|
|
832
864
|
# What the repo declared it leaves out. Checked before the entry is opened, because an
|
|
833
865
|
# excluded path is not read and its type on disk is therefore not this gate's business.
|
|
866
|
+
# It is not scanned at all, so its NAME is left out with its content.
|
|
834
867
|
if is_excluded "$f"; then
|
|
835
868
|
declared_excluded=$((declared_excluded + 1))
|
|
836
869
|
continue
|
|
837
870
|
fi
|
|
838
871
|
|
|
872
|
+
# A NAME IS COPY TOO (D56): every tracked path the declaration does not leave out has its
|
|
873
|
+
# name scanned, a gitlink's included, before any test of what is on disk.
|
|
874
|
+
printf '%s\0' "$f" >> "$NAMELIST"
|
|
875
|
+
|
|
839
876
|
# `git ls-files` lists a submodule as a gitlink, which on disk is a REAL directory.
|
|
840
877
|
# config has none today (checked: `git ls-files -s` lists no mode 160000 entry). The
|
|
841
878
|
# skip is keyed on the INDEX MODE and never on the type on disk, because the two
|
|
@@ -886,12 +923,22 @@ if [ "$MODE" = list ]; then
|
|
|
886
923
|
exit 0
|
|
887
924
|
fi
|
|
888
925
|
|
|
926
|
+
# File names first: NUL-separated in, one per line out. `-a` because a name is text whatever
|
|
927
|
+
# bytes it holds, and the stderr capture refuses a name scan that could not complete.
|
|
928
|
+
NAME_HITS=$(grep -z -a -P -e "$PATTERN" < "$NAMELIST" 2>> "$ERRLOG" | tr '\0' '\n' || true)
|
|
929
|
+
refuse_if_incomplete
|
|
930
|
+
|
|
889
931
|
HITS=$(xargs -0 -r grep -H -nP -e "$PATTERN" -- < "$SCANLIST" 2>> "$ERRLOG" || true)
|
|
890
932
|
|
|
891
933
|
refuse_if_incomplete
|
|
892
934
|
|
|
935
|
+
if [ -n "$NAME_HITS" ] && [ -n "$HITS" ]; then
|
|
936
|
+
fail_with_hits "the tracked file names and the tracked files listed above" \
|
|
937
|
+
"$NAME_HITS"$'\n'"$HITS"
|
|
938
|
+
fi
|
|
939
|
+
[ -n "$NAME_HITS" ] && fail_with_hits "the tracked file names listed above" "$NAME_HITS"
|
|
893
940
|
[ -n "$HITS" ] && fail_with_hits "the tracked files listed above" "$HITS"
|
|
894
941
|
|
|
895
|
-
echo "check-no-emdash: OK (no em or en dashes in ${scanned} of ${#TRACKED[@]} tracked file(s)
|
|
896
|
-
"${gitlinks} gitlink(s) skipped, ${self_excluded} excluded as the
|
|
897
|
-
"scan, ${declared_excluded} excluded by ${DECLARATION})"
|
|
942
|
+
echo "check-no-emdash: OK (no em or en dashes in ${scanned} of ${#TRACKED[@]} tracked file(s)" \
|
|
943
|
+
"or their names; ${gitlinks} gitlink(s) skipped, ${self_excluded} excluded as the" \
|
|
944
|
+
"implementation under scan, ${declared_excluded} excluded by ${DECLARATION})"
|