@firela/hk-hsbc-credit 0.1.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.
Binary file
package/index-node.cjs ADDED
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+ // _loader-template-node.cjs
3
+ //
4
+ // Phase 74 npm-package Node loader TEMPLATE. At package-assembly time,
5
+ // scripts/assemble-npm-packages.sh sed-substitutes hk_hsbc_credit.opt.wasm with the
6
+ // crate's underscored .opt.wasm name (e.g. eu_deutsche_bank.opt.wasm) and
7
+ // writes the result as `index-node.cjs` into dist/@firela/<name>/. One
8
+ // template serves all 7 publishable packages.
9
+ //
10
+ // This is a rename + templating of the PROVEN loaders/node.js FFI cycle
11
+ // (26 MB/s flat on Node V8 — the run that isolated the wasmi 0.31 superlinear
12
+ // root cause). DO NOT add new behavior here; if the cycle needs to change,
13
+ // change loaders/node.js first and re-template.
14
+ //
15
+ // Contract (docs/WASM_BOUNDARY.md §4): every parser .wasm exports
16
+ // alloc(size)->ptr, parse(inputPtr,inputLen)->u64, detect(inputPtr,inputLen)->u8,
17
+ // free_result(ptr,len), + linear `memory`, with ZERO imports.
18
+ // `WebAssembly.instantiate(bytes, {})` — the empty import object encodes that.
19
+ //
20
+ // FFI cycle (identical to the Worker branch — only the module-load differs):
21
+ // alloc(inputLen) → inputPtr; on 0 return ProcessFailure INTERNAL OOM
22
+ // RE-FETCH view: new Uint8Array(ex.memory.buffer).set(input, inputPtr)
23
+ // (a grow inside parse/alloc invalidates any cached ArrayBuffer —
24
+ // RESEARCH Pitfall 3; must re-fetch every cycle)
25
+ // ex.parse(inputPtr, inputLen) → BigInt packed; trap caught → ProcessFailure
26
+ // INTERNAL, free input best-effort, do NOT rethrow
27
+ // ptr = Number(packed & 0xffffffffn); len = Number((packed >> 32n) & 0xffffffffn)
28
+ // decode JSON from memory.buffer slice [ptr, ptr+len]
29
+ // ex.free_result(inputPtr, inputLen); ex.free_result(ptr, len) — BOTH buffers
30
+ //
31
+ // CF Worker BRANCHES ONLY at module-load (workerd blocks instantiate(bytes));
32
+ // see _loader-template-worker.js. After instantiation the FFI cycle is identical.
33
+
34
+ const fs = require('fs');
35
+ const path = require('path');
36
+
37
+ const REQUIRED_EXPORTS = ['alloc', 'parse', 'detect', 'free_result', 'memory'];
38
+
39
+ /// Build a ProcessFailure-shaped object matching the sdk-core wire envelope
40
+ /// `{success:false, errors:[{code, message}]}`. Used when the HOST catches a
41
+ /// trap / OOM (the parser's own failures arrive as normal ProcessFailure JSON).
42
+ function processFailure(code, message) {
43
+ return { success: false, errors: [{ code, message }] };
44
+ }
45
+
46
+ // The wasm bytes are read ONCE at module load (not per-call) — Node caches the
47
+ // file read, and the bytes are immutable. WebAssembly instantiation is deferred
48
+ // to first use (lazy _exports cache below) so the module is cheap to require.
49
+ const wasmBytes = fs.readFileSync(path.join(__dirname, 'hk_hsbc_credit.opt.wasm'));
50
+
51
+ let _exports;
52
+
53
+ /// Lazily instantiate the wasm module ONCE. Cached so the cold-start cost is
54
+ /// paid only on the first parse/detect call. Returns the exports object.
55
+ async function getExports() {
56
+ if (!_exports) {
57
+ const { instance } = await WebAssembly.instantiate(wasmBytes, {}); // {} = zero imports
58
+ _exports = instance.exports;
59
+ }
60
+ return _exports;
61
+ }
62
+
63
+ function makeParse(ex) {
64
+ return async function parse(bytes) {
65
+ const input = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
66
+ const inputLen = input.length;
67
+
68
+ const inputPtr = ex.alloc(inputLen);
69
+ if (inputPtr === 0) {
70
+ // OOM: nothing was allocated, nothing to free.
71
+ return processFailure('INTERNAL', 'alloc returned 0 (OOM) for ' + inputLen + ' bytes');
72
+ }
73
+ // Re-fetch the view EVERY cycle: a grow inside parse/alloc invalidates
74
+ // any cached ArrayBuffer over ex.memory.buffer (RESEARCH Pitfall 3).
75
+ new Uint8Array(ex.memory.buffer).set(input, inputPtr);
76
+
77
+ let packed;
78
+ try {
79
+ packed = ex.parse(inputPtr, inputLen); // arrives as BigInt (i64)
80
+ } catch (e) {
81
+ // WASM trap (RuntimeError): free the input buffer, surface as
82
+ // ProcessFailure — do NOT rethrow. Instance stays reusable.
83
+ try {
84
+ ex.free_result(inputPtr, inputLen);
85
+ } catch {
86
+ /* allocator already torn down — best-effort free */
87
+ }
88
+ return processFailure(
89
+ 'INTERNAL',
90
+ 'wasm trap: ' + ((e && e.message) || String(e))
91
+ );
92
+ }
93
+
94
+ const ptr = Number(packed & 0xffffffffn); // low 32 = result ptr
95
+ const len = Number((packed >> 32n) & 0xffffffffn); // high 32 = result len
96
+ if (ptr === 0 || len === 0) {
97
+ try {
98
+ ex.free_result(inputPtr, inputLen);
99
+ } catch {
100
+ /* best-effort */
101
+ }
102
+ return processFailure('INTERNAL', 'parse returned empty result');
103
+ }
104
+
105
+ const resultBytes = Buffer.from(new Uint8Array(ex.memory.buffer, ptr, len));
106
+ const json = JSON.parse(resultBytes.toString('utf8'));
107
+
108
+ // Free BOTH buffers every cycle (input + result) per the FFI contract.
109
+ ex.free_result(inputPtr, inputLen);
110
+ ex.free_result(ptr, len);
111
+ return json;
112
+ };
113
+ }
114
+
115
+ function makeDetect(ex) {
116
+ return async function detect(bytes) {
117
+ const input = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
118
+ const inputLen = input.length;
119
+ const inputPtr = ex.alloc(inputLen);
120
+ if (inputPtr === 0) return 0;
121
+ new Uint8Array(ex.memory.buffer).set(input, inputPtr);
122
+ let conf;
123
+ try {
124
+ conf = ex.detect(inputPtr, inputLen); // u8 confidence (0/50/100)
125
+ } catch (e) {
126
+ try {
127
+ ex.free_result(inputPtr, inputLen);
128
+ } catch {
129
+ /* best-effort */
130
+ }
131
+ return 0; // a trap in detect → treat as "no match"
132
+ }
133
+ ex.free_result(inputPtr, inputLen);
134
+ return conf;
135
+ };
136
+ }
137
+
138
+ /// Parse CSV/JSON bytes into structured JSON. Returns the parser's own envelope
139
+ /// (ProcessSuccess `{success:true, data, warnings?}` or ProcessFailure). A WASM
140
+ /// trap during parse() is caught and surfaced as ProcessFailure — the host
141
+ /// never crashes and the instance stays reusable (RUNT-02).
142
+ async function parse(bytes) {
143
+ const ex = await getExports();
144
+ const missing = REQUIRED_EXPORTS.filter((name) => !ex[name]);
145
+ if (missing.length) {
146
+ throw new Error(
147
+ 'createParser: missing required WASM exports: ' + missing.join(', ')
148
+ );
149
+ }
150
+ return makeParse(ex)(bytes);
151
+ }
152
+
153
+ /// Detection confidence (0/50/100) for bytes.
154
+ async function detect(bytes) {
155
+ const ex = await getExports();
156
+ return makeDetect(ex)(bytes);
157
+ }
158
+
159
+ module.exports = { parse, detect };
@@ -0,0 +1,134 @@
1
+ // _loader-template-worker.js
2
+ //
3
+ // Phase 74 npm-package CF Worker loader TEMPLATE. At package-assembly time,
4
+ // scripts/assemble-npm-packages.sh sed-substitutes hk_hsbc_credit.opt.wasm with the
5
+ // crate's underscored .opt.wasm name (e.g. eu_deutsche_bank.opt.wasm) and
6
+ // writes the result as `index-worker.js` into dist/@firela/<name>/. One
7
+ // template serves all 7 publishable packages.
8
+ //
9
+ // HARD CONSTRAINT (Cloudflare Workers runtime): dynamic WASM *compilation* is
10
+ // BLOCKED (like eval). A parser .wasm MUST be loaded as a Wrangler-bundled,
11
+ // pre-compiled ES-module Module:
12
+ //
13
+ // import mod from "./eu_deutsche_bank.wasm"; // mod is a WebAssembly.Module
14
+ //
15
+ // ...NOT `fs.readFileSync(...)` + `WebAssembly.instantiate(bytes)`. There is no
16
+ // Node `fs` here and `WebAssembly.instantiate(bytes)` is disallowed at runtime.
17
+ // (https://developers.cloudflare.com/workers/runtime-apis/webassembly/)
18
+ //
19
+ // FFI contract (docs/WASM_BOUNDARY.md §4): every parser .wasm exports
20
+ // alloc(size)->ptr, parse(inputPtr,inputLen)->u64, detect(inputPtr,inputLen)->u8,
21
+ // free_result(ptr,len), + linear `memory`, with ZERO imports. The empty import
22
+ // object `{}` below encodes that.
23
+ //
24
+ // This is the ES-import-constrained variant of the Node loader's FFI cycle
25
+ // (loaders/node.js). The alloc → re-fetch view → set → parse (trap-safe)
26
+ // → unpack packed ptr/len → decode → JSON → free BOTH buffers cycle is IDENTICAL
27
+ // to the Node branch; only the WASM entry differs (pre-compiled Module vs raw
28
+ // bytes). The branch is ONLY at module-load.
29
+
30
+ import mod from "./hk_hsbc_credit.opt.wasm";
31
+
32
+ const REQUIRED_EXPORTS = ['alloc', 'parse', 'detect', 'free_result', 'memory'];
33
+
34
+ // Instantiate ONCE, lazily on first request (per CF official guidance — top-level
35
+ // instantiation; the result is cached so the cold-start cost is paid once). `mod`
36
+ // is a pre-compiled WebAssembly.Module, so instantiate() returns an Instance
37
+ // (unlike the bytes overload, which returns {module, instance}) — handle both.
38
+ let _exports;
39
+ async function getExports() {
40
+ if (!_exports) {
41
+ const result = await WebAssembly.instantiate(mod, {}); // {} = zero imports
42
+ const instance = result.instance || result; // bytes→{instance}, Module→Instance
43
+ _exports = instance.exports;
44
+ }
45
+ return _exports;
46
+ }
47
+
48
+ function processFailure(code, message) {
49
+ return { success: false, errors: [{ code, message }] };
50
+ }
51
+
52
+ /// Parse CSV/JSON bytes into structured JSON. Returns the parser's own envelope
53
+ /// (ProcessSuccess `{success:true, data, warnings?}` or ProcessFailure). A WASM
54
+ /// trap during parse() is caught and surfaced as ProcessFailure — the Worker
55
+ /// never crashes and the instance stays reusable (RUNT-02 CF half).
56
+ export async function parse(bytes) {
57
+ const ex = await getExports();
58
+ const input = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
59
+ const inputLen = input.length;
60
+
61
+ const inputPtr = ex.alloc(inputLen);
62
+ if (inputPtr === 0) {
63
+ // OOM: nothing allocated, nothing to free.
64
+ return processFailure(
65
+ 'INTERNAL',
66
+ 'alloc returned 0 (OOM) for ' + inputLen + ' bytes'
67
+ );
68
+ }
69
+ // Re-fetch the view EVERY cycle: a grow inside parse/alloc invalidates any
70
+ // cached ArrayBuffer over ex.memory.buffer (stale-view hazard).
71
+ new Uint8Array(ex.memory.buffer).set(input, inputPtr);
72
+
73
+ let packed;
74
+ try {
75
+ packed = ex.parse(inputPtr, inputLen); // arrives as BigInt (i64)
76
+ } catch (e) {
77
+ // WASM trap (RuntimeError): free the input buffer, surface as
78
+ // ProcessFailure — do NOT rethrow. Instance stays reusable.
79
+ try {
80
+ ex.free_result(inputPtr, inputLen);
81
+ } catch {
82
+ /* allocator already torn down — best-effort free */
83
+ }
84
+ return processFailure(
85
+ 'INTERNAL',
86
+ 'wasm trap: ' + ((e && e.message) || String(e))
87
+ );
88
+ }
89
+
90
+ const ptr = Number(packed & 0xffffffffn); // low 32 = result ptr
91
+ const len = Number((packed >> 32n) & 0xffffffffn); // high 32 = result len
92
+ if (ptr === 0 || len === 0) {
93
+ try {
94
+ ex.free_result(inputPtr, inputLen);
95
+ } catch {
96
+ /* best-effort */
97
+ }
98
+ return processFailure('INTERNAL', 'parse returned empty result');
99
+ }
100
+
101
+ const resultBytes = new Uint8Array(ex.memory.buffer, ptr, len);
102
+ const json = JSON.parse(new TextDecoder().decode(resultBytes));
103
+
104
+ // Free BOTH buffers every cycle (input + result) per the FFI contract.
105
+ ex.free_result(inputPtr, inputLen);
106
+ ex.free_result(ptr, len);
107
+ return json;
108
+ }
109
+
110
+ /// Detection confidence (0/50/100) for bytes.
111
+ export async function detect(bytes) {
112
+ const ex = await getExports();
113
+ const input = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
114
+ const inputLen = input.length;
115
+ const inputPtr = ex.alloc(inputLen);
116
+ if (inputPtr === 0) return 0;
117
+ new Uint8Array(ex.memory.buffer).set(input, inputPtr);
118
+ let conf;
119
+ try {
120
+ conf = ex.detect(inputPtr, inputLen);
121
+ } catch (e) {
122
+ try {
123
+ ex.free_result(inputPtr, inputLen);
124
+ } catch {
125
+ /* best-effort */
126
+ }
127
+ return 0; // a trap in detect → treat as "no match"
128
+ }
129
+ ex.free_result(inputPtr, inputLen);
130
+ return conf;
131
+ }
132
+
133
+ export { parse, detect };
134
+ export default { parse, detect };
package/index.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ // _index-dts.template.ts
2
+ //
3
+ // Phase 74 npm-package TypeScript declarations TEMPLATE. Hand-written from
4
+ // sdk-core/src/types.rs (the wire-format contract). NOT generated by wasm-bindgen
5
+ // — this SDK uses raw extern "C" + serde_json at the FFI boundary (ERRATUM:
6
+ // ADR-0101; wasm-bindgen/serde-wasm-bindgen are NOT used). The surface is small
7
+ // and stable (parse/detect + wire types), so hand-writing is simpler and keeps
8
+ // the declarations free of wasm-bindgen runtime coupling.
9
+ //
10
+ // Wire-format invariants (types.rs §wire-format contract):
11
+ // - `success` is a JSON boolean (true/false), never a string. Hence
12
+ // ProcessSuccess / ProcessFailure use literal `true` / `false` here.
13
+ // - `amount` is an exact decimal STRING ("128.50"), never a float/number
14
+ // (WASM-02: preserve precision across the boundary).
15
+ // - `metadata` is optional; institution-specific fields live there.
16
+
17
+ /// Parsed transaction in stable JSON wire form.
18
+ ///
19
+ /// `date` is an ISO 8601 string, `amount` is an exact decimal string (never
20
+ /// f64 — preserves precision per WASM-02), `currency` is ISO 4217.
21
+ export interface RawTransaction {
22
+ /** ISO 8601 date, e.g. "2024-01-15". */
23
+ date: string;
24
+ /** Exact decimal string ("128.50", "-35.00") — NEVER a number. */
25
+ amount: string;
26
+ /** ISO 4217 currency code, e.g. "CNY", "USD", "EUR". */
27
+ currency: string;
28
+ /** Transaction description / narration. */
29
+ description: string;
30
+ /** Payee / counterparty (optional — omitted from JSON when absent). */
31
+ payee?: string;
32
+ /** Institution-specific structured metadata (optional). */
33
+ metadata?: Record<string, unknown>;
34
+ }
35
+
36
+ /// Non-fatal warning surfaced alongside a successful result.
37
+ export interface ProcessWarning {
38
+ message: string;
39
+ }
40
+
41
+ /// Categorized processing error. Serializes as `{code, message}` matching the
42
+ /// sdk-core ProcessError enum variants.
43
+ export type ProcessError =
44
+ | { code: 'FORMAT'; message: string }
45
+ | { code: 'DETECT'; message: string }
46
+ | { code: 'ENCODING'; message: string }
47
+ | { code: 'INTERNAL'; message: string };
48
+
49
+ /// Successful parse/sanitize result.
50
+ ///
51
+ /// JSON: `{"success": true, "data": [...], "warnings": [...]}` — the `success`
52
+ /// field is always the boolean literal `true`.
53
+ export interface ProcessSuccess {
54
+ success: true;
55
+ data: RawTransaction[];
56
+ warnings?: ProcessWarning[];
57
+ }
58
+
59
+ /// Failed parse/sanitize result.
60
+ ///
61
+ /// JSON: `{"success": false, "errors": [...]}` — `success` is always `false`.
62
+ export interface ProcessFailure {
63
+ success: false;
64
+ errors: ProcessError[];
65
+ }
66
+
67
+ /// Result envelope returned by `parse()`. Discriminated union on the boolean
68
+ /// `success` field — narrow with `result.success === true`.
69
+ export type ParseResult = ProcessSuccess | ProcessFailure;
70
+
71
+ /**
72
+ * Parse CSV/JSON bytes into structured transactions.
73
+ *
74
+ * @param bytes - Statement bytes (Uint8Array or Node Buffer).
75
+ * @returns The parser's wire envelope. On success, `data` is the transaction
76
+ * array; on failure (malformed input, encoding error, internal trap),
77
+ * `errors` lists the categorized error(s). The host never throws on
78
+ * parser-level failures — they arrive as `ProcessFailure`.
79
+ */
80
+ export declare function parse(bytes: Uint8Array | Buffer): Promise<ParseResult>;
81
+
82
+ /**
83
+ * Detection confidence for the given bytes.
84
+ *
85
+ * @param bytes - Statement bytes to identify.
86
+ * @returns Confidence in {0, 50, 100}: 0 = no match, 50 = partial, 100 = definite.
87
+ */
88
+ export declare function detect(bytes: Uint8Array | Buffer): Promise<number>;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@firela/hk-hsbc-credit",
3
+ "version": "0.1.0",
4
+ "description": "firela parser/sanitizer WASM module — isomorphic Node.js + Cloudflare Worker loader (ADR-0101 compiled-only).",
5
+ "type": "module",
6
+ "main": "./index-node.cjs",
7
+ "module": "./index-worker.js",
8
+ "exports": {
9
+ ".": {
10
+ "node": {
11
+ "require": "./index-node.cjs"
12
+ },
13
+ "worker": "./index-worker.js",
14
+ "default": "./index-worker.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "index-node.cjs",
20
+ "index-worker.js",
21
+ "index.d.ts",
22
+ "hk_hsbc_credit.opt.wasm"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "firela",
29
+ "wasm",
30
+ "parser",
31
+ "bank-statement",
32
+ "isomorphic"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }