deepdebug-local-agent 1.0.22 → 1.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.
Files changed (188) hide show
  1. package/.claude/context.md +76 -0
  2. package/.claude/skills/README.md +25 -0
  3. package/.claude/skills/tdd-debugging/SKILL.md +53 -0
  4. package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
  5. package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
  6. package/.claude/standards/README.md +51 -0
  7. package/.claude/standards/clean-code.md +75 -0
  8. package/.claude/standards/code-review-checklist.md +61 -0
  9. package/.claude/standards/layered-architecture.md +163 -0
  10. package/.claude/standards/node.md +45 -0
  11. package/.claude/standards/security-owasp.md +114 -0
  12. package/.claude/standards/simplicity.md +42 -0
  13. package/.claude/standards/solid.md +64 -0
  14. package/.claude/standards/testing.md +59 -0
  15. package/.cursor/rules/standards.mdc +17 -0
  16. package/.github/workflows/build-byoc-image.yml +315 -0
  17. package/.github/workflows/deploy-gcp.yml +102 -0
  18. package/.github/workflows/npm-publish.yml +69 -8
  19. package/AGENTS.md +21 -0
  20. package/CHANGELOG.md +69 -0
  21. package/CLAUDE.md +13 -0
  22. package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
  23. package/Dockerfile +4 -0
  24. package/Dockerfile.full +55 -3
  25. package/README.md +81 -0
  26. package/bin/install.js +16 -5
  27. package/cloudbuild-full.yaml +57 -0
  28. package/docker-compose.yml +5 -3
  29. package/docs/OBSERVABILITY.md +164 -0
  30. package/docs/byoc-agent-image.md +70 -0
  31. package/docs/graphify-packaging-plan.md +133 -0
  32. package/docs/graphify-validation-run-2026-10-28.md +243 -0
  33. package/docs/legacy-connectors.md +268 -0
  34. package/docs/releasing.md +43 -0
  35. package/docs/signed-commands.md +163 -0
  36. package/env.example +30 -0
  37. package/helm/Chart.yaml +2 -2
  38. package/helm/templates/deployment.yaml +13 -3
  39. package/helm/templates/networkpolicy.yaml +22 -0
  40. package/helm/templates/secret.yaml +1 -0
  41. package/helm/values.yaml +13 -14
  42. package/mcp-server.js +4 -1
  43. package/package.json +12 -4
  44. package/scripts/sync-fixtures.mjs +246 -0
  45. package/scripts/upgrade-observability-push.sh +227 -0
  46. package/src/adapters/as400/as400-parser.js +12 -0
  47. package/src/adapters/as400/index.js +134 -0
  48. package/src/adapters/cobol/cobol-parser.js +37 -0
  49. package/src/adapters/cobol/index.js +151 -0
  50. package/src/adapters/ibmmq/ibmmq-parser.js +23 -0
  51. package/src/adapters/ibmmq/index.js +137 -0
  52. package/src/adapters/index.js +79 -0
  53. package/src/adapters/oracle-forms/index.js +145 -0
  54. package/src/adapters/oracle-forms/oracle-forms-parser.js +11 -0
  55. package/src/adapters/sap/index.js +146 -0
  56. package/src/adapters/sap/sap-parser.js +41 -0
  57. package/src/ai-engine.js +22 -22
  58. package/src/config/trusted-signing-keys.json +3 -0
  59. package/src/detectors/build-tool-detector.js +50 -0
  60. package/src/detectors/multi-project-detector.js +22 -22
  61. package/src/detectors/service-detector.js +24 -24
  62. package/src/exec-utils.js +26 -4
  63. package/src/git/base-git-provider.js +117 -117
  64. package/src/git/bitbucket-provider.js +2 -2
  65. package/src/git/git-provider-registry.js +18 -18
  66. package/src/git/github-provider.js +13 -13
  67. package/src/git/multi-repo-pr.js +161 -0
  68. package/src/mcp-http-server.js +32 -16
  69. package/src/observability/bundle-files.js +418 -0
  70. package/src/observability/detect.js +151 -0
  71. package/src/observability/provision.js +212 -0
  72. package/src/observability/render-cli.js +59 -0
  73. package/src/observability/routes.js +88 -0
  74. package/src/patch/patch-engine.js +28 -28
  75. package/src/patch-manager.js +85 -85
  76. package/src/patch_bkp.js +14 -14
  77. package/src/ports.js +4 -4
  78. package/src/routes/workspace.route.js +47 -16
  79. package/src/runtimes/base-runtime.js +75 -75
  80. package/src/runtimes/java/gradle-runtime.js +17 -17
  81. package/src/runtimes/java/java-integrations.js +25 -25
  82. package/src/runtimes/java/maven-runtime.js +34 -34
  83. package/src/runtimes/node/node-integrations.js +1 -1
  84. package/src/runtimes/node/npm-runtime.js +14 -14
  85. package/src/runtimes/node/yarn-runtime.js +4 -4
  86. package/src/runtimes/runtime-registry.js +25 -25
  87. package/src/server-local.js +4 -4
  88. package/src/server.js +1412 -335
  89. package/src/siblings/burst-suppressor.js +89 -0
  90. package/src/siblings/capabilities.js +273 -0
  91. package/src/siblings/env-compat.cjs +58 -0
  92. package/src/siblings/incident-forwarder.js +320 -0
  93. package/src/siblings/logger.js +15 -0
  94. package/src/siblings/metrics.js +94 -0
  95. package/src/utils/agent-auth.js +184 -0
  96. package/src/utils/approval-token.js +56 -0
  97. package/src/utils/bind-host.js +29 -0
  98. package/src/utils/branch-refspec.js +98 -0
  99. package/src/utils/child-env.js +34 -0
  100. package/src/utils/environment-diagnostics.js +55 -55
  101. package/src/utils/exec-policy.js +962 -0
  102. package/src/utils/exec-utils.js +15 -15
  103. package/src/utils/file-count.js +46 -0
  104. package/src/utils/fs-utils.js +10 -10
  105. package/src/utils/multi-repo-open.js +132 -0
  106. package/src/utils/open-gate.js +183 -0
  107. package/src/utils/ranged-read.js +65 -0
  108. package/src/utils/request-guards.js +77 -0
  109. package/src/utils/request-signing.js +342 -0
  110. package/src/utils/root-recovery.js +244 -0
  111. package/src/utils/safe-exec-args.js +58 -0
  112. package/src/utils/sandbox.js +182 -0
  113. package/src/utils/search-outcome.js +56 -0
  114. package/src/utils/workspace-root.js +80 -0
  115. package/src/validators/syntax-validator.js +149 -0
  116. package/src/workspace/detect-port.js +16 -16
  117. package/src/workspace/file-reader.js +1 -1
  118. package/src/workspace/process-manager.js +46 -46
  119. package/src/workspace-manager.js +86 -24
  120. package/test/adapters/as400/as400.test.js +121 -0
  121. package/test/adapters/cobol/cobol.test.js +217 -0
  122. package/test/adapters/ibmmq/ibmmq.test.js +122 -0
  123. package/test/adapters/oracle-forms/oracle-forms.test.js +180 -0
  124. package/test/adapters/registry.test.js +108 -0
  125. package/test/adapters/sap/sap.test.js +142 -0
  126. package/test/agent-auth.test.js +194 -0
  127. package/test/bind-host.test.js +26 -0
  128. package/test/branch-refspec.test.js +224 -0
  129. package/test/burst-suppressor.test.js +60 -0
  130. package/test/capabilities-access.test.js +47 -0
  131. package/test/capabilities.test.js +87 -0
  132. package/test/child-env.test.js +23 -0
  133. package/test/env-compat.test.js +71 -0
  134. package/test/exec-policy.test.js +463 -0
  135. package/test/file-count.test.js +64 -0
  136. package/test/incident-forwarder.test.js +231 -0
  137. package/test/multi-repo-open.test.js +180 -0
  138. package/test/multi-repo-pr.test.js +129 -0
  139. package/test/observability-detect.test.js +199 -0
  140. package/test/open-gate.test.js +341 -0
  141. package/test/ranged-read.test.js +60 -0
  142. package/test/request-guards.test.js +91 -0
  143. package/test/request-signing.test.js +284 -0
  144. package/test/root-recovery-sidecar.test.js +171 -0
  145. package/test/root-recovery.test.js +274 -0
  146. package/test/safe-exec-args.test.js +58 -0
  147. package/test/sandbox.test.js +98 -0
  148. package/test/search-outcome.test.js +50 -0
  149. package/test/workspace-root.test.js +64 -0
  150. package/tools/graphify-validation/README.md +92 -0
  151. package/tools/graphify-validation/config/questions.json +31 -0
  152. package/tools/graphify-validation/config/repos.json +9 -0
  153. package/tools/graphify-validation/config/symbols.json +6 -0
  154. package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
  155. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
  156. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
  157. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
  158. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
  159. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
  160. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
  161. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
  162. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
  163. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
  164. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
  165. package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
  166. package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
  167. package/tools/graphify-validation/lib/graph_metrics.py +96 -0
  168. package/tools/graphify-validation/lib/gv_common.py +130 -0
  169. package/tools/graphify-validation/lib/licenses.py +47 -0
  170. package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
  171. package/tools/graphify-validation/lib/recall.py +115 -0
  172. package/tools/graphify-validation/lib/report.py +226 -0
  173. package/tools/graphify-validation/lib/run_all.py +237 -0
  174. package/tools/graphify-validation/lib/run_graphify.py +144 -0
  175. package/tools/graphify-validation/lib/rusage_helper.py +17 -0
  176. package/tools/graphify-validation/lib/sample_edges.py +246 -0
  177. package/tools/graphify-validation/lib/spring_fixture.py +81 -0
  178. package/tools/graphify-validation/requirements-py311.lock +508 -0
  179. package/tools/graphify-validation/requirements-tools.in +1 -0
  180. package/tools/graphify-validation/requirements-tools.lock +371 -0
  181. package/tools/graphify-validation/requirements.in +1 -0
  182. package/tools/graphify-validation/requirements.lock +502 -0
  183. package/tools/graphify-validation/run.sh +43 -0
  184. package/tools/graphify-validation/tests/test_harness.py +151 -0
  185. package/cloudbuild-agent-qa.yaml +0 -43
  186. package/cloudbuild-agent-staging.yaml +0 -40
  187. package/cloudbuild.yaml.deprecated +0 -42
  188. package/src/local-agent-source.tar.gz +0 -0
@@ -0,0 +1,37 @@
1
+ // src/adapters/cobol/cobol-parser.js
2
+ // Multi-pattern parser surface for COBOL/JES2 spool exports. Each regex is
3
+ // exported as a named const for isolated unit-testing (T-2-03-equivalent
4
+ // mitigation surface). The `extractCobolCode` helper picks the first matching
5
+ // pattern in priority order.
6
+ //
7
+ // Priority (highest first): SYSTEM COMPLETION CODE → SQLCODE → FILE STATUS →
8
+ // ABEND CODE → IEF info code. The first three are hard error conditions; IEF
9
+ // info codes are the lowest priority because the same line often carries
10
+ // the actual completion code (e.g. `IEF450I ... ABEND=S0C7`) and the
11
+ // system-completion match is more specific.
12
+
13
+ export const COBOL_COMPLETION_REGEX = /SYSTEM COMPLETION CODE=([SU]?\w{3,4})/;
14
+ export const COBOL_SQLCODE_REGEX = /SQLCODE\s*=?\s*(-?\d+)/;
15
+ export const COBOL_IEF_REGEX = /\b(IEF\d{3}I)\b/;
16
+ export const COBOL_FILE_STATUS_REGEX = /FILE STATUS\s+(\d+)/;
17
+ export const COBOL_ABEND_CODE_REGEX = /ABEND CODE=(\w+)/;
18
+
19
+ export function extractCobolCode(line) {
20
+ let m = line.match(COBOL_COMPLETION_REGEX);
21
+ if (m) {
22
+ const raw = m[1];
23
+ // System codes start with 'S' (system) or 'U' (user). 0C7 → S0C7
24
+ // normalization per IBM convention.
25
+ if (/^[SU]/.test(raw)) return raw;
26
+ return `S${raw}`;
27
+ }
28
+ m = line.match(COBOL_SQLCODE_REGEX);
29
+ if (m) return m[1]; // includes leading '-' for negative codes
30
+ m = line.match(COBOL_FILE_STATUS_REGEX);
31
+ if (m) return `FILE_STATUS_${m[1]}`;
32
+ m = line.match(COBOL_ABEND_CODE_REGEX);
33
+ if (m) return m[1];
34
+ m = line.match(COBOL_IEF_REGEX);
35
+ if (m) return m[1];
36
+ return null;
37
+ }
@@ -0,0 +1,151 @@
1
+ // src/adapters/cobol/index.js
2
+ // CobolLogAdapter — watches arbitrary files in the configured JES2 spool
3
+ // directory; emits 6-key error events. AD-1 lifecycle, AD-3 polling, AD-4
4
+ // byte-position dedup with TAIL-on-startup default, AD-5 per-file ordering,
5
+ // AD-6 CRLF tolerance + 80-col fixed-width column-1 preservation (z/OS spool
6
+ // trap). T-2-01 mitigation: default polling interval 5000 ms.
7
+ //
8
+ // The glob is `*` (any file) — JES2 spool exports have arbitrary names.
9
+ // This is broader than sibling adapters but the parser regexes are narrow
10
+ // enough (SYSTEM COMPLETION CODE, SQLCODE, IEF, FILE STATUS, ABEND CODE)
11
+ // that lines without any match are silently dropped.
12
+
13
+ import { EventEmitter } from 'node:events';
14
+ import { stat, open } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import chokidar from 'chokidar';
17
+ import { extractCobolCode } from './cobol-parser.js';
18
+
19
+ const ADAPTER = 'cobol';
20
+
21
+ export class CobolLogAdapter extends EventEmitter {
22
+ constructor(config = {}) {
23
+ super();
24
+ this.spoolDir = config.spoolDir || '/var/spool/jes2';
25
+ this.interval = config.interval ?? 5000;
26
+ this.replayOnStart = config.replayOnStart === true;
27
+ this._watcher = null;
28
+ this._offsets = new Map(); // filePath → byte offset
29
+ this._inodes = new Map(); // filePath → inode
30
+ this._isRunning = false;
31
+ }
32
+
33
+ get isRunning() {
34
+ return this._isRunning;
35
+ }
36
+
37
+ async start() {
38
+ if (this._isRunning) return;
39
+ this._isRunning = true;
40
+
41
+ // Glob bound to any file in spool dir. depth:0 prevents recursion.
42
+ const glob = join(this.spoolDir, '*');
43
+
44
+ this._watcher = chokidar.watch(glob, {
45
+ usePolling: true,
46
+ interval: this.interval,
47
+ ignoreInitial: false, // we handle initial ourselves via TAIL seek
48
+ awaitWriteFinish: false,
49
+ depth: 0, // do not recurse into subdirs
50
+ });
51
+
52
+ const onAdd = async (filePath) => {
53
+ if (this.replayOnStart) {
54
+ this._offsets.set(filePath, 0);
55
+ // fall through to _readNew below
56
+ } else {
57
+ try {
58
+ const st = await stat(filePath);
59
+ this._offsets.set(filePath, st.size);
60
+ this._inodes.set(filePath, st.ino);
61
+ return; // TAIL mode — don't emit pre-existing content
62
+ } catch {
63
+ return;
64
+ }
65
+ }
66
+ this._readNew(filePath).catch(() => { /* ENOENT silence */ });
67
+ };
68
+
69
+ this._watcher.on('add', onAdd);
70
+ this._watcher.on('change', (fp) => {
71
+ this._readNew(fp).catch(() => { /* ENOENT silence */ });
72
+ });
73
+
74
+ // Swallow chokidar's own bubbled errors (e.g., watch-path ENOENT) —
75
+ // dual-layer suppression with the per-read try/catch ensures full
76
+ // ENOENT silence (canonical pattern from Plan 02-07).
77
+ this._watcher.on('error', () => {});
78
+ }
79
+
80
+ async stop() {
81
+ if (!this._isRunning) return;
82
+ this._isRunning = false;
83
+ if (this._watcher) {
84
+ await this._watcher.close().catch(() => {});
85
+ this._watcher = null;
86
+ }
87
+ this._offsets.clear();
88
+ this._inodes.clear();
89
+ }
90
+
91
+ async _readNew(filePath) {
92
+ let st;
93
+ try {
94
+ st = await stat(filePath);
95
+ } catch {
96
+ return; // ENOENT silence
97
+ }
98
+ if (!st.isFile()) return; // skip dirs that may surface from broad `*` glob
99
+
100
+ const lastOffset = this._offsets.get(filePath) ?? 0;
101
+ const lastInode = this._inodes.get(filePath);
102
+
103
+ // Rotation: inode changed OR size shrunk → reset to 0
104
+ if ((lastInode !== undefined && st.ino !== lastInode) || st.size < lastOffset) {
105
+ this._offsets.set(filePath, 0);
106
+ this._inodes.set(filePath, st.ino);
107
+ return this._readNew(filePath); // re-enter with offset=0
108
+ }
109
+ this._inodes.set(filePath, st.ino);
110
+
111
+ if (st.size <= lastOffset) return;
112
+
113
+ const fh = await open(filePath, 'r');
114
+ try {
115
+ const len = st.size - lastOffset;
116
+ const buf = Buffer.alloc(len);
117
+ await fh.read(buf, 0, len, lastOffset);
118
+ this._offsets.set(filePath, st.size);
119
+ const text = buf.toString('utf8');
120
+ // CRLF-tolerant split: \r?\n splits on either ending. The trailing
121
+ // \r is consumed by the regex so event.message is clean of artifacts
122
+ // (AD-6 z/OS spool trap).
123
+ const lines = text.split(/\r?\n/);
124
+ const sourceDir = filePath.split('/').slice(0, -1).join('/');
125
+ for (const line of lines) {
126
+ // CRITICAL: do NOT trim. Column-1 in z/OS spool = print control char
127
+ // (0/1/space) and is semantically meaningful (page eject markers,
128
+ // ABEND headers, etc.) — AD-6 fixed-width trap. Skip only truly
129
+ // empty lines; lines that are pure whitespace (column-1 padding)
130
+ // are also skipped because no parser regex would match them.
131
+ if (line.length === 0) continue;
132
+ const code = extractCobolCode(line);
133
+ if (!code) continue;
134
+ // 6-key frozen event payload (AD-4 contract — extra keys forbidden)
135
+ const event = Object.freeze({
136
+ adapter: ADAPTER,
137
+ errorCode: code,
138
+ file: filePath,
139
+ message: line,
140
+ source: sourceDir,
141
+ timestamp: new Date().toISOString(),
142
+ });
143
+ this.emit('error', event);
144
+ }
145
+ } finally {
146
+ await fh.close();
147
+ }
148
+ }
149
+ }
150
+
151
+ export default CobolLogAdapter;
@@ -0,0 +1,23 @@
1
+ // src/adapters/ibmmq/ibmmq-parser.js
2
+ // T-2-03 isolation surface: regexes exported as named constants for unit testing.
3
+ // Adapter binds chokidar to AMQERR0[1-3].LOG glob only; these regexes never
4
+ // run against arbitrary text by the adapter itself.
5
+
6
+ // Pattern: AMQ followed by 4 digits, optional suffix letter (E error / W warn / I info).
7
+ // IBM MQ V9 message-code shape per Knowledge Center.
8
+ export const IBMMQ_AMQ_REGEX = /\b(AMQ\d{4}[EWI]?)\b/;
9
+
10
+ // Secondary capture: MQRC reason code from "ArithInsert1(NNNN)" (formal SLOG)
11
+ // or "returned NNNN" (free-form prose). Group 1 = ArithInsert1 form, group 2 = "returned" form.
12
+ export const IBMMQ_MQRC_REGEX = /(?:ArithInsert1\((\d+)\)|returned\s+(\d+))/;
13
+
14
+ export function extractAmqCode(line) {
15
+ const m = line.match(IBMMQ_AMQ_REGEX);
16
+ return m ? m[1] : null;
17
+ }
18
+
19
+ export function extractMqrc(line) {
20
+ const m = line.match(IBMMQ_MQRC_REGEX);
21
+ if (!m) return null;
22
+ return m[1] || m[2] || null;
23
+ }
@@ -0,0 +1,137 @@
1
+ // src/adapters/ibmmq/index.js
2
+ // IbmMqLogAdapter — watches AMQERR0[1-3].LOG in the configured MQ errors dir;
3
+ // emits 6-key error events. AD-1 lifecycle, AD-3 polling, AD-4 byte-position
4
+ // dedup with TAIL-on-startup default, AD-5 per-file ordering.
5
+ // T-2-01 mitigation: default polling interval 5000 ms.
6
+
7
+ import { EventEmitter } from 'node:events';
8
+ import { stat, open } from 'node:fs/promises';
9
+ import { basename, join } from 'node:path';
10
+ import chokidar from 'chokidar';
11
+ import { extractAmqCode } from './ibmmq-parser.js';
12
+
13
+ const ADAPTER = 'ibmmq';
14
+
15
+ // T-2-03 anchor: only AMQERR01.LOG / AMQERR02.LOG / AMQERR03.LOG are accepted.
16
+ // Equivalent to the glob AMQERR0[1-3].LOG, but enforced post-watch in the handler
17
+ // because chokidar usePolling:true does not engage with bracket-class globs on
18
+ // macOS/Node 24 (verified empirically: AMQERR*.LOG fires events; AMQERR0[1-3].LOG
19
+ // fires nothing). The watcher uses AMQERR*.LOG; this regex narrows back to 01..03.
20
+ const IBMMQ_FILENAME_ANCHOR = /^AMQERR0[1-3]\.LOG$/;
21
+
22
+ export class IbmMqLogAdapter extends EventEmitter {
23
+ constructor(config = {}) {
24
+ super();
25
+ this.errorsDir = config.errorsDir || '/var/mqm/qmgrs/QM_PRD01/errors';
26
+ this.interval = config.interval ?? 5000;
27
+ this.replayOnStart = config.replayOnStart === true;
28
+ this._watcher = null;
29
+ this._offsets = new Map(); // filePath → byte offset
30
+ this._inodes = new Map(); // filePath → inode
31
+ this._isRunning = false;
32
+ }
33
+
34
+ get isRunning() { return this._isRunning; }
35
+
36
+ async start() {
37
+ if (this._isRunning) return;
38
+ this._isRunning = true;
39
+
40
+ // Watch glob = AMQERR*.LOG (chokidar polling-compatible); the strict anchor
41
+ // AMQERR0[1-3].LOG is enforced by IBMMQ_FILENAME_ANCHOR on every event.
42
+ const glob = join(this.errorsDir, 'AMQERR*.LOG');
43
+
44
+ this._watcher = chokidar.watch(glob, {
45
+ usePolling: true,
46
+ interval: this.interval,
47
+ ignoreInitial: false, // we handle initial ourselves via TAIL seek
48
+ awaitWriteFinish: false,
49
+ });
50
+
51
+ const isAnchoredFile = (fp) => IBMMQ_FILENAME_ANCHOR.test(basename(fp));
52
+
53
+ const onAdd = async (filePath) => {
54
+ if (!isAnchoredFile(filePath)) return; // T-2-03: drop non-anchored
55
+ if (this.replayOnStart) {
56
+ this._offsets.set(filePath, 0);
57
+ // fall through to _readNew below
58
+ } else {
59
+ try {
60
+ const st = await stat(filePath);
61
+ this._offsets.set(filePath, st.size);
62
+ this._inodes.set(filePath, st.ino);
63
+ return; // TAIL mode — don't emit pre-existing content
64
+ } catch { return; }
65
+ }
66
+ this._readNew(filePath).catch(() => { /* ENOENT silence */ });
67
+ };
68
+
69
+ this._watcher.on('add', onAdd);
70
+ this._watcher.on('change', (fp) => {
71
+ if (!isAnchoredFile(fp)) return; // T-2-03: drop non-anchored
72
+ this._readNew(fp).catch(() => { /* ENOENT silence */ });
73
+ });
74
+
75
+ // Swallow chokidar's own bubbled errors (e.g., watch-path ENOENT).
76
+ this._watcher.on('error', () => {});
77
+ }
78
+
79
+ async stop() {
80
+ if (!this._isRunning) return;
81
+ this._isRunning = false;
82
+ if (this._watcher) {
83
+ await this._watcher.close().catch(() => {});
84
+ this._watcher = null;
85
+ }
86
+ this._offsets.clear();
87
+ this._inodes.clear();
88
+ }
89
+
90
+ async _readNew(filePath) {
91
+ let st;
92
+ try { st = await stat(filePath); } catch { return; } // ENOENT silence
93
+
94
+ const lastOffset = this._offsets.get(filePath) ?? 0;
95
+ const lastInode = this._inodes.get(filePath);
96
+
97
+ // Rotation: inode changed OR size shrunk → reset to 0
98
+ if ((lastInode !== undefined && st.ino !== lastInode) || st.size < lastOffset) {
99
+ this._offsets.set(filePath, 0);
100
+ this._inodes.set(filePath, st.ino);
101
+ return this._readNew(filePath); // re-enter with offset=0
102
+ }
103
+ this._inodes.set(filePath, st.ino);
104
+
105
+ if (st.size <= lastOffset) return;
106
+
107
+ const fh = await open(filePath, 'r');
108
+ try {
109
+ const len = st.size - lastOffset;
110
+ const buf = Buffer.alloc(len);
111
+ await fh.read(buf, 0, len, lastOffset);
112
+ this._offsets.set(filePath, st.size);
113
+ const text = buf.toString('utf8');
114
+ const lines = text.split(/\r?\n/);
115
+ const sourceDir = filePath.split('/').slice(0, -1).join('/');
116
+ for (const line of lines) {
117
+ if (line.trim().length === 0) continue;
118
+ const code = extractAmqCode(line);
119
+ if (!code) continue;
120
+ // 6-key frozen event payload (AD-4 contract — extra keys forbidden)
121
+ const event = Object.freeze({
122
+ adapter: ADAPTER,
123
+ errorCode: code,
124
+ file: filePath,
125
+ message: line,
126
+ source: sourceDir,
127
+ timestamp: new Date().toISOString(),
128
+ });
129
+ this.emit('error', event);
130
+ }
131
+ } finally {
132
+ await fh.close();
133
+ }
134
+ }
135
+ }
136
+
137
+ export default IbmMqLogAdapter;
@@ -0,0 +1,79 @@
1
+ // src/adapters/index.js
2
+ // Phase 2 — Legacy connectors registry. AD-1: returns unstarted instances; caller starts.
3
+ //
4
+ // Public API:
5
+ // loadAdapters(config) → unstarted Adapter[]
6
+ // stopAll(adapters, timeoutMs?) → Promise<void>, never throws
7
+ // ALLOWED_STACK_LIST → frozen string[] of valid stack names
8
+
9
+ import { SapLogAdapter } from './sap/index.js';
10
+ import { IbmMqLogAdapter } from './ibmmq/index.js';
11
+ import { As400LogAdapter } from './as400/index.js';
12
+ import { OracleFormsLogAdapter } from './oracle-forms/index.js';
13
+ import { CobolLogAdapter } from './cobol/index.js';
14
+
15
+ const ALLOWED_STACKS = Object.freeze(['sap', 'ibmmq', 'as400', 'oracle-forms', 'cobol']);
16
+
17
+ const FACTORIES = Object.freeze({
18
+ 'sap': (cfg) => new SapLogAdapter(cfg),
19
+ 'ibmmq': (cfg) => new IbmMqLogAdapter(cfg),
20
+ 'as400': (cfg) => new As400LogAdapter(cfg),
21
+ 'oracle-forms': (cfg) => new OracleFormsLogAdapter(cfg),
22
+ 'cobol': (cfg) => new CobolLogAdapter(cfg),
23
+ });
24
+
25
+ /**
26
+ * Load configured adapter instances WITHOUT starting them (AD-1).
27
+ *
28
+ * @param {object} config
29
+ * @param {string[]} config.legacyStacks subset of ['sap','ibmmq','as400','oracle-forms','cobol']
30
+ * @param {object} [config.sap] per-adapter config object
31
+ * @param {object} [config.ibmmq]
32
+ * @param {object} [config.as400]
33
+ * @param {object} [config['oracle-forms']]
34
+ * @param {object} [config.cobol]
35
+ * @returns {Array} unstarted adapters; caller invokes .start() on each
36
+ * @throws {Error} on unknown stack name (message names the offender)
37
+ */
38
+ export function loadAdapters(config = {}) {
39
+ const stacks = Array.isArray(config.legacyStacks) ? config.legacyStacks : [];
40
+ for (const s of stacks) {
41
+ if (!ALLOWED_STACKS.includes(s)) {
42
+ throw new Error(`loadAdapters: unknown stack '${s}'. Allowed: ${ALLOWED_STACKS.join(', ')}`);
43
+ }
44
+ }
45
+ return stacks.map((s) => FACTORIES[s](config[s] || {}));
46
+ }
47
+
48
+ /**
49
+ * Stop all adapters in parallel with overall timeout (AC-11).
50
+ * Used by SIGTERM handler. Never throws — failures are swallowed so the
51
+ * shutdown path always returns and the event loop drains.
52
+ *
53
+ * @param {Array} adapters
54
+ * @param {number} [timeoutMs=5000]
55
+ * @returns {Promise<void>}
56
+ */
57
+ export async function stopAll(adapters, timeoutMs = 5000) {
58
+ if (!Array.isArray(adapters) || adapters.length === 0) return;
59
+ const stopPromises = adapters.map((a) =>
60
+ Promise.resolve()
61
+ .then(() => (typeof a?.stop === 'function' ? a.stop() : undefined))
62
+ .catch(() => { /* swallow per never-throw contract */ })
63
+ );
64
+ // Bounded timer so a non-resolving stop() cannot block process exit.
65
+ let timer;
66
+ const timeoutPromise = new Promise((resolve) => {
67
+ timer = setTimeout(resolve, timeoutMs);
68
+ // Per project convention, unref the timer so it never holds the loop open
69
+ // beyond the rest of the work (AD-3 timer hygiene).
70
+ if (typeof timer.unref === 'function') timer.unref();
71
+ });
72
+ try {
73
+ await Promise.race([Promise.all(stopPromises), timeoutPromise]);
74
+ } finally {
75
+ if (timer) clearTimeout(timer);
76
+ }
77
+ }
78
+
79
+ export const ALLOWED_STACK_LIST = ALLOWED_STACKS;
@@ -0,0 +1,145 @@
1
+ // src/adapters/oracle-forms/index.js
2
+ // OracleFormsLogAdapter — watches *.trc files in the configured Forms trace
3
+ // dir; emits 6-key error events. AD-1 lifecycle, AD-3 polling, AD-4 byte-position
4
+ // dedup with TAIL-on-startup default, AD-5 per-file ordering, AD-6 Latin-1
5
+ // mojibake-trap tolerance via latin1 decode (never throws on any byte).
6
+ // T-2-01 mitigation: default polling interval 5000 ms.
7
+
8
+ import { EventEmitter } from 'node:events';
9
+ import { stat, open } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ import chokidar from 'chokidar';
12
+ import { extractFormsCode } from './oracle-forms-parser.js';
13
+
14
+ const ADAPTER = 'oracle-forms';
15
+
16
+ // Decode buffer using Latin-1 (single-byte) encoding — never throws on any
17
+ // byte sequence. ORA/FRM message codes are ASCII so the regex match is
18
+ // unaffected by the choice; accented free-text payload bytes survive into
19
+ // `message` as Latin-1 characters (mojibake-tolerant per AD-6 Forms trap).
20
+ function decodeTolerant(buf) {
21
+ return buf.toString('latin1');
22
+ }
23
+
24
+ export class OracleFormsLogAdapter extends EventEmitter {
25
+ constructor(config = {}) {
26
+ super();
27
+ this.traceDir = config.traceDir || '/u01/oracle/forms/trace';
28
+ this.interval = config.interval ?? 5000;
29
+ this.replayOnStart = config.replayOnStart === true;
30
+ this._watcher = null;
31
+ this._offsets = new Map(); // filePath → byte offset
32
+ this._inodes = new Map(); // filePath → inode
33
+ this._isRunning = false;
34
+ }
35
+
36
+ get isRunning() {
37
+ return this._isRunning;
38
+ }
39
+
40
+ async start() {
41
+ if (this._isRunning) return;
42
+ this._isRunning = true;
43
+
44
+ // Glob bound to *.trc only (wildcard — chokidar polling-mode bracket-class
45
+ // workaround from Plan 02-08 does NOT apply here; *.trc is a plain wildcard).
46
+ const glob = join(this.traceDir, '*.trc');
47
+
48
+ this._watcher = chokidar.watch(glob, {
49
+ usePolling: true,
50
+ interval: this.interval,
51
+ ignoreInitial: false, // we handle initial ourselves via TAIL seek
52
+ awaitWriteFinish: false,
53
+ });
54
+
55
+ const onAdd = async (filePath) => {
56
+ if (this.replayOnStart) {
57
+ this._offsets.set(filePath, 0);
58
+ // fall through to _readNew below
59
+ } else {
60
+ try {
61
+ const st = await stat(filePath);
62
+ this._offsets.set(filePath, st.size);
63
+ this._inodes.set(filePath, st.ino);
64
+ return; // TAIL mode — don't emit pre-existing content
65
+ } catch {
66
+ return;
67
+ }
68
+ }
69
+ this._readNew(filePath).catch(() => { /* ENOENT silence */ });
70
+ };
71
+
72
+ this._watcher.on('add', onAdd);
73
+ this._watcher.on('change', (fp) => {
74
+ this._readNew(fp).catch(() => { /* ENOENT silence */ });
75
+ });
76
+
77
+ // Swallow chokidar's own bubbled errors (e.g., watch-path ENOENT) —
78
+ // dual-layer suppression with the per-read try/catch ensures full
79
+ // ENOENT silence (canonical pattern from Plan 02-07).
80
+ this._watcher.on('error', () => {});
81
+ }
82
+
83
+ async stop() {
84
+ if (!this._isRunning) return;
85
+ this._isRunning = false;
86
+ if (this._watcher) {
87
+ await this._watcher.close().catch(() => {});
88
+ this._watcher = null;
89
+ }
90
+ this._offsets.clear();
91
+ this._inodes.clear();
92
+ }
93
+
94
+ async _readNew(filePath) {
95
+ let st;
96
+ try {
97
+ st = await stat(filePath);
98
+ } catch {
99
+ return; // ENOENT silence
100
+ }
101
+
102
+ const lastOffset = this._offsets.get(filePath) ?? 0;
103
+ const lastInode = this._inodes.get(filePath);
104
+
105
+ // Rotation: inode changed OR size shrunk → reset to 0
106
+ if ((lastInode !== undefined && st.ino !== lastInode) || st.size < lastOffset) {
107
+ this._offsets.set(filePath, 0);
108
+ this._inodes.set(filePath, st.ino);
109
+ return this._readNew(filePath); // re-enter with offset=0
110
+ }
111
+ this._inodes.set(filePath, st.ino);
112
+
113
+ if (st.size <= lastOffset) return;
114
+
115
+ const fh = await open(filePath, 'r');
116
+ try {
117
+ const len = st.size - lastOffset;
118
+ const buf = Buffer.alloc(len);
119
+ await fh.read(buf, 0, len, lastOffset);
120
+ this._offsets.set(filePath, st.size);
121
+ const text = decodeTolerant(buf);
122
+ const lines = text.split(/\r?\n/);
123
+ const sourceDir = filePath.split('/').slice(0, -1).join('/');
124
+ for (const line of lines) {
125
+ if (line.trim().length === 0) continue;
126
+ const code = extractFormsCode(line);
127
+ if (!code) continue;
128
+ // 6-key frozen event payload (AD-4 contract — extra keys forbidden)
129
+ const event = Object.freeze({
130
+ adapter: ADAPTER,
131
+ errorCode: code,
132
+ file: filePath,
133
+ message: line,
134
+ source: sourceDir,
135
+ timestamp: new Date().toISOString(),
136
+ });
137
+ this.emit('error', event);
138
+ }
139
+ } finally {
140
+ await fh.close();
141
+ }
142
+ }
143
+ }
144
+
145
+ export default OracleFormsLogAdapter;
@@ -0,0 +1,11 @@
1
+ // src/adapters/oracle-forms/oracle-forms-parser.js
2
+ // T-2-03 isolation surface: regex exported as a named const so it can be
3
+ // unit-tested against arbitrary text decoupled from the file-watching
4
+ // machinery in index.js. The watcher itself is bound to a literal *.trc
5
+ // glob so this regex never engages arbitrary text by construction.
6
+ export const ORACLE_FORMS_REGEX = /(?:Line \d+:|^)\s*(ORA-\d{5}|FRM-\d{5}):\s*(.+)/;
7
+
8
+ export function extractFormsCode(line) {
9
+ const m = line.match(ORACLE_FORMS_REGEX);
10
+ return m ? m[1] : null;
11
+ }