@pipeworx/mcp-guam-code 0.1.1 → 0.1.3

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Guam Code Annotated (GCA) — Guam territorial statutes, by citation and by
4
4
  topic/keyword search. Keyless.
5
5
 
6
- Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1784+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
6
+ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1819+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
7
7
 
8
8
  ## Tools
9
9
 
@@ -224,7 +224,7 @@ directly, instead of just this one's:
224
224
  }
225
225
  ```
226
226
 
227
- Both URLs reach the same gateway and the same 1784+ data sources. The
227
+ Both URLs reach the same gateway and the same 1819+ data sources. The
228
228
  only difference is which pack's tools are listed **directly**; `ask_pipeworx`
229
229
  reaches all of them from either one.
230
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipeworx/mcp-guam-code",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Guam Code Annotated (GCA) — section lookup and search. Fleet #2888.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -26,7 +26,7 @@
26
26
  "@cloudflare/workers-types": "^4.20260405.1"
27
27
  },
28
28
  "pipeworx": {
29
- "sourceHash": "v1-964fe40aed9923b374141088f42860d68ffb5efa97b3c7c3fc37b9902c46c79f",
30
- "sourceCommit": "3723e46c6d115bd4a1fd2175e6bf758555e7cb50"
29
+ "sourceHash": "v1-e101ffcf2c7ba1d5bce6f1a3825e124694dc1e346ebf10ef701f4239c45c5202",
30
+ "sourceCommit": "a03f96ed672b8c149593c64ecbf30280cabf084c"
31
31
  }
32
32
  }
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.pipeworx-io/guam-code",
4
4
  "title": "Guam Code",
5
5
  "description": "Guam Code Annotated (GCA) — section lookup and search. Fleet #2888.",
6
- "version": "0.1.1",
6
+ "version": "0.1.3",
7
7
  "websiteUrl": "https://pipeworx.io/packs/guam-code",
8
8
  "repository": {
9
9
  "url": "https://github.com/pipeworx-io/mcp-guam-code",
package/src/index.ts CHANGED
@@ -935,6 +935,48 @@ function unescapePdfLiteral(s: string): string {
935
935
  return out;
936
936
  }
937
937
 
938
+ /**
939
+ * Recover the ORIGINAL raw byte (0-255) that `new TextDecoder('latin1')`
940
+ * produced a given codepoint for. This exists because the WHATWG Encoding
941
+ * Standard makes "latin1" an ALIAS for "windows-1252", not true ISO-8859-1 —
942
+ * every browser and Node's TextDecoder follow that spec. Bytes 0x00-0x7F and
943
+ * 0xA0-0xFF decode as identity either way, but bytes 0x80-0x9F come back as
944
+ * windows-1252's typographic symbols (0x99 -> U+2122 "™", 0x87 -> U+2021
945
+ * "‡", …) instead of themselves — so `charCodeAt(i) & 0xff`, used everywhere
946
+ * else in this file to recover a raw byte from a decoded string, silently
947
+ * returns the WRONG byte for any of those 32 codepoints (U+2122 & 0xff is
948
+ * 0x22, not the real 0x99).
949
+ *
950
+ * This only matters where the decoded character is being used as DATA (a
951
+ * CID half-byte, a `mapped1` font's 1-byte code) rather than as the actual
952
+ * text it renders (a true WinAnsi-encoded character, where the windows-1252
953
+ * reading IS the correct glyph — see `fontDecoderFor`'s `'winansi'` kind,
954
+ * which returns the decoded string unchanged on purpose and must NOT be
955
+ * routed through this function). Found on mainland China's NPC/MOJ law PDFs
956
+ * (cn-law, fleet #3136): Founder PDFGenerator (方正) writes CJK body text as
957
+ * LITERAL strings under a Type0/Identity-H CID font, and roughly an eighth
958
+ * of all 2-byte CIDs have at least one half-byte landing in 0x80-0x9F —
959
+ * `合` (U+5408, CID 0x0F87) was coming back as `骨` (U+9AA8) because its low
960
+ * byte 0x87 decoded to U+2021, and `U+2021 & 0xff` (0x21) is a different,
961
+ * also-valid CID that happens to mean `骨` in the same font.
962
+ */
963
+ function rawByteFromLatin1Char(code: number): number {
964
+ const b = WIN1252_HIGH_TO_BYTE.get(code);
965
+ return b !== undefined ? b : code & 0xff;
966
+ }
967
+ // windows-1252's 0x80-0x9F table (the WHATWG "windows-1252" index), inverted
968
+ // codepoint -> original byte. 0x81/0x8D/0x8F/0x90/0x9D are undefined in that
969
+ // table and decode as themselves (identity), so they need no entry here.
970
+ const WIN1252_HIGH_TO_BYTE = new Map<number, number>([
971
+ [0x20ac, 0x80], [0x201a, 0x82], [0x0192, 0x83], [0x201e, 0x84],
972
+ [0x2026, 0x85], [0x2020, 0x86], [0x2021, 0x87], [0x02c6, 0x88],
973
+ [0x2030, 0x89], [0x0160, 0x8a], [0x2039, 0x8b], [0x0152, 0x8c],
974
+ [0x017d, 0x8e], [0x2018, 0x91], [0x2019, 0x92], [0x201c, 0x93],
975
+ [0x201d, 0x94], [0x2022, 0x95], [0x2013, 0x96], [0x2014, 0x97],
976
+ [0x02dc, 0x98], [0x2122, 0x99], [0x0161, 0x9a], [0x203a, 0x9b],
977
+ [0x0153, 0x9c], [0x017e, 0x9e], [0x0178, 0x9f],
978
+ ]);
979
+
938
980
  interface RawObject {
939
981
  dict: string;
940
982
  streamBytes: Uint8Array | null;
@@ -1283,7 +1325,18 @@ async function extractPdfText(buf: ArrayBuffer, options?: PdfExtractOptions): Pr
1283
1325
 
1284
1326
  const TOKEN_RE =
1285
1327
  /\/(\S+)\s+[\d.]+\s+Tf|\[((?:\\.|[^\]])*)\]\s*TJ|<([0-9A-Fa-f\s]*)>\s*Tj|\(((?:\\.|[^()])*)\)\s*Tj|1\s+0\s+0\s+1\s+[\d.-]+\s+([\d.-]+)\s+Tm/g;
1286
- const TJ_PIECE_RE = /\(((?:\\.|[^()])*)\)|<([0-9A-Fa-f\s]*)>/g;
1328
+ // Third alternative (bare number) added for Indonesia's JDIH statute PDFs
1329
+ // (jdih.setneg.go.id, fleet #3121): that producer never writes a literal
1330
+ // space byte between words — "REPUBLIK INDONESIA" is encoded as one TJ run
1331
+ // `[(…K)-269.0(I)…]`, where the -269 IS the word gap, not kerning. Every
1332
+ // sampled font's intra-word kerning tops out around +-30 (observed: 2.0,
1333
+ // -2.9, -14.0, -24.0); the real word gaps in the same document measured
1334
+ // -269, -286, -290, -692, -1396, -1400 — two clusters with nothing between
1335
+ // them, so a flat threshold cleanly separates "kerning" from "space" without
1336
+ // needing the current font size (the TJ number is already in thousandths of
1337
+ // text space, before the Tfs/Th scale-up ISO 32000 §9.4.3 applies).
1338
+ const TJ_PIECE_RE = /\(((?:\\.|[^()])*)\)|<([0-9A-Fa-f\s]*)>|(-?\d+(?:\.\d+)?)/g;
1339
+ const TJ_WORD_GAP_THRESHOLD = -100;
1287
1340
 
1288
1341
  function decodeRun(dec: FontDecoder, literal: string | undefined, hex: string | undefined): string {
1289
1342
  if (literal !== undefined) {
@@ -1298,10 +1351,38 @@ async function extractPdfText(buf: ArrayBuffer, options?: PdfExtractOptions): Pr
1298
1351
  // font's /ToUnicode map entirely and produces line-noise control
1299
1352
  // characters instead of the real character -- route it through the
1300
1353
  // SAME decoder a hex run below already uses.
1354
+ //
1355
+ // 'cid' (Type0/Identity-H) is a 2-BYTE code space, same as the hex-
1356
+ // string path below -- a literal string's BYTES must be paired up
1357
+ // big-endian into one CID per pair, not looked up one byte at a time.
1358
+ // Found on mainland China's NPC/MOJ law PDFs (cn-law, fleet #3136):
1359
+ // Founder PDFGenerator (方正), the producer behind flk.npc.gov.cn's
1360
+ // and xzfg.moj.gov.cn's statute PDFs, writes most CJK body text as
1361
+ // literal strings like `(\000I)Tj` under a Type0 CID font rather than
1362
+ // as hex strings -- the shape this branch was missing. Decoding each
1363
+ // byte separately (the pre-fix behavior) looks up CID 0x00 and CID
1364
+ // 0x49 independently instead of the real CID 0x0049, so virtually
1365
+ // every CJK glyph misses its ToUnicode entry and comes back as
1366
+ // U+FFFD -- silently: no warning, a non-empty result, just wrong.
1367
+ // Measured against 中华人民共和国个人信息保护法 (PIPL)'s official
1368
+ // gazette PDF: 18,929 of 19,858 extracted characters were U+FFFD
1369
+ // before this fix (95%), 0 after, and `第十三条` (Article 13) and its
1370
+ // neighbors read correctly. If the literal string has an odd byte
1371
+ // count (shouldn't happen for a well-formed 2-byte-code run), the
1372
+ // trailing lone byte is silently dropped rather than guessed at.
1301
1373
  if (dec.kind === 'winansi') return unescaped;
1374
+ if (dec.kind === 'cid') {
1375
+ let out = '';
1376
+ for (let i = 0; i + 2 <= unescaped.length; i += 2) {
1377
+ const code =
1378
+ (rawByteFromLatin1Char(unescaped.charCodeAt(i)) << 8) | rawByteFromLatin1Char(unescaped.charCodeAt(i + 1));
1379
+ out += String.fromCodePoint(dec.map.get(code) ?? 0xfffd);
1380
+ }
1381
+ return out;
1382
+ }
1302
1383
  let out = '';
1303
1384
  for (let i = 0; i < unescaped.length; i++) {
1304
- const code = unescaped.charCodeAt(i) & 0xff;
1385
+ const code = rawByteFromLatin1Char(unescaped.charCodeAt(i));
1305
1386
  out += String.fromCodePoint(dec.map.get(code) ?? 0xfffd);
1306
1387
  }
1307
1388
  return out;
@@ -1380,7 +1461,17 @@ async function extractPdfText(buf: ArrayBuffer, options?: PdfExtractOptions): Pr
1380
1461
  }
1381
1462
  const dec = (currentFont && fontMap.get(currentFont)) || { kind: 'winansi' as const };
1382
1463
  if (m[2] !== undefined) {
1383
- for (const piece of m[2].matchAll(TJ_PIECE_RE)) out += decodeRun(dec, piece[1], piece[2]);
1464
+ for (const piece of m[2].matchAll(TJ_PIECE_RE)) {
1465
+ if (piece[3] !== undefined) {
1466
+ // A big negative adjustment between two TJ pieces with no
1467
+ // literal space byte at all — see TJ_PIECE_RE's comment above.
1468
+ // Skip if `out` already ends in whitespace so a producer that
1469
+ // DOES write literal spaces never gets a doubled one.
1470
+ if (parseFloat(piece[3]) <= TJ_WORD_GAP_THRESHOLD && !/\s$/.test(out)) out += ' ';
1471
+ continue;
1472
+ }
1473
+ out += decodeRun(dec, piece[1], piece[2]);
1474
+ }
1384
1475
  } else if (m[3] !== undefined) {
1385
1476
  out += decodeRun(dec, undefined, m[3]);
1386
1477
  } else if (m[4] !== undefined) {
package/src/server.ts CHANGED
@@ -11,7 +11,7 @@ import * as bakedIndexModule from './gu-baked-data.js';
11
11
  const bakedIndex = Object.fromEntries(Object.entries(bakedIndexModule));
12
12
 
13
13
  const server = new Server(
14
- { name: '@pipeworx/mcp-guam-code', version: '0.1.1' },
14
+ { name: '@pipeworx/mcp-guam-code', version: '0.1.3' },
15
15
  { capabilities: { tools: {} } },
16
16
  );
17
17